This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,219 @@
<audio id="audio" title="23 | Host容器Tomcat如何实现热部署和热加载" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a7/73/a7049437f86d62aeb23a8c93d15f4473.mp3"></audio>
从这一期我们开始学习Tomcat的容器模块来聊一聊各容器组件实现的功能主要有热部署热加载、类加载机制以及Servlet规范的实现。最后还会谈到Spring Boot是如何与Web容器进行交互的。
今天我们首先来看热部署和热加载。要在运行的过程中升级Web应用如果你不想重启系统实现的方式有两种热加载和热部署。
那如何实现热部署和热加载呢?它们跟类加载机制有关,具体来说就是:
- 热加载的实现方式是Web容器启动一个后台线程定期检测类文件的变化如果有变化就重新加载类在这个过程中不会清空Session ,一般用在开发环境。
- 热部署原理类似也是由后台线程定时检测Web应用的变化但它会重新加载整个Web应用。这种方式会清空Session比热加载更加干净、彻底一般用在生产环境。
今天我们来学习一下Tomcat是如何用后台线程来实现热加载和热部署的。Tomcat通过开启后台线程使得各个层次的容器组件都有机会完成一些周期性任务。我们在实际工作中往往也需要执行一些周期性的任务比如监控程序周期性拉取系统的健康状态就可以借鉴这种设计。
## Tomcat的后台线程
要说开启后台线程做周期性的任务有经验的同学马上会想到线程池中的ScheduledThreadPoolExecutor它除了具有线程池的功能还能够执行周期性的任务。Tomcat就是通过它来开启后台线程的
```
bgFuture = exec.scheduleWithFixedDelay(
new ContainerBackgroundProcessor(),//要执行的Runnable
backgroundProcessorDelay, //第一次执行延迟多久
backgroundProcessorDelay, //之后每次执行间隔多久
TimeUnit.SECONDS); //时间单位
```
上面的代码调用了scheduleWithFixedDelay方法传入了四个参数第一个参数就是要周期性执行的任务类ContainerBackgroundProcessor它是一个Runnable同时也是ContainerBase的内部类ContainerBase是所有容器组件的基类我们来回忆一下容器组件有哪些有Engine、Host、Context和Wrapper等它们具有父子关系。
**ContainerBackgroundProcessor实现**
我们接来看ContainerBackgroundProcessor具体是如何实现的。
```
protected class ContainerBackgroundProcessor implements Runnable {
@Override
public void run() {
//请注意这里传入的参数是&quot;宿主类&quot;的实例
processChildren(ContainerBase.this);
}
protected void processChildren(Container container) {
try {
//1. 调用当前容器的backgroundProcess方法。
container.backgroundProcess();
//2. 遍历所有的子容器递归调用processChildren
//这样当前容器的子孙都会被处理
Container[] children = container.findChildren();
for (int i = 0; i &lt; children.length; i++) {
//这里请你注意容器基类有个变量叫做backgroundProcessorDelay如果大于0表明子容器有自己的后台线程无需父容器来调用它的processChildren方法。
if (children[i].getBackgroundProcessorDelay() &lt;= 0) {
processChildren(children[i]);
}
}
} catch (Throwable t) { ... }
```
上面的代码逻辑也是比较清晰的首先ContainerBackgroundProcessor是一个Runnable它需要实现run方法它的run很简单就是调用了processChildren方法。这里有个小技巧它把“宿主类”也就是**ContainerBase的类实例当成参数传给了run方法**。
而在processChildren方法里就做了两步调用当前容器的backgroundProcess方法以及递归调用子孙的backgroundProcess方法。请你注意backgroundProcess是Container接口中的方法也就是说所有类型的容器都可以实现这个方法在这个方法里完成需要周期性执行的任务。
这样的设计意味着什么呢我们只需要在顶层容器也就是Engine容器中启动一个后台线程那么这个线程**不但会执行Engine容器的周期性任务它还会执行所有子容器的周期性任务**。
**backgroundProcess方法**
上述代码都是在基类ContainerBase中实现的那具体容器类需要做什么呢其实很简单如果有周期性任务要执行就实现backgroundProcess方法如果没有就重用基类ContainerBase的方法。ContainerBase的backgroundProcess方法实现如下
```
public void backgroundProcess() {
//1.执行容器中Cluster组件的周期性任务
Cluster cluster = getClusterInternal();
if (cluster != null) {
cluster.backgroundProcess();
}
//2.执行容器中Realm组件的周期性任务
Realm realm = getRealmInternal();
if (realm != null) {
realm.backgroundProcess();
}
//3.执行容器中Valve组件的周期性任务
Valve current = pipeline.getFirst();
while (current != null) {
current.backgroundProcess();
current = current.getNext();
}
//4. 触发容器的&quot;周期事件&quot;Host容器的监听器HostConfig就靠它来调用
fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}
```
从上面的代码可以看到不仅每个容器可以有周期性任务每个容器中的其他通用组件比如跟集群管理有关的Cluster组件、跟安全管理有关的Realm组件都可以有自己的周期性任务。
我在前面的专栏里提到过容器之间的链式调用是通过Pipeline-Valve机制来实现的从上面的代码你可以看到容器中的Valve也可以有周期性任务并且被ContainerBase统一处理。
请你特别注意的是在backgroundProcess方法的最后还触发了容器的“周期事件”。我们知道容器的生命周期事件有初始化、启动和停止等那“周期事件”又是什么呢它跟生命周期事件一样是一种扩展机制你可以这样理解
又一段时间过去了,容器还活着,你想做点什么吗?如果你想做点什么,就创建一个监听器来监听这个“周期事件”,事件到了我负责调用你的方法。
总之有了ContainerBase中的后台线程和backgroundProcess方法各种子容器和通用组件不需要各自弄一个后台线程来处理周期性任务这样的设计显得优雅和整洁。
## Tomcat热加载
有了ContainerBase的周期性任务处理“框架”作为具体容器子类只需要实现自己的周期性任务就行。而Tomcat的热加载就是在Context容器中实现的。Context容器的backgroundProcess方法是这样实现的
```
public void backgroundProcess() {
//WebappLoader周期性的检查WEB-INF/classes和WEB-INF/lib目录下的类文件
Loader loader = getLoader();
if (loader != null) {
loader.backgroundProcess();
}
//Session管理器周期性的检查是否有过期的Session
Manager manager = getManager();
if (manager != null) {
manager.backgroundProcess();
}
//周期性的检查静态资源是否有变化
WebResourceRoot resources = getResources();
if (resources != null) {
resources.backgroundProcess();
}
//调用父类ContainerBase的backgroundProcess方法
super.backgroundProcess();
}
```
从上面的代码我们看到Context容器通过WebappLoader来检查类文件是否有更新通过Session管理器来检查是否有Session过期并且通过资源管理器来检查静态资源是否有更新最后还调用了父类ContainerBase的backgroundProcess方法。
这里我们要重点关注WebappLoader是如何实现热加载的它主要是调用了Context容器的reload方法而Context的reload方法比较复杂总结起来主要完成了下面这些任务
1. 停止和销毁Context容器及其所有子容器子容器其实就是Wrapper也就是说Wrapper里面Servlet实例也被销毁了。
1. 停止和销毁Context容器关联的Listener和Filter。
1. 停止和销毁Context下的Pipeline和各种Valve。
1. 停止和销毁Context的类加载器以及类加载器加载的类文件资源。
1. 启动Context容器在这个过程中会重新创建前面四步被销毁的资源。
在这个过程中类加载器发挥着关键作用。一个Context容器对应一个类加载器类加载器在销毁的过程中会把它加载的所有类也全部销毁。Context容器在启动过程中会创建一个新的类加载器来加载新的类文件。
在Context的reload方法里并没有调用Session管理器的destroy方法也就是说这个Context关联的Session是没有销毁的。你还需要注意的是Tomcat的热加载默认是关闭的你需要在conf目录下的context.xml文件中设置reloadable参数来开启这个功能像下面这样
```
&lt;Context reloadable=&quot;true&quot;/&gt;
```
## Tomcat热部署
我们再来看看热部署热部署跟热加载的本质区别是热部署会重新部署Web应用原来的Context对象会整个被销毁掉因此这个Context所关联的一切资源都会被销毁包括Session。
那么Tomcat热部署又是由哪个容器来实现的呢应该不是由Context因为热部署过程中Context容器被销毁了那么这个重担就落在Host身上了因为它是Context的父容器。
跟Context不一样Host容器并没有在backgroundProcess方法中实现周期性检测的任务而是通过监听器HostConfig来实现的HostConfig就是前面提到的“周期事件”的监听器那“周期事件”达到时HostConfig会做什么事呢
```
public void lifecycleEvent(LifecycleEvent event) {
// 执行check方法。
if (event.getType().equals(Lifecycle.PERIODIC_EVENT)) {
check();
}
}
```
它执行了check方法我们接着来看check方法里做了什么。
```
protected void check() {
if (host.getAutoDeploy()) {
// 检查这个Host下所有已经部署的Web应用
DeployedApplication[] apps =
deployed.values().toArray(new DeployedApplication[0]);
for (int i = 0; i &lt; apps.length; i++) {
//检查Web应用目录是否有变化
checkResources(apps[i], false);
}
//执行部署
deployApps();
}
}
```
其实HostConfig会检查webapps目录下的所有Web应用
- 如果原来Web应用目录被删掉了就把相应Context容器整个销毁掉。
- 是否有新的Web应用目录放进来了或者有新的WAR包放进来了就部署相应的Web应用。
因此HostConfig做的事情都是比较“宏观”的它不会去检查具体类文件或者资源文件是否有变化而是检查Web应用目录级别的变化。
## 本期精华
今天我们学习Tomcat的热加载和热部署它们的目的都是在不重启Tomcat的情况下实现Web应用的更新。
热加载的粒度比较小主要是针对类文件的更新通过创建新的类加载器来实现重新加载。而热部署是针对整个Web应用的Tomcat会将原来的Context对象整个销毁掉再重新创建Context容器对象。
热加载和热部署的实现都离不开后台线程的周期性检查Tomcat在基类ContainerBase中统一实现了后台线程的处理逻辑并在顶层容器Engine启动后台线程这样子容器组件甚至各种通用组件都不需要自己去创建后台线程这样的设计显得优雅整洁。
## 课后思考
为什么Host容器不通过重写backgroundProcess方法来实现热部署呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,216 @@
<audio id="audio" title="24 | Context容器Tomcat如何打破双亲委托机制" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/80/95/80efe60469187d94ab5d63c83cc21195.mp3"></audio>
相信我们平时在工作中都遇到过ClassNotFound异常这个异常表示JVM在尝试加载某个类的时候失败了。想要解决这个问题首先你需要知道什么是类加载JVM是如何加载类的以及为什么会出现ClassNotFound异常弄懂上面这些问题之后我们接着要思考Tomcat作为Web容器它是如何加载和管理Web应用下的Servlet呢
Tomcat正是通过Context组件来加载管理Web应用的所以今天我会详细分析Tomcat的类加载机制。但在这之前我们有必要预习一下JVM的类加载机制我会先回答一下一开始抛出来的问题接着再谈谈Tomcat的类加载器如何打破Java的双亲委托机制。
## JVM的类加载器
Java的类加载就是把字节码格式“.class”文件加载到JVM的**方法区**并在JVM的**堆区**建立一个`java.lang.Class`对象的实例用来封装Java类相关的数据和方法。那Class对象又是什么呢你可以把它理解成业务类的模板JVM根据这个模板来创建具体业务类对象实例。
JVM并不是在启动时就把所有的“.class”文件都加载一遍而是程序在运行过程中用到了这个类才去加载。JVM类加载是由类加载器来完成的JDK提供一个抽象类ClassLoader这个抽象类中定义了三个关键方法理解清楚它们的作用和关系非常重要。
```
public abstract class ClassLoader {
//每个类加载器都有个父加载器
private final ClassLoader parent;
public Class&lt;?&gt; loadClass(String name) {
//查找一下这个类是不是已经加载过了
Class&lt;?&gt; c = findLoadedClass(name);
//如果没有加载过
if( c == null ){
//先委托给父加载器去加载,注意这是个递归调用
if (parent != null) {
c = parent.loadClass(name);
}else {
// 如果父加载器为空查找Bootstrap加载器是不是加载过了
c = findBootstrapClassOrNull(name);
}
}
// 如果父加载器没加载成功调用自己的findClass去加载
if (c == null) {
c = findClass(name);
}
return c
}
protected Class&lt;?&gt; findClass(String name){
//1. 根据传入的类名name到在特定目录下去寻找类文件把.class文件读入内存
...
//2. 调用defineClass将字节数组转成Class对象
return defineClass(buf, off, len)
}
// 将字节码数组解析成一个Class对象用native方法实现
protected final Class&lt;?&gt; defineClass(byte[] b, int off, int len){
...
}
}
```
从上面的代码我们可以得到几个关键信息:
- JVM的类加载器是分层次的它们有父子关系每个类加载器都持有一个parent字段指向父加载器。
- defineClass是个工具方法它的职责是调用native方法把Java类的字节码解析成一个Class对象所谓的native方法就是由C语言实现的方法Java通过JNI机制调用。
- findClass方法的主要职责就是找到“.class”文件可能来自文件系统或者网络找到后把“.class”文件读到内存得到字节码数组然后调用defineClass方法得到Class对象。
- loadClass是个public方法说明它才是对外提供服务的接口具体实现也比较清晰首先检查这个类是不是已经被加载过了如果加载过了直接返回否则交给父加载器去加载。请你注意这是一个递归调用也就是说子加载器持有父加载器的引用当一个类加载器需要加载一个Java类时会先委托父加载器去加载然后父加载器在自己的加载路径中搜索Java类当父加载器在自己的加载范围内找不到时才会交还给子加载器加载这就是双亲委托机制。
JDK中有哪些默认的类加载器它们的本质区别是什么为什么需要双亲委托机制JDK中有3个类加载器另外你也可以自定义类加载器它们的关系如下图所示。
<img src="https://static001.geekbang.org/resource/image/43/90/43ebbd8e7d24467d2182969fb0496d90.png" alt="">
- BootstrapClassLoader是启动类加载器由C语言实现用来加载JVM启动时所需要的核心类比如`rt.jar``resources.jar`等。
- ExtClassLoader是扩展类加载器用来加载`\jre\lib\ext`目录下JAR包。
- AppClassLoader是系统类加载器用来加载classpath下的类应用程序默认用它来加载类。
- 自定义类加载器,用来加载自定义路径下的类。
这些类加载器的工作原理是一样的区别是它们的加载路径不同也就是说findClass这个方法查找的路径不同。双亲委托机制是为了保证一个Java类在JVM中是唯一的假如你不小心写了一个与JRE核心类同名的类比如Object类双亲委托机制能保证加载的是JRE里的那个Object类而不是你写的Object类。这是因为AppClassLoader在加载你的Object类时会委托给ExtClassLoader去加载而ExtClassLoader又会委托给BootstrapClassLoaderBootstrapClassLoader发现自己已经加载过了Object类会直接返回不会去加载你写的Object类。
这里请你注意类加载器的父子关系不是通过继承来实现的比如AppClassLoader并不是ExtClassLoader的子类而是说AppClassLoader的parent成员变量指向ExtClassLoader对象。同样的道理如果你要自定义类加载器不去继承AppClassLoader而是继承ClassLoader抽象类再重写findClass和loadClass方法即可Tomcat就是通过自定义类加载器来实现自己的类加载逻辑。不知道你发现没有如果你要打破双亲委托机制就需要重写loadClass方法因为loadClass的默认实现就是双亲委托机制。
## Tomcat的类加载器
Tomcat的自定义类加载器WebAppClassLoader打破了双亲委托机制它**首先自己尝试去加载某个类,如果找不到再代理给父类加载器**其目的是优先加载Web应用自己定义的类。具体实现就是重写ClassLoader的两个方法findClass和loadClass。
**findClass方法**
我们先来看看findClass方法的实现为了方便理解和阅读我去掉了一些细节
```
public Class&lt;?&gt; findClass(String name) throws ClassNotFoundException {
...
Class&lt;?&gt; clazz = null;
try {
//1. 先在Web应用目录下查找类
clazz = findClassInternal(name);
} catch (RuntimeException e) {
throw e;
}
if (clazz == null) {
try {
//2. 如果在本地目录没有找到,交给父加载器去查找
clazz = super.findClass(name);
} catch (RuntimeException e) {
throw e;
}
//3. 如果父类也没找到抛出ClassNotFoundException
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
```
在findClass方法里主要有三个步骤
1. 先在Web应用本地目录下查找要加载的类。
1. 如果没有找到交给父加载器去查找它的父加载器就是上面提到的系统类加载器AppClassLoader。
1. 如何父加载器也没找到这个类抛出ClassNotFound异常。
**loadClass方法**
接着我们再来看Tomcat类加载器的loadClass方法的实现同样我也去掉了一些细节
```
public Class&lt;?&gt; loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class&lt;?&gt; clazz = null;
//1. 先在本地cache查找该类是否已经加载过
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//2. 从系统类加载器的cache中查找是否加载过
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// 3. 尝试用ExtClassLoader类加载器类加载为什么
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4. 尝试在本地目录搜索class并加载
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//6. 上述过程都加载失败,抛出异常
throw new ClassNotFoundException(name);
}
```
loadClass方法稍微复杂一点主要有六个步骤
1. 先在本地Cache查找该类是否已经加载过也就是说Tomcat的类加载器是否已经加载过这个类。
1. 如果Tomcat类加载器没有加载过这个类再看看系统类加载器是否加载过。
1. 如果都没有,就让**ExtClassLoader**去加载,这一步比较关键,目的**防止Web应用自己的类覆盖JRE的核心类**。因为Tomcat需要打破双亲委托机制假如Web应用里自定义了一个叫Object的类如果先加载这个Object类就会覆盖JRE里面的那个Object类这就是为什么Tomcat的类加载器会优先尝试用ExtClassLoader去加载因为ExtClassLoader会委托给BootstrapClassLoader去加载BootstrapClassLoader发现自己已经加载了Object类直接返回给Tomcat的类加载器这样Tomcat的类加载器就不会去加载Web应用下的Object类了也就避免了覆盖JRE核心类的问题。
1. 如果ExtClassLoader加载器加载失败也就是说JRE核心类中没有这类那么就在本地Web应用目录下查找并加载。
1. 如果本地目录下没有这个类说明不是Web应用自己定义的类那么由系统类加载器去加载。这里请你注意Web应用是通过`Class.forName`调用交给系统类加载器的,因为`Class.forName`的默认加载器就是系统类加载器。
1. 如果上述加载过程全部失败抛出ClassNotFound异常。
从上面的过程我们可以看到Tomcat的类加载器打破了双亲委托机制没有一上来就直接委托给父加载器而是先在本地目录下加载为了避免本地目录下的类覆盖JRE的核心类先尝试用JVM扩展类加载器ExtClassLoader去加载。那为什么不先用系统类加载器AppClassLoader去加载很显然如果是这样的话那就变成双亲委托机制了这就是Tomcat类加载器的巧妙之处。
## 本期精华
今天我介绍了JVM的类加载器原理和源码剖析以及Tomcat的类加载器是如何打破双亲委托机制的目的是为了优先加载Web应用目录下的类然后再加载其他目录下的类这也是Servlet规范的推荐做法。
要打破双亲委托机制需要继承ClassLoader抽象类并且需要重写它的loadClass方法因为ClassLoader的默认实现就是双亲委托。
## 课后思考
如果你并不想打破双亲委托机制但是又想定义自己的类加载器来加载特定目录下的类你需要重写findClass和loadClass方法中的哪一个还是两个都要重写
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,87 @@
<audio id="audio" title="25 | Context容器Tomcat如何隔离Web应用" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c3/ce/c3dbdc4cb28ef833a956e29578e436ce.mp3"></audio>
我在专栏上一期提到Tomcat通过自定义类加载器WebAppClassLoader打破了双亲委托机制具体来说就是重写了JVM的类加载器ClassLoader的findClass方法和loadClass方法这样做的目的是优先加载Web应用目录下的类。除此之外你觉得Tomcat的类加载器还需要完成哪些需求呢或者说在设计上还需要考虑哪些方面
我们知道Tomcat作为Servlet容器它负责加载我们的Servlet类此外它还负责加载Servlet所依赖的JAR包。并且Tomcat本身也是一个Java程序因此它需要加载自己的类和依赖的JAR包。首先让我们思考这一下这几个问题
1. 假如我们在Tomcat中运行了两个Web应用程序两个Web应用中有同名的Servlet但是功能不同Tomcat需要同时加载和管理这两个同名的Servlet类保证它们不会冲突因此Web应用之间的类需要隔离。
1. 假如两个Web应用都依赖同一个第三方的JAR包比如Spring那Spring的JAR包被加载到内存后Tomcat要保证这两个Web应用能够共享也就是说Spring的JAR包只被加载一次否则随着依赖的第三方JAR包增多JVM的内存会膨胀。
1. 跟JVM一样我们需要隔离Tomcat本身的类和Web应用的类。
在了解了Tomcat的类加载器在设计时要考虑的这些问题以后今天我们主要来学习一下Tomcat是如何通过设计多层次的类加载器来解决这些问题的。
## Tomcat类加载器的层次结构
为了解决这些问题Tomcat设计了类加载器的层次结构它们的关系如下图所示。下面我来详细解释为什么要设计这些类加载器告诉你它们是怎么解决上面这些问题的。
<img src="https://static001.geekbang.org/resource/image/62/23/6260716096c77cb89a375e4ac3572923.png" alt="">
我们先来看**第1个问题**假如我们使用JVM默认AppClassLoader来加载Web应用AppClassLoader只能加载一个Servlet类在加载第二个同名Servlet类时AppClassLoader会返回第一个Servlet类的Class实例这是因为在AppClassLoader看来同名的Servlet类只被加载一次。
因此Tomcat的解决方案是自定义一个类加载器WebAppClassLoader 并且给每个Web应用创建一个类加载器实例。我们知道Context容器组件对应一个Web应用因此每个Context容器负责创建和维护一个WebAppClassLoader加载器实例。这背后的原理是**不同的加载器实例加载的类被认为是不同的类**即使它们的类名相同。这就相当于在Java虚拟机内部创建了一个个相互隔离的Java类空间每一个Web应用都有自己的类空间Web应用之间通过各自的类加载器互相隔离。
**SharedClassLoader**
我们再来看**第2个问题**本质需求是两个Web应用之间怎么共享库类并且不能重复加载相同的类。我们知道在双亲委托机制里各个子加载器都可以通过父加载器去加载类那么把需要共享的类放到父加载器的加载路径下不就行了吗应用程序也正是通过这种方式共享JRE的核心类。因此Tomcat的设计者又加了一个类加载器SharedClassLoader作为WebAppClassLoader的父加载器专门来加载Web应用之间共享的类。如果WebAppClassLoader自己没有加载到某个类就会委托父加载器SharedClassLoader去加载这个类SharedClassLoader会在指定目录下加载共享类之后返回给WebAppClassLoader这样共享的问题就解决了。
**CatalinaClassLoader**
我们来看**第3个问题**如何隔离Tomcat本身的类和Web应用的类我们知道要共享可以通过父子关系要隔离那就需要兄弟关系了。兄弟关系就是指两个类加载器是平行的它们可能拥有同一个父加载器但是两个兄弟类加载器加载的类是隔离的。基于此Tomcat又设计一个类加载器CatalinaClassLoader专门来加载Tomcat自身的类。这样设计有个问题那Tomcat和各Web应用之间需要共享一些类时该怎么办呢
**CommonClassLoader**
老办法还是再增加一个CommonClassLoader作为CatalinaClassLoader和SharedClassLoader的父加载器。CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader 使用而CatalinaClassLoader和SharedClassLoader能加载的类则与对方相互隔离。WebAppClassLoader可以使用SharedClassLoader加载到的类但各个WebAppClassLoader实例之间相互隔离。
## Spring的加载问题
在JVM的实现中有一条隐含的规则默认情况下如果一个类由类加载器A加载那么这个类的依赖类也是由相同的类加载器加载。比如Spring作为一个Bean工厂它需要创建业务类的实例并且在创建业务类实例之前需要加载这些类。Spring是通过调用`Class.forName`来加载业务类的我们来看一下forName的源码
```
public static Class&lt;?&gt; forName(String className) {
Class&lt;?&gt; caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
```
可以看到在forName的函数里会用调用者也就是Spring的加载器去加载业务类。
我在前面提到Web应用之间共享的JAR包可以交给SharedClassLoader来加载从而避免重复加载。Spring作为共享的第三方JAR包它本身是由SharedClassLoader来加载的Spring又要去加载业务类按照前面那条规则加载Spring的类加载器也会用来加载业务类但是业务类在Web应用目录下不在SharedClassLoader的加载路径下这该怎么办呢
于是线程上下文加载器登场了它其实是一种类加载器传递机制。为什么叫作“线程上下文加载器”呢因为这个类加载器保存在线程私有数据里只要是同一个线程一旦设置了线程上下文加载器在线程后续执行过程中就能把这个类加载器取出来用。因此Tomcat为每个Web应用创建一个WebAppClassLoader类加载器并在启动Web应用的线程里设置线程上下文加载器这样Spring在启动时就将线程上下文加载器取出来用来加载Bean。Spring取线程上下文加载的代码如下
```
cl = Thread.currentThread().getContextClassLoader();
```
## 本期精华
今天我介绍了JVM的类加载器原理并剖析了源码以及Tomcat的类加载器的设计。重点需要你理解的是Tomcat的Context组件为每个Web应用创建一个WebAppClassLoader类加载器由于**不同类加载器实例加载的类是互相隔离的**因此达到了隔离Web应用的目的同时通过CommonClassLoader等父加载器来共享第三方JAR包。而共享的第三方JAR包怎么加载特定Web应用的类呢可以通过设置线程上下文加载器来解决。而作为Java程序员我们应该牢记的是
- 每个Web应用自己的Java类文件和依赖的JAR包分别放在`WEB-INF/classes``WEB-INF/lib`目录下面。
- 多个应用共享的Java类文件和JAR包分别放在Web容器指定的共享目录下。
- 当出现ClassNotFound错误时应该检查你的类加载器是否正确。
线程上下文加载器不仅仅可以用在Tomcat和Spring类加载的场景里核心框架类需要加载具体实现类时都可以用到它比如我们熟悉的JDBC就是通过上下文类加载器来加载不同的数据库驱动的感兴趣的话可以深入了解一下。
## 课后思考
在StandardContext的启动方法里会将当前线程的上下文加载器设置为WebAppClassLoader。
```
originalClassLoader = Thread.currentThread().getContextClassLoader();
Thread.currentThread().setContextClassLoader(webApplicationClassLoader);
```
在启动方法结束的时候,还会恢复线程的上下文加载器:
```
Thread.currentThread().setContextClassLoader(originalClassLoader);
```
这是为什么呢?
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,210 @@
<audio id="audio" title="26 | Context容器Tomcat如何实现Servlet规范" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/9b/34/9bab34cddb058c3054405af038809e34.mp3"></audio>
我们知道Servlet容器最重要的任务就是创建Servlet的实例并且调用Servlet在前面两期我谈到了Tomcat如何定义自己的类加载器来加载Servlet但加载Servlet的类不等于创建Servlet的实例类加载只是第一步类加载好了才能创建类的实例也就是说Tomcat先加载Servlet的类然后在Java堆上创建了一个Servlet实例。
一个Web应用里往往有多个Servlet而在Tomcat中一个Web应用对应一个Context容器也就是说一个Context容器需要管理多个Servlet实例。但Context容器并不直接持有Servlet实例而是通过子容器Wrapper来管理Servlet你可以把Wrapper容器看作是Servlet的包装。
那为什么需要Wrapper呢Context容器直接维护一个Servlet数组不就行了吗这是因为Servlet不仅仅是一个类实例它还有相关的配置信息比如它的URL映射、它的初始化参数因此设计出了一个包装器把Servlet本身和它相关的数据包起来没错这就是面向对象的思想。
那管理好Servlet就完事大吉了吗别忘了Servlet还有两个兄弟Listener和Filter它们也是Servlet规范中的重要成员因此Tomcat也需要创建它们的实例也需要在合适的时机去调用它们的方法。
说了那么多下面我们就来聊一聊Tomcat是如何做到上面这些事的。
## Servlet管理
前面提到Tomcat是用Wrapper容器来管理Servlet的那Wrapper容器具体长什么样子呢我们先来看看它里面有哪些关键的成员变量
```
protected volatile Servlet instance = null;
```
毫无悬念它拥有一个Servlet实例并且Wrapper通过loadServlet方法来实例化Servlet。为了方便你阅读我简化了代码
```
public synchronized Servlet loadServlet() throws ServletException {
Servlet servlet;
//1. 创建一个Servlet实例
servlet = (Servlet) instanceManager.newInstance(servletClass);
//2.调用了Servlet的init方法这是Servlet规范要求的
initServlet(servlet);
return servlet;
}
```
其实loadServlet主要做了两件事创建Servlet的实例并且调用Servlet的init方法因为这是Servlet规范要求的。
那接下来的问题是什么时候会调到这个loadServlet方法呢为了加快系统的启动速度我们往往会采取资源延迟加载的策略Tomcat也不例外默认情况下Tomcat在启动时不会加载你的Servlet除非你把Servlet的`loadOnStartup`参数设置为`true`
这里还需要你注意的是虽然Tomcat在启动时不会创建Servlet实例但是会创建Wrapper容器就好比尽管枪里面还没有子弹先把枪造出来。那子弹什么时候造呢是真正需要开枪的时候也就是说有请求来访问某个Servlet时这个Servlet的实例才会被创建。
那Servlet是被谁调用的呢我们回忆一下专栏前面提到过Tomcat的Pipeline-Valve机制每个容器组件都有自己的Pipeline每个Pipeline中有一个Valve链并且每个容器组件有一个BasicValve基础阀。Wrapper作为一个容器组件它也有自己的Pipeline和BasicValveWrapper的BasicValve叫**StandardWrapperValve**。
你可以想到当请求到来时Context容器的BasicValve会调用Wrapper容器中Pipeline中的第一个Valve然后会调用到StandardWrapperValve。我们先来看看它的invoke方法是如何实现的同样为了方便你阅读我简化了代码
```
public final void invoke(Request request, Response response) {
//1.实例化Servlet
servlet = wrapper.allocate();
//2.给当前请求创建一个Filter链
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
//3. 调用这个Filter链Filter链中的最后一个Filter会调用Servlet
filterChain.doFilter(request.getRequest(), response.getResponse());
}
```
StandardWrapperValve的invoke方法比较复杂去掉其他异常处理的一些细节本质上就是三步
- 第一步创建Servlet实例
- 第二步给当前请求创建一个Filter链
- 第三步调用这个Filter链。
你可能会问为什么需要给每个请求创建一个Filter链这是因为每个请求的请求路径都不一样而Filter都有相应的路径映射因此不是所有的Filter都需要来处理当前的请求我们需要根据请求的路径来选择特定的一些Filter来处理。
第二个问题是为什么没有看到调到Servlet的service方法这是因为Filter链的doFilter方法会负责调用Servlet具体来说就是Filter链中的最后一个Filter会负责调用Servlet。
接下来我们来看Filter的实现原理。
## Filter管理
我们知道跟Servlet一样Filter也可以在`web.xml`文件里进行配置不同的是Filter的作用域是整个Web应用因此Filter的实例是在Context容器中进行管理的Context容器用Map集合来保存Filter。
```
private Map&lt;String, FilterDef&gt; filterDefs = new HashMap&lt;&gt;();
```
那上面提到的Filter链又是什么呢Filter链的存活期很短它是跟每个请求对应的。一个新的请求来了就动态创建一个Filter链请求处理完了Filter链也就被回收了。理解它的原理也非常关键我们还是来看看源码
```
public final class ApplicationFilterChain implements FilterChain {
//Filter链中有Filter数组这个好理解
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
//Filter链中的当前的调用位置
private int pos = 0;
//总共有多少了Filter
private int n = 0;
//每个Filter链对应一个Servlet也就是它要调用的Servlet
private Servlet servlet = null;
public void doFilter(ServletRequest req, ServletResponse res) {
internalDoFilter(request,response);
}
private void internalDoFilter(ServletRequest req,
ServletResponse res){
// 每个Filter链在内部维护了一个Filter数组
if (pos &lt; n) {
ApplicationFilterConfig filterConfig = filters[pos++];
Filter filter = filterConfig.getFilter();
filter.doFilter(request, response, this);
return;
}
servlet.service(request, response);
}
```
从ApplicationFilterChain的源码我们可以看到几个关键信息
1. Filter链中除了有Filter对象的数组还有一个整数变量pos这个变量用来记录当前被调用的Filter在数组中的位置。
1. Filter链中有个Servlet实例这个好理解因为上面提到了每个Filter链最后都会调到一个Servlet。
1. Filter链本身也实现了doFilter方法直接调用了一个内部方法internalDoFilter。
1. internalDoFilter方法的实现比较有意思它做了一个判断如果当前Filter的位置小于Filter数组的长度也就是说Filter还没调完就从Filter数组拿下一个Filter调用它的doFilter方法。否则意味着所有Filter都调到了就调用Servlet的service方法。
但问题是方法体里没看到循环谁在不停地调用Filter链的doFilter方法呢Filter是怎么依次调到的呢
答案是**Filter本身的doFilter方法会调用Filter链的doFilter方法**,我们还是来看看代码就明白了:
```
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain){
...
//调用Filter的方法
chain.doFilter(request, response);
}
```
注意Filter的doFilter方法有个关键参数FilterChain就是Filter链。并且每个Filter在实现doFilter时必须要调用Filter链的doFilter方法而Filter链中保存当前Filter的位置会调用下一个Filter的doFilter方法这样链式调用就完成了。
Filter链跟Tomcat的Pipeline-Valve本质都是责任链模式但是在具体实现上稍有不同你可以细细体会一下。
## Listener管理
我们接着聊Servlet规范里Listener。跟Filter一样Listener也是一种扩展机制你可以监听容器内部发生的事件主要有两类事件
- 第一类是生命状态的变化比如Context容器启动和停止、Session的创建和销毁。
- 第二类是属性的变化比如Context容器某个属性值变了、Session的某个属性值变了以及新的请求来了等。
我们可以在`web.xml`配置或者通过注解的方式来添加监听器在监听器里实现我们的业务逻辑。对于Tomcat来说它需要读取配置文件拿到监听器类的名字实例化这些类并且在合适的时机调用这些监听器的方法。
Tomcat是通过Context容器来管理这些监听器的。Context容器将两类事件分开来管理分别用不同的集合来存放不同类型事件的监听器
```
//监听属性值变化的监听器
private List&lt;Object&gt; applicationEventListenersList = new CopyOnWriteArrayList&lt;&gt;();
//监听生命事件的监听器
private Object applicationLifecycleListenersObjects[] = new Object[0];
```
剩下的事情就是触发监听器了比如在Context容器的启动方法里就触发了所有的ServletContextListener
```
//1.拿到所有的生命周期监听器
Object instances[] = getApplicationLifecycleListeners();
for (int i = 0; i &lt; instances.length; i++) {
//2. 判断Listener的类型是不是ServletContextListener
if (!(instances[i] instanceof ServletContextListener))
continue;
//3.触发Listener的方法
ServletContextListener lr = (ServletContextListener) instances[i];
lr.contextInitialized(event);
}
```
需要注意的是这里的ServletContextListener接口是一种留给用户的扩展机制用户可以实现这个接口来定义自己的监听器监听Context容器的启停事件。Spring就是这么做的。ServletContextListener跟Tomcat自己的生命周期事件LifecycleListener是不同的。LifecycleListener定义在生命周期管理组件中由基类LifecycleBase统一管理。
## 本期精华
Servlet规范中最重要的就是Servlet、Filter和Listener“三兄弟”。Web容器最重要的职能就是把它们创建出来并在适当的时候调用它们的方法。
Tomcat通过Wrapper容器来管理ServletWrapper包装了Servlet本身以及相应的参数这体现了面向对象中“封装”的设计原则。
Tomcat会给**每个请求生成一个Filter链**Filter链中的最后一个Filter会负责调用Servlet的service方法。
对于Listener来说我们可以定制自己的监听器来监听Tomcat内部发生的各种事件包括Web应用级别的、Session级别的和请求级别的。Tomcat中的Context容器统一维护了这些监听器并负责触发。
最后小结一下这3期内容Context组件通过自定义类加载器来加载Web应用并实现了Servlet规范直接跟Web应用打交道是一个核心的容器组件。也因此我用了很重的篇幅去讲解它也非常建议你花点时间阅读一下它的源码。
## 课后思考
Context容器分别用了CopyOnWriteArrayList和对象数组来存储两种不同的监听器为什么要这样设计你可以思考一下背后的原因。
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,210 @@
<audio id="audio" title="27 | 新特性Tomcat如何支持异步Servlet" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/2f/fe/2fe0ba6d4f94b033c41f112a237045fe.mp3"></audio>
通过专栏前面的学习我们知道当一个新的请求到达时Tomcat和Jetty会从线程池里拿出一个线程来处理请求这个线程会调用你的Web应用Web应用在处理请求的过程中Tomcat线程会一直阻塞直到Web应用处理完毕才能再输出响应最后Tomcat才回收这个线程。
我们来思考这样一个问题假如你的Web应用需要较长的时间来处理请求比如数据库查询或者等待下游的服务调用返回那么Tomcat线程一直不回收会占用系统资源在极端情况下会导致“线程饥饿”也就是说Tomcat和Jetty没有更多的线程来处理新的请求。
那该如何解决这个问题呢方案是Servlet 3.0中引入的异步Servlet。主要是在Web应用里启动一个单独的线程来执行这些比较耗时的请求而Tomcat线程立即返回不再等待Web应用将请求处理完这样Tomcat线程可以立即被回收到线程池用来响应其他请求降低了系统的资源消耗同时还能提高系统的吞吐量。
今天我们就来学习一下如何开发一个异步Servlet以及异步Servlet的工作原理也就是Tomcat是如何支持异步Servlet的让你彻底理解它的来龙去脉。
## 异步Servlet示例
我们先通过一个简单的示例来了解一下异步Servlet的实现。
```
@WebServlet(urlPatterns = {&quot;/async&quot;}, asyncSupported = true)
public class AsyncServlet extends HttpServlet {
//Web应用线程池用来处理异步Servlet
ExecutorService executor = Executors.newSingleThreadExecutor();
public void service(HttpServletRequest req, HttpServletResponse resp) {
//1. 调用startAsync或者异步上下文
final AsyncContext ctx = req.startAsync();
//用线程池来执行耗时操作
executor.execute(new Runnable() {
@Override
public void run() {
//在这里做耗时的操作
try {
ctx.getResponse().getWriter().println(&quot;Handling Async Servlet&quot;);
} catch (IOException e) {}
//3. 异步Servlet处理完了调用异步上下文的complete方法
ctx.complete();
}
});
}
}
```
上面的代码有三个要点:
1. 通过注解的方式来注册Servlet除了@WebServlet注解,还需要加上`asyncSupported=true`的属性表明当前的Servlet是一个异步Servlet。
1. Web应用程序需要调用Request对象的startAsync方法来拿到一个异步上下文AsyncContext。这个上下文保存了请求和响应对象。
1. Web应用需要开启一个新线程来处理耗时的操作处理完成后需要调用AsyncContext的complete方法。目的是告诉Tomcat请求已经处理完成。
这里请你注意虽然异步Servlet允许用更长的时间来处理请求但是也有超时限制的默认是30秒如果30秒内请求还没处理完Tomcat会触发超时机制向浏览器返回超时错误如果这个时候你的Web应用再调用`ctx.complete`方法会得到一个IllegalStateException异常。
## 异步Servlet原理
通过上面的例子相信你对Servlet的异步实现有了基本的理解。要理解Tomcat在这个过程都做了什么事情关键就是要弄清楚`req.startAsync`方法和`ctx.complete`方法都做了什么。
**startAsync方法**
startAsync方法其实就是创建了一个异步上下文AsyncContext对象AsyncContext对象的作用是保存请求的中间信息比如Request和Response对象等上下文信息。你来思考一下为什么需要保存这些信息呢
这是因为Tomcat的工作线程在`request.startAsync`调用之后就直接结束回到线程池中了线程本身不会保存任何信息。也就是说一个请求到服务端执行到一半你的Web应用正在处理这个时候Tomcat的工作线程没了这就需要有个缓存能够保存原始的Request和Response对象而这个缓存就是AsyncContext。
有了AsyncContext你的Web应用通过它拿到Request和Response对象拿到Request对象后就可以读取请求信息请求处理完了还需要通过Response对象将HTTP响应发送给浏览器。
除了创建AsyncContext对象startAsync还需要完成一个关键任务那就是告诉Tomcat当前的Servlet处理方法返回时不要把响应发到浏览器因为这个时候响应还没生成呢并且不能把Request对象和Response对象销毁因为后面Web应用还要用呢。
在Tomcat中负责flush响应数据的是CoyoteAdapter它还会销毁Request对象和Response对象因此需要通过某种机制通知CoyoteAdapter具体来说是通过下面这行代码
```
this.request.getCoyoteRequest().action(ActionCode.ASYNC_START, this);
```
你可以把它理解为一个Callback在这个action方法里设置了Request对象的状态设置它为一个异步Servlet请求。
我们知道连接器是调用CoyoteAdapter的service方法来处理请求的而CoyoteAdapter会调用容器的service方法当容器的service方法返回时CoyoteAdapter判断当前的请求是不是异步Servlet请求如果是就不会销毁Request和Response对象也不会把响应信息发到浏览器。你可以通过下面的代码理解一下这是CoyoteAdapter的service方法我对它进行了简化
```
public void service(org.apache.coyote.Request req, org.apache.coyote.Response res) {
//调用容器的service方法处理请求
connector.getService().getContainer().getPipeline().
getFirst().invoke(request, response);
//如果是异步Servlet请求仅仅设置一个标志
//否则说明是同步Servlet请求就将响应数据刷到浏览器
if (request.isAsync()) {
async = true;
} else {
request.finishRequest();
response.finishResponse();
}
//如果不是异步Servlet请求就销毁Request对象和Response对象
if (!async) {
request.recycle();
response.recycle();
}
}
```
接下来当CoyoteAdapter的service方法返回到ProtocolHandler组件时ProtocolHandler判断返回值如果当前请求是一个异步Servlet请求它会把当前Socket的协议处理者Processor缓存起来将SocketWrapper对象和相应的Processor存到一个Map数据结构里。
```
private final Map&lt;S,Processor&gt; connections = new ConcurrentHashMap&lt;&gt;();
```
之所以要缓存是因为这个请求接下来还要接着处理还是由原来的Processor来处理通过SocketWrapper就能从Map里找到相应的Processor。
**complete方法**
接着我们再来看关键的`ctx.complete`方法当请求处理完成时Web应用调用这个方法。那么这个方法做了些什么事情呢最重要的就是把响应数据发送到浏览器。
这件事情不能由Web应用线程来做也就是说`ctx.complete`方法不能直接把响应数据发送到浏览器因为这件事情应该由Tomcat线程来做但具体怎么做呢
我们知道连接器中的Endpoint组件检测到有请求数据达到时会创建一个SocketProcessor对象交给线程池去处理因此Endpoint的通信处理和具体请求处理在两个线程里运行。
在异步Servlet的场景里Web应用通过调用`ctx.complete`方法时也可以生成一个新的SocketProcessor任务类交给线程池处理。对于异步Servlet请求来说相应的Socket和协议处理组件Processor都被缓存起来了并且这些对象都可以通过Request对象拿到。
讲到这里,你可能已经猜到`ctx.complete`是如何实现的了:
```
public void complete() {
//检查状态合法性,我们先忽略这句
check();
//调用Request对象的action方法其实就是通知连接器这个异步请求处理完了
request.getCoyoteRequest().action(ActionCode.ASYNC_COMPLETE, null);
}
```
我们可以看到complete方法调用了Request对象的action方法。而在action方法里则是调用了Processor的processSocketEvent方法并且传入了操作码OPEN_READ。
```
case ASYNC_COMPLETE: {
clearDispatches();
if (asyncStateMachine.asyncComplete()) {
processSocketEvent(SocketEvent.OPEN_READ, true);
}
break;
}
```
我们接着看processSocketEvent方法它调用SocketWrapper的processSocket方法
```
protected void processSocketEvent(SocketEvent event, boolean dispatch) {
SocketWrapperBase&lt;?&gt; socketWrapper = getSocketWrapper();
if (socketWrapper != null) {
socketWrapper.processSocket(event, dispatch);
}
}
```
而SocketWrapper的processSocket方法会创建SocketProcessor任务类并通过Tomcat线程池来处理
```
public boolean processSocket(SocketWrapperBase&lt;S&gt; socketWrapper,
SocketEvent event, boolean dispatch) {
if (socketWrapper == null) {
return false;
}
SocketProcessorBase&lt;S&gt; sc = processorCache.pop();
if (sc == null) {
sc = createSocketProcessor(socketWrapper, event);
} else {
sc.reset(socketWrapper, event);
}
//线程池运行
Executor executor = getExecutor();
if (dispatch &amp;&amp; executor != null) {
executor.execute(sc);
} else {
sc.run();
}
}
```
请你注意createSocketProcessor函数的第二个参数是SocketEvent这里我们传入的是OPEN_READ。通过这个参数我们就能控制SocketProcessor的行为因为我们不需要再把请求发送到容器进行处理只需要向浏览器端发送数据并且重新在这个Socket上监听新的请求就行了。
最后我通过一张在帮你理解一下整个过程:
<img src="https://static001.geekbang.org/resource/image/d2/ae/d2d96b7450dff9735989005958fa13ae.png" alt="">
## 本期精华
非阻塞I/O模型可以利用很少的线程处理大量的连接提高了并发度本质就是通过一个Selector线程查询多个Socket的I/O事件减少了线程的阻塞等待。
同样异步Servlet机制也是减少了线程的阻塞等待将Tomcat线程和业务线程分开Tomca线程不再等待业务代码的执行。
那什么样的场景适合异步Servlet呢适合的场景有很多最主要的还是根据你的实际情况如果你拿不准是否适合异步Servlet就看一条如果你发现Tomcat的线程不够了大量线程阻塞在等待Web应用的处理上而Web应用又没有优化的空间了确实需要长时间处理这个时候你不妨尝试一下异步Servlet。
## 课后思考
异步Servlet将Tomcat线程和Web应用线程分开体现了隔离的思想也就是把不同的业务处理所使用的资源隔离开使得它们互不干扰尤其是低优先级的业务不能影响高优先级的业务。你可以思考一下在你的Web应用内部是不是也可以运用这种设计思想呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,260 @@
<audio id="audio" title="28 | 新特性Spring Boot如何使用内嵌式的Tomcat和Jetty" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/96/ad/96e044ff5b42bc3a553b64e5d41873ad.mp3"></audio>
为了方便开发和部署Spring Boot在内部启动了一个嵌入式的Web容器。我们知道Tomcat和Jetty是组件化的设计要启动Tomcat或者Jetty其实就是启动这些组件。在Tomcat独立部署的模式下我们通过startup脚本来启动TomcatTomcat中的Bootstrap和Catalina会负责初始化类加载器并解析`server.xml`和启动这些组件。
在内嵌式的模式下Bootstrap和Catalina的工作就由Spring Boot来做了Spring Boot调用了Tomcat和Jetty的API来启动这些组件。那Spring Boot具体是怎么做的呢而作为程序员我们如何向Spring Boot中的Tomcat注册Servlet或者Filter呢我们又如何定制内嵌式的Tomcat今天我们就来聊聊这些话题。
## Spring Boot中Web容器相关的接口
既然要支持多种Web容器Spring Boot对内嵌式Web容器进行了抽象定义了**WebServer**接口:
```
public interface WebServer {
void start() throws WebServerException;
void stop() throws WebServerException;
int getPort();
}
```
各种Web容器比如Tomcat和Jetty需要去实现这个接口。
Spring Boot还定义了一个工厂**ServletWebServerFactory**来创建Web容器返回的对象就是上面提到的WebServer。
```
public interface ServletWebServerFactory {
WebServer getWebServer(ServletContextInitializer... initializers);
}
```
可以看到getWebServer有个参数类型是**ServletContextInitializer**。它表示ServletContext的初始化器用于ServletContext中的一些配置
```
public interface ServletContextInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
```
这里请注意上面提到的getWebServer方法会调用ServletContextInitializer的onStartup方法也就是说如果你想在Servlet容器启动时做一些事情比如注册你自己的Servlet可以实现一个ServletContextInitializer在Web容器启动时Spring Boot会把所有实现了ServletContextInitializer接口的类收集起来统一调它们的onStartup方法。
为了支持对内嵌式Web容器的定制化Spring Boot还定义了**WebServerFactoryCustomizerBeanPostProcessor**接口它是一个BeanPostProcessor它在postProcessBeforeInitialization过程中去寻找Spring容器中WebServerFactoryCustomizer类型的Bean并依次调用WebServerFactoryCustomizer接口的customize方法做一些定制化。
```
public interface WebServerFactoryCustomizer&lt;T extends WebServerFactory&gt; {
void customize(T factory);
}
```
## 内嵌式Web容器的创建和启动
铺垫了这些接口我们再来看看Spring Boot是如何实例化和启动一个Web容器的。我们知道Spring的核心是一个ApplicationContext它的抽象实现类AbstractApplicationContext实现了著名的**refresh**方法它用来新建或者刷新一个ApplicationContext在refresh方法中会调用onRefresh方法AbstractApplicationContext的子类可以重写这个onRefresh方法来实现特定Context的刷新逻辑因此ServletWebServerApplicationContext就是通过重写onRefresh方法来创建内嵌式的Web容器具体创建过程是这样的
```
@Override
protected void onRefresh() {
super.onRefresh();
try {
//重写onRefresh方法调用createWebServer创建和启动Tomcat
createWebServer();
}
catch (Throwable ex) {
}
}
//createWebServer的具体实现
private void createWebServer() {
//这里WebServer是Spring Boot抽象出来的接口具体实现类就是不同的Web容器
WebServer webServer = this.webServer;
ServletContext servletContext = this.getServletContext();
//如果Web容器还没创建
if (webServer == null &amp;&amp; servletContext == null) {
//通过Web容器工厂来创建
ServletWebServerFactory factory = this.getWebServerFactory();
//注意传入了一个&quot;SelfInitializer&quot;
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
} else if (servletContext != null) {
try {
this.getSelfInitializer().onStartup(servletContext);
} catch (ServletException var4) {
...
}
}
this.initPropertySources();
}
```
再来看看getWebServer具体做了什么以Tomcat为例主要调用Tomcat的API去创建各种组件
```
public WebServer getWebServer(ServletContextInitializer... initializers) {
//1.实例化一个Tomcat可以理解为Server组件。
Tomcat tomcat = new Tomcat();
//2. 创建一个临时目录
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir(&quot;tomcat&quot;);
tomcat.setBaseDir(baseDir.getAbsolutePath());
//3.初始化各种组件
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
//4. 创建定制版的&quot;Context&quot;组件。
this.prepareContext(tomcat.getHost(), initializers);
return this.getTomcatWebServer(tomcat);
}
```
你可能好奇prepareContext方法是做什么的呢这里的Context是指**Tomcat中的Context组件**为了方便控制Context组件的行为Spring Boot定义了自己的TomcatEmbeddedContext它扩展了Tomcat的StandardContext
```
class TomcatEmbeddedContext extends StandardContext {}
```
## 注册Servlet的三种方式
**1. Servlet注解**
在Spring Boot启动类上加上@ServletComponentScan注解后,使用@WebServlet@WebFilter@WebListener标记的Servlet、Filter、Listener就可以自动注册到Servlet容器中无需其他代码我们通过下面的代码示例来理解一下。
```
@SpringBootApplication
@ServletComponentScan
public class xxxApplication
{}
```
```
@WebServlet(&quot;/hello&quot;)
public class HelloServlet extends HttpServlet {}
```
在Web应用的入口类上加上@ServletComponentScan并且在Servlet类上加上@WebServlet这样Spring Boot会负责将Servlet注册到内嵌的Tomcat中。
**2. ServletRegistrationBean**
同时Spring Boot也提供了ServletRegistrationBean、FilterRegistrationBean和ServletListenerRegistrationBean这三个类分别用来注册Servlet、Filter、Listener。假如要注册一个Servlet可以这样做
```
@Bean
public ServletRegistrationBean servletRegistrationBean() {
return new ServletRegistrationBean(new HelloServlet(),&quot;/hello&quot;);
}
```
这段代码实现的方法返回一个ServletRegistrationBean并将它当作Bean注册到Spring中因此你需要把这段代码放到Spring Boot自动扫描的目录中或者放到@Configuration标识的类中
**3. 动态注册**
你还可以创建一个类去实现前面提到的ServletContextInitializer接口并把它注册为一个BeanSpring Boot会负责调用这个接口的onStartup方法。
```
@Component
public class MyServletRegister implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) {
//Servlet 3.0规范新的API
ServletRegistration myServlet = servletContext
.addServlet(&quot;HelloServlet&quot;, HelloServlet.class);
myServlet.addMapping(&quot;/hello&quot;);
myServlet.setInitParameter(&quot;name&quot;, &quot;Hello Servlet&quot;);
}
}
```
这里请注意两点:
- ServletRegistrationBean其实也是通过ServletContextInitializer来实现的它实现了ServletContextInitializer接口。
- 注意到onStartup方法的参数是我们熟悉的ServletContext可以通过调用它的addServlet方法来动态注册新的Servlet这是Servlet 3.0以后才有的功能。
## Web容器的定制
我们再来考虑一个问题那就是如何在Spring Boot中定制Web容器。在Spring Boot 2.0中我们可以通过两种方式来定制Web容器。
**第一种方式**是通过通用的Web容器工厂ConfigurableServletWebServerFactory来定制一些Web容器通用的参数
```
@Component
public class MyGeneralCustomizer implements
WebServerFactoryCustomizer&lt;ConfigurableServletWebServerFactory&gt; {
public void customize(ConfigurableServletWebServerFactory factory) {
factory.setPort(8081);
factory.setContextPath(&quot;/hello&quot;);
}
}
```
**第二种方式**是通过特定Web容器的工厂比如TomcatServletWebServerFactory来进一步定制。下面的例子里我们给Tomcat增加一个Valve这个Valve的功能是向请求头里添加traceid用于分布式追踪。TraceValve的定义如下
```
class TraceValve extends ValveBase {
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
request.getCoyoteRequest().getMimeHeaders().
addValue(&quot;traceid&quot;).setString(&quot;1234xxxxabcd&quot;);
Valve next = getNext();
if (null == next) {
return;
}
next.invoke(request, response);
}
}
```
跟第一种方式类似,再添加一个定制器,代码如下:
```
@Component
public class MyTomcatCustomizer implements
WebServerFactoryCustomizer&lt;TomcatServletWebServerFactory&gt; {
@Override
public void customize(TomcatServletWebServerFactory factory) {
factory.setPort(8081);
factory.setContextPath(&quot;/hello&quot;);
factory.addEngineValves(new TraceValve() );
}
}
```
## 本期精华
今天我们学习了Spring Boot如何利用Web容器的API来启动Web容器、如何向Web容器注册Servlet以及如何定制化Web容器除了给Web容器配置参数还可以增加或者修改Web容器本身的组件。
## 课后思考
我在文章中提到通过ServletContextInitializer接口可以向Web容器注册Servlet那ServletContextInitializer跟Tomcat中的ServletContainerInitializer有什么区别和联系呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,339 @@
<audio id="audio" title="29 | 比较Jetty如何实现具有上下文信息的责任链" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/c4/87/c4ff4e83056c1e75f919bf3ccef48687.mp3"></audio>
我们知道Tomcat和Jetty的核心功能是处理请求并且请求的处理者不止一个因此Tomcat和Jetty都实现了责任链模式其中Tomcat是通过Pipeline-Valve来实现的而Jetty是通过HandlerWrapper来实现的。HandlerWrapper中保存了下一个Handler的引用将各Handler组成一个链表像下面这样
WebAppContext -&gt; SessionHandler -&gt; SecurityHandler -&gt; ServletHandler
这样链中的Handler从头到尾能被依次调用除此之外Jetty还实现了“回溯”的链式调用那就是从头到尾依次链式调用Handler的**方法A**,完成后再回到头节点,再进行一次链式调用,只不过这一次调用另一个**方法B**。你可能会问一次链式调用不就够了吗为什么还要回过头再调一次呢这是因为一次请求到达时Jetty需要先调用各Handler的初始化方法之后再调用各Handler的请求处理方法并且初始化必须在请求处理之前完成。
而Jetty是通过ScopedHandler来做到这一点的那ScopedHandler跟HandlerWrapper有什么关系呢ScopedHandler是HandlerWrapper的子类我们还是通过一张图来回顾一下各种Handler的继承关系
<img src="https://static001.geekbang.org/resource/image/68/50/68f3668cc7b179b5311d1bb5cb3cf350.jpg" alt="">
从图上我们看到ScopedHandler是Jetty非常核心的一个Handler跟Servlet规范相关的Handler比如ContextHandler、SessionHandler、ServletHandler、WebappContext等都直接或间接地继承了ScopedHandler。
今天我就分析一下ScopedHandler是如何实现“回溯”的链式调用的。
## HandlerWrapper
为了方便理解我们先来回顾一下HandlerWrapper的源码
```
public class HandlerWrapper extends AbstractHandlerContainer
{
protected Handler _handler;
@Override
public void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
Handler handler=_handler;
if (handler!=null)
handler.handle(target,baseRequest, request, response);
}
}
```
从代码可以看到它持有下一个Handler的引用并且会在handle方法里调用下一个Handler。
## ScopedHandler
ScopedHandler的父类是HandlerWrapperScopedHandler重写了handle方法在HandlerWrapper的handle方法的基础上引入了doScope方法。
```
public final void handle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
if (isStarted())
{
if (_outerScope==null)
doScope(target,baseRequest,request, response);
else
doHandle(target,baseRequest,request, response);
}
}
```
上面的代码中是根据`_outerScope`是否为null来判断是使用doScope还是doHandle方法。那`_outScope`又是什么呢?`_outScope`是ScopedHandler引入的一个辅助变量此外还有一个`_nextScope`变量。
```
protected ScopedHandler _outerScope;
protected ScopedHandler _nextScope;
private static final ThreadLocal&lt;ScopedHandler&gt; __outerScope= new ThreadLocal&lt;ScopedHandler&gt;();
```
我们看到`__outerScope`是一个ThreadLocal变量ThreadLocal表示线程的私有数据跟特定线程绑定。需要注意的是`__outerScope`实际上保存了一个ScopedHandler。
下面通过我通过一个例子来说明`_outScope``_nextScope`的含义。我们知道ScopedHandler继承自HandlerWrapper所以也是可以形成Handler链的Jetty的源码注释中给出了下面这样一个例子
```
ScopedHandler scopedA;
ScopedHandler scopedB;
HandlerWrapper wrapperX;
ScopedHandler scopedC;
scopedA.setHandler(scopedB);
scopedB.setHandler(wrapperX);
wrapperX.setHandler(scopedC)
```
经过上面的设置之后形成的Handler链是这样的
<img src="https://static001.geekbang.org/resource/image/5f/2a/5f18bc5677f9216a9126413db4f4b22a.png" alt="">
上面的过程只是设置了`_handler`变量,那`_outScope``_nextScope`需要设置成什么样呢?为了方便你理解,我们先来看最后的效果图:
<img src="https://static001.geekbang.org/resource/image/21/4c/21a2e99691804f64d13d62ab9b3f924c.png" alt="">
从上图我们看到scopedA的`_nextScope=scopedB`scopedB的`_nextScope=scopedC`为什么scopedB的`_nextScope`不是WrapperX呢因为WrapperX不是一个ScopedHandler。scopedC的`_nextScope`是null因为它是链尾没有下一个节点。因此我们得出一个结论`_nextScope`**指向下一个Scoped节点**的引用由于WrapperX不是Scoped节点它没有`_outScope``_nextScope`变量。
注意到scopedA的`_outerScope`是nullscopedB和scopedC的`_outScope`都是指向scopedA`_outScope`**指向的是当前Handler链的头节点**,头节点本身`_outScope`为null。
弄清楚了`_outScope``_nextScope`的含义下一个问题就是对于一个ScopedHandler对象如何设置这两个值以及在何时设置这两个值。答案是在组件启动的时候下面是ScopedHandler中的doStart方法源码
```
@Override
protected void doStart() throws Exception
{
try
{
//请注意_outScope是一个实例变量而__outerScope是一个全局变量。先读取全局的线程私有变量__outerScope到_outerScope中
_outerScope=__outerScope.get();
//如果全局的__outerScope还没有被赋值说明执行doStart方法的是头节点
if (_outerScope==null)
//handler链的头节点将自己的引用填充到__outerScope
__outerScope.set(this);
//调用父类HandlerWrapper的doStart方法
super.doStart();
//各Handler将自己的_nextScope指向下一个ScopedHandler
_nextScope= getChildHandlerByClass(ScopedHandler.class);
}
finally
{
if (_outerScope==null)
__outerScope.set(null);
}
}
```
你可能会问,为什么要设计这样一个全局的`__outerScope`这是因为这个变量不能通过方法参数在Handler链中进行传递但是在形成链的过程中又需要用到它。
你可以想象当scopedA调用start方法时会把自己填充到`__scopeHandler`接着scopedA调用`super.doStart`。由于scopedA是一个HandlerWrapper类型并且它持有的`_handler`引用指向的是scopedB所以`super.doStart`实际上会调用scopedB的start方法。
这个方法里同样会执行scopedB的doStart方法不过这次`__outerScope.get`方法返回的不是null而是scopedA的引用所以scopedB的`_outScope`被设置为scopedA。
接着`super.dostart`会进入到scopedC也会将scopedC的`_outScope`指向scopedA。到了scopedC执行doStart方法时它的`_handler`属性为null因为它是Handler链的最后一个所以它的`super.doStart`会直接返回。接着继续执行scopedC的doStart方法的下一行代码
```
_nextScope=(ScopedHandler)getChildHandlerByClass(ScopedHandler.class)
```
对于HandlerWrapper来说getChildHandlerByClass返回的就是其包装的`_handler`对象这里返回的就是null。所以scopedC的`_nextScope`为null这段方法结束返回后继续执行scopedB中的doStart中同样执行这句代码
```
_nextScope=(ScopedHandler)getChildHandlerByClass(ScopedHandler.class)
```
因为scopedB的`_handler`引用指向的是scopedC所以getChildHandlerByClass返回的结果就是scopedC的引用即scopedB的`_nextScope`指向scopedC。
同理scopedA的`_nextScope`会指向scopedB。scopedA的doStart方法返回之后`_outScope`为null。请注意执行到这里只有scopedA的`_outScope`为null所以doStart中finally部分的逻辑被触发这个线程的ThreadLocal变量又被设置为null。
```
finally
{
if (_outerScope==null)
__outerScope.set(null);
}
```
你可能会问,费这么大劲设置`_outScope``_nextScope`的值到底有什么用?如果你觉得上面的过程比较复杂,可以跳过这个过程,直接通过图来理解`_outScope``_nextScope`的值而这样设置的目的是用来控制doScope方法和doHandle方法的调用顺序。
实际上在ScopedHandler中对于doScope和doHandle方法是没有具体实现的但是提供了nextHandle和nextScope两个方法下面是它们的源码
```
public void doScope(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
nextScope(target,baseRequest,request,response);
}
public final void nextScope(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
if (_nextScope!=null)
_nextScope.doScope(target,baseRequest,request, response);
else if (_outerScope!=null)
_outerScope.doHandle(target,baseRequest,request, response);
else
doHandle(target,baseRequest,request, response);
}
public abstract void doHandle(String target,
Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException;
public final void nextHandle(String target,
final Request baseRequest,
HttpServletRequest request,
HttpServletResponse response)
throws IOException, ServletException
{
if (_nextScope!=null &amp;&amp; _nextScope==_handler)
_nextScope.doHandle(target,baseRequest,request, response);
else if (_handler!=null)
super.handle(target,baseRequest,request,response);
}
```
从nextHandle和nextScope方法大致上可以猜到doScope和doHandle的调用流程。我通过一个调用栈来帮助你理解
```
A.handle(...)
A.doScope(...)
B.doScope(...)
C.doScope(...)
A.doHandle(...)
B.doHandle(...)
X.handle(...)
C.handle(...)
C.doHandle(...)
```
因此通过设置`_outScope``_nextScope`的值并且在代码中判断这些值并采取相应的动作目的就是让ScopedHandler链上的**doScope方法在doHandle、handle方法之前执行**。并且不同ScopedHandler的doScope都是按照它在链上的先后顺序执行的doHandle和handle方法也是如此。
这样ScopedHandler帮我们把调用框架搭好了它的子类只需要实现doScope和doHandle方法。比如在doScope方法里做一些初始化工作在doHanlde方法处理请求。
## ContextHandler
接下来我们来看看ScopedHandler的子类ContextHandler是如何实现doScope和doHandle方法的。ContextHandler可以理解为Tomcat中的Context组件对应一个Web应用它的功能是给Servlet的执行维护一个上下文环境并且将请求转发到相应的Servlet。那什么是Servlet执行的上下文我们通过ContextHandler的构造函数来了解一下
```
private ContextHandler(Context context, HandlerContainer parent, String contextPath)
{
//_scontext就是Servlet规范中的ServletContext
_scontext = context == null?new Context():context;
//Web应用的初始化参数
_initParams = new HashMap&lt;String, String&gt;();
...
}
```
我们看到ContextHandler维护了ServletContext和Web应用的初始化参数。那ContextHandler的doScope方法做了些什么呢我们看看它的关键代码
```
public void doScope(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
...
//1.修正请求的URL去掉多余的'/',或者加上'/'
if (_compactPath)
target = URIUtil.compactPath(target);
if (!checkContext(target,baseRequest,response))
return;
if (target.length() &gt; _contextPath.length())
{
if (_contextPath.length() &gt; 1)
target = target.substring(_contextPath.length());
pathInfo = target;
}
else if (_contextPath.length() == 1)
{
target = URIUtil.SLASH;
pathInfo = URIUtil.SLASH;
}
else
{
target = URIUtil.SLASH;
pathInfo = null;
}
//2.设置当前Web应用的类加载器
if (_classLoader != null)
{
current_thread = Thread.currentThread();
old_classloader = current_thread.getContextClassLoader();
current_thread.setContextClassLoader(_classLoader);
}
//3. 调用nextScope
nextScope(target,baseRequest,request,response);
...
}
```
从代码我们看到在doScope方法里主要是做了一些请求的修正、类加载器的设置并调用nextScope请你注意nextScope调用是由父类ScopedHandler实现的。接着我们来ContextHandler的doHandle方法
```
public void doHandle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException
{
final DispatcherType dispatch = baseRequest.getDispatcherType();
final boolean new_context = baseRequest.takeNewContext();
try
{
//请求的初始化工作,主要是为请求添加ServletRequestAttributeListener监听器,并将&quot;开始处理一个新请求&quot;这个事件通知ServletRequestListener
if (new_context)
requestInitialized(baseRequest,request);
...
//继续调用下一个Handler下一个Handler可能是ServletHandler、SessionHandler ...
nextHandle(target,baseRequest,request,response);
}
finally
{
//同样一个Servlet请求处理完毕也要通知相应的监听器
if (new_context)
requestDestroyed(baseRequest,request);
}
}
```
从上面的代码我们看到ContextHandler在doHandle方法里分别完成了相应的请求处理工作。
## 本期精华
今天我们分析了Jetty中ScopedHandler的实现原理剖析了如何实现链式调用的“回溯”。主要是确定了doScope和doHandle的调用顺序doScope依次调用完以后再依次调用doHandle它的子类比如ContextHandler只需要实现doScope和doHandle方法而不需要关心它们被调用的顺序。
这背后的原理是ScopedHandler通过递归的方式来设置`_outScope``_nextScope`两个变量然后通过判断这些值来控制调用的顺序。递归是计算机编程的一个重要的概念在各种面试题中也经常出现如果你能读懂Jetty中的这部分代码毫无疑问你已经掌握了递归的精髓。
另外我们进行层层递归调用中需要用到一些变量比如ScopedHandler中的`__outerScope`它保存了Handler链中的头节点但是它不是递归方法的参数那参数怎么传递过去呢一种可能的办法是设置一个全局变量各Handler都能访问到这个变量。但这样会有线程安全的问题因此ScopedHandler通过线程私有数据ThreadLocal来保存变量这样既达到了传递变量的目的又没有线程安全的问题。
## 课后思考
ScopedHandler的doStart方法最后一步是将线程私有变量`__outerScope`设置成null为什么需要这样做呢
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

View File

@@ -0,0 +1,212 @@
<audio id="audio" title="30 | 热点问题答疑3Spring框架中的设计模式" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/5d/99/5dc1c344dde037893983e93ae74caf99.mp3"></audio>
在构思这个专栏的时候回想当时我是如何研究Tomcat和Jetty源码的除了理解它们的实现之外也从中学到了很多架构和设计的理念其中很重要的就是对设计模式的运用让我收获到不少经验。而且这些经验通过自己消化和吸收是可以把它应用到实际工作中去的。
在专栏的热点问题答疑第三篇我想跟你分享一些我对设计模式的理解。有关Tomcat和Jetty所运用的设计模式我在专栏里已经有所介绍今天想跟你分享一下Spring框架里的设计模式。Spring的核心功能是IOC容器以及AOP面向切面编程同样也是很多Web后端工程师每天都要打交道的框架相信你一定可以从中吸收到一些设计方面的精髓帮助你提升设计能力。
## 简单工厂模式
我们来考虑这样一个场景当A对象需要调用B对象的方法时我们需要在A中new一个B的实例我们把这种方式叫作硬编码耦合它的缺点是一旦需求发生变化比如需要使用C类来代替B时就要改写A类的方法。假如应用中有1000个类以硬编码的方式耦合了B那改起来就费劲了。于是简单工厂模式就登场了简单工厂模式又叫静态工厂方法其实质是由一个工厂类根据传入的参数动态决定应该创建哪一个产品类。
Spring中的BeanFactory就是简单工厂模式的体现BeanFactory是Spring IOC容器中的一个核心接口它的定义如下
```
public interface BeanFactory {
Object getBean(String name) throws BeansException;
&lt;T&gt; T getBean(String name, Class&lt;T&gt; requiredType);
Object getBean(String name, Object... args);
&lt;T&gt; T getBean(Class&lt;T&gt; requiredType);
&lt;T&gt; T getBean(Class&lt;T&gt; requiredType, Object... args);
boolean containsBean(String name);
boolean isSingleton(String name);
boolea isPrototype(String name);
boolean isTypeMatch(String name, ResolvableType typeToMatch);
boolean isTypeMatch(String name, Class&lt;?&gt; typeToMatch);
Class&lt;?&gt; getType(String name);
String[] getAliases(String name);
}
```
我们可以通过它的具体实现类比如ClassPathXmlApplicationContext来获取Bean
```
BeanFactory bf = new ClassPathXmlApplicationContext(&quot;spring.xml&quot;);
User userBean = (User) bf.getBean(&quot;userBean&quot;);
```
从上面代码可以看到使用者不需要自己来new对象而是通过工厂类的方法getBean来获取对象实例这是典型的简单工厂模式只不过Spring是用反射机制来创建Bean的。
## 工厂方法模式
工厂方法模式说白了其实就是简单工厂模式的一种升级或者说是进一步抽象,它可以应用于更加复杂的场景,灵活性也更高。在简单工厂中,由工厂类进行所有的逻辑判断、实例创建;如果不想在工厂类中进行判断,可以为不同的产品提供不同的工厂,不同的工厂生产不同的产品,每一个工厂都只对应一个相应的对象,这就是工厂方法模式。
Spring中的FactoryBean就是这种思想的体现FactoryBean可以理解为工厂Bean先来看看它的定义
```
public interface FactoryBean&lt;T&gt; {
T getObject()
Class&lt;?&gt; getObjectType();
boolean isSingleton();
}
```
我们定义一个类UserFactoryBean来实现FactoryBean接口主要是在getObject方法里new一个User对象。这样我们通过getBean(id) 获得的是该工厂所产生的User的实例而不是UserFactoryBean本身的实例像下面这样
```
BeanFactory bf = new ClassPathXmlApplicationContext(&quot;user.xml&quot;);
User userBean = (User) bf.getBean(&quot;userFactoryBean&quot;);
```
## 单例模式
单例模式是指一个类在整个系统运行过程中只允许产生一个实例。在Spring中Bean可以被定义为两种模式Prototype多例和Singleton单例Spring Bean默认是单例模式。那Spring是如何实现单例模式的呢答案是通过单例注册表的方式具体来说就是使用了HashMap。请注意为了方便你阅读我对代码进行了简化
```
public class DefaultSingletonBeanRegistry {
//使用了线程安全容器ConcurrentHashMap保存各种单实例对象
private final Map&lt;String, Object&gt; singletonObjects = new ConcurrentHashMap&lt;String, Object&gt;;
protected Object getSingleton(String beanName) {
//先到HashMap中拿Object
Object singletonObject = singletonObjects.get(beanName);
//如果没拿到通过反射创建一个对象实例并添加到HashMap中
if (singletonObject == null) {
singletonObjects.put(beanName,
Class.forName(beanName).newInstance());
}
//返回对象实例
return singletonObjects.get(beanName);
}
}
```
上面的代码逻辑比较清晰先到HashMap去拿单实例对象没拿到就创建一个添加到HashMap。
## 代理模式
所谓代理,是指它与被代理对象实现了相同的接口,客户端必须通过代理才能与被代理的目标类进行交互,而代理一般在交互的过程中(交互前后),进行某些特定的处理,比如在调用这个方法前做前置处理,调用这个方法后做后置处理。代理模式中有下面几种角色:
- **抽象接口**:定义目标类及代理类的共同接口,这样在任何可以使用目标对象的地方都可以使用代理对象。
- **目标对象** 定义了代理对象所代表的目标对象,专注于业务功能的实现。
- **代理对象** 代理对象内部含有目标对象的引用,收到客户端的调用请求时,代理对象通常不会直接调用目标对象的方法,而是在调用之前和之后实现一些额外的逻辑。
代理模式的好处是,可以在目标对象业务功能的基础上添加一些公共的逻辑,比如我们想给目标对象加入日志、权限管理和事务控制等功能,我们就可以使用代理类来完成,而没必要修改目标类,从而使得目标类保持稳定。这其实是开闭原则的体现,不要随意去修改别人已经写好的代码或者方法。
代理又分为静态代理和动态代理两种方式。静态代理需要定义接口被代理对象目标对象与代理对象Proxy)一起实现相同的接口,我们通过一个例子来理解一下:
```
//抽象接口
public interface IStudentDao {
void save();
}
//目标对象
public class StudentDao implements IStudentDao {
public void save() {
System.out.println(&quot;保存成功&quot;);
}
}
//代理对象
public class StudentDaoProxy implements IStudentDao{
//持有目标对象的引用
private IStudentDao target;
public StudentDaoProxy(IStudentDao target){
this.target = target;
}
//在目标功能对象方法的前后加入事务控制
public void save() {
System.out.println(&quot;开始事务&quot;);
target.save();//执行目标对象的方法
System.out.println(&quot;提交事务&quot;);
}
}
public static void main(String[] args) {
//创建目标对象
StudentDao target = new StudentDao();
//创建代理对象,把目标对象传给代理对象,建立代理关系
StudentDaoProxy proxy = new StudentDaoProxy(target);
//执行的是代理的方法
proxy.save();
}
```
而Spring的AOP采用的是动态代理的方式而动态代理就是指代理类在程序运行时由JVM动态创建。在上面静态代理的例子中代理类StudentDaoProxy是我们自己定义好的在程序运行之前就已经编译完成。而动态代理代理类并不是在Java代码中定义的而是在运行时根据我们在Java代码中的“指示”动态生成的。那我们怎么“指示”JDK去动态地生成代理类呢
在Java的`java.lang.reflect`包里提供了一个Proxy类和一个InvocationHandler接口通过这个类和这个接口可以生成动态代理对象。具体来说有如下步骤
1.定义一个InvocationHandler类将需要扩展的逻辑集中放到这个类中比如下面的例子模拟了添加事务控制的逻辑。
```
public class MyInvocationHandler implements InvocationHandler {
private Object obj;
public MyInvocationHandler(Object obj){
this.obj=obj;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
System.out.println(&quot;开始事务&quot;);
Object result = method.invoke(obj, args);
System.out.println(&quot;开始事务&quot;);
return result;
}
}
```
2.使用Proxy的newProxyInstance方法动态的创建代理对象
```
public static void main(String[] args) {
//创建目标对象StudentDao
IStudentDao stuDAO = new StudentDao();
//创建MyInvocationHandler对象
InvocationHandler handler = new MyInvocationHandler(stuDAO);
//使用Proxy.newProxyInstance动态的创建代理对象stuProxy
IStudentDao stuProxy = (IStudentDao)
Proxy.newProxyInstance(stuDAO.getClass().getClassLoader(), stuDAO.getClass().getInterfaces(), handler);
//动用代理对象的方法
stuProxy.save();
}
```
上面的代码实现和静态代理一样的功能,相比于静态代理,动态代理的优势在于可以很方便地对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。
Spring实现了通过动态代理对类进行方法级别的切面增强我来解释一下这句话其实就是动态生成目标对象的代理类并在代理类的方法中设置拦截器通过执行拦截器中的逻辑增强了代理方法的功能从而实现AOP。
## 本期精华
今天我和你聊了Spring中的设计模式我记得我刚毕业那会儿拿到一个任务时我首先考虑的是怎么把功能实现了从不考虑设计的问题因此写出来的代码就显得比较稚嫩。后来随着经验的积累我会有意识地去思考这个场景是不是用个设计模式会更高大上呢以后重构起来是不是会更轻松呢慢慢我也就形成一个习惯那就是用优雅的方式去实现一个系统这也是每个程序员需要经历的过程。
今天我们学习了Spring的两大核心功能IOC和AOP中用到的一些设计模式主要有简单工厂模式、工厂方法模式、单例模式和代理模式。而代理模式又分为静态代理和动态代理。JDK提供实现动态代理的机制除此之外还可以通过CGLIB来实现有兴趣的同学可以理解一下它的原理。
## 课后思考
注意到在newProxyInstance方法中传入了目标类的加载器、目标类实现的接口以及MyInvocationHandler三个参数就能得到一个动态代理对象请你思考一下newProxyInstance方法是如何实现的。
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。