mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-19 23:53:47 +08:00
mod
This commit is contained in:
@@ -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() {
|
||||
//请注意这里传入的参数是"宿主类"的实例
|
||||
processChildren(ContainerBase.this);
|
||||
}
|
||||
|
||||
protected void processChildren(Container container) {
|
||||
try {
|
||||
//1. 调用当前容器的backgroundProcess方法。
|
||||
container.backgroundProcess();
|
||||
|
||||
//2. 遍历所有的子容器,递归调用processChildren,
|
||||
//这样当前容器的子孙都会被处理
|
||||
Container[] children = container.findChildren();
|
||||
for (int i = 0; i < children.length; i++) {
|
||||
//这里请你注意,容器基类有个变量叫做backgroundProcessorDelay,如果大于0,表明子容器有自己的后台线程,无需父容器来调用它的processChildren方法。
|
||||
if (children[i].getBackgroundProcessorDelay() <= 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. 触发容器的"周期事件",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参数来开启这个功能,像下面这样:
|
||||
|
||||
```
|
||||
<Context reloadable="true"/>
|
||||
|
||||
```
|
||||
|
||||
## 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 < 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方法来实现热部署呢?
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
|
||||
|
||||
@@ -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<?> loadClass(String name) {
|
||||
|
||||
//查找一下这个类是不是已经加载过了
|
||||
Class<?> 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<?> findClass(String name){
|
||||
//1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存
|
||||
...
|
||||
|
||||
//2. 调用defineClass将字节数组转成Class对象
|
||||
return defineClass(buf, off, len);
|
||||
}
|
||||
|
||||
// 将字节码数组解析成一个Class对象,用native方法实现
|
||||
protected final Class<?> 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又会委托给BootstrapClassLoader,BootstrapClassLoader发现自己已经加载过了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<?> findClass(String name) throws ClassNotFoundException {
|
||||
...
|
||||
|
||||
Class<?> 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<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
|
||||
Class<?> 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方法中的哪一个?还是两个都要重写?
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
|
||||
|
||||
@@ -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<?> forName(String className) {
|
||||
Class<?> 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);
|
||||
|
||||
```
|
||||
|
||||
这是为什么呢?
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
@@ -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和BasicValve,Wrapper的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<String, FilterDef> filterDefs = new HashMap<>();
|
||||
|
||||
```
|
||||
|
||||
那上面提到的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 < 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<Object> applicationEventListenersList = new CopyOnWriteArrayList<>();
|
||||
|
||||
//监听生命事件的监听器
|
||||
private Object applicationLifecycleListenersObjects[] = new Object[0];
|
||||
|
||||
```
|
||||
|
||||
剩下的事情就是触发监听器了,比如在Context容器的启动方法里,就触发了所有的ServletContextListener:
|
||||
|
||||
```
|
||||
//1.拿到所有的生命周期监听器
|
||||
Object instances[] = getApplicationLifecycleListeners();
|
||||
|
||||
for (int i = 0; i < 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容器来管理Servlet,Wrapper包装了Servlet本身以及相应的参数,这体现了面向对象中“封装”的设计原则。
|
||||
|
||||
Tomcat会给**每个请求生成一个Filter链**,Filter链中的最后一个Filter会负责调用Servlet的service方法。
|
||||
|
||||
对于Listener来说,我们可以定制自己的监听器来监听Tomcat内部发生的各种事件:包括Web应用级别的、Session级别的和请求级别的。Tomcat中的Context容器统一维护了这些监听器,并负责触发。
|
||||
|
||||
最后小结一下这3期内容,Context组件通过自定义类加载器来加载Web应用,并实现了Servlet规范,直接跟Web应用打交道,是一个核心的容器组件。也因此我用了很重的篇幅去讲解它,也非常建议你花点时间阅读一下它的源码。
|
||||
|
||||
## 课后思考
|
||||
|
||||
Context容器分别用了CopyOnWriteArrayList和对象数组来存储两种不同的监听器,为什么要这样设计,你可以思考一下背后的原因。
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
@@ -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 = {"/async"}, 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("Handling Async Servlet");
|
||||
} 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<S,Processor> connections = new ConcurrentHashMap<>();
|
||||
|
||||
```
|
||||
|
||||
之所以要缓存是因为这个请求接下来还要接着处理,还是由原来的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<?> socketWrapper = getSocketWrapper();
|
||||
if (socketWrapper != null) {
|
||||
socketWrapper.processSocket(event, dispatch);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
而SocketWrapper的processSocket方法会创建SocketProcessor任务类,并通过Tomcat线程池来处理:
|
||||
|
||||
```
|
||||
public boolean processSocket(SocketWrapperBase<S> socketWrapper,
|
||||
SocketEvent event, boolean dispatch) {
|
||||
|
||||
if (socketWrapper == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SocketProcessorBase<S> sc = processorCache.pop();
|
||||
if (sc == null) {
|
||||
sc = createSocketProcessor(socketWrapper, event);
|
||||
} else {
|
||||
sc.reset(socketWrapper, event);
|
||||
}
|
||||
//线程池运行
|
||||
Executor executor = getExecutor();
|
||||
if (dispatch && 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应用内部,是不是也可以运用这种设计思想呢?
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
|
||||
|
||||
@@ -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脚本来启动Tomcat,Tomcat中的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<T extends WebServerFactory> {
|
||||
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 && servletContext == null) {
|
||||
//通过Web容器工厂来创建
|
||||
ServletWebServerFactory factory = this.getWebServerFactory();
|
||||
//注意传入了一个"SelfInitializer"
|
||||
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("tomcat");
|
||||
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. 创建定制版的"Context"组件。
|
||||
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("/hello")
|
||||
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(),"/hello");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这段代码实现的方法返回一个ServletRegistrationBean,并将它当作Bean注册到Spring中,因此你需要把这段代码放到Spring Boot自动扫描的目录中,或者放到@Configuration标识的类中。
|
||||
|
||||
**3. 动态注册**
|
||||
|
||||
你还可以创建一个类去实现前面提到的ServletContextInitializer接口,并把它注册为一个Bean,Spring Boot会负责调用这个接口的onStartup方法。
|
||||
|
||||
```
|
||||
@Component
|
||||
public class MyServletRegister implements ServletContextInitializer {
|
||||
|
||||
@Override
|
||||
public void onStartup(ServletContext servletContext) {
|
||||
|
||||
//Servlet 3.0规范新的API
|
||||
ServletRegistration myServlet = servletContext
|
||||
.addServlet("HelloServlet", HelloServlet.class);
|
||||
|
||||
myServlet.addMapping("/hello");
|
||||
|
||||
myServlet.setInitParameter("name", "Hello Servlet");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里请注意两点:
|
||||
|
||||
- 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<ConfigurableServletWebServerFactory> {
|
||||
|
||||
public void customize(ConfigurableServletWebServerFactory factory) {
|
||||
factory.setPort(8081);
|
||||
factory.setContextPath("/hello");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**第二种方式**是通过特定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("traceid").setString("1234xxxxabcd");
|
||||
|
||||
Valve next = getNext();
|
||||
if (null == next) {
|
||||
return;
|
||||
}
|
||||
|
||||
next.invoke(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
跟第一种方式类似,再添加一个定制器,代码如下:
|
||||
|
||||
```
|
||||
@Component
|
||||
public class MyTomcatCustomizer implements
|
||||
WebServerFactoryCustomizer<TomcatServletWebServerFactory> {
|
||||
|
||||
@Override
|
||||
public void customize(TomcatServletWebServerFactory factory) {
|
||||
factory.setPort(8081);
|
||||
factory.setContextPath("/hello");
|
||||
factory.addEngineValves(new TraceValve() );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 本期精华
|
||||
|
||||
今天我们学习了Spring Boot如何利用Web容器的API来启动Web容器、如何向Web容器注册Servlet,以及如何定制化Web容器,除了给Web容器配置参数,还可以增加或者修改Web容器本身的组件。
|
||||
|
||||
## 课后思考
|
||||
|
||||
我在文章中提到,通过ServletContextInitializer接口可以向Web容器注册Servlet,那ServletContextInitializer跟Tomcat中的ServletContainerInitializer有什么区别和联系呢?
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
|
||||
|
||||
@@ -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 -> SessionHandler -> SecurityHandler -> 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的父类是HandlerWrapper,ScopedHandler重写了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<ScopedHandler> __outerScope= new ThreadLocal<ScopedHandler>();
|
||||
|
||||
```
|
||||
|
||||
我们看到`__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`是null,scopedB和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 && _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<String, String>();
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们看到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() > _contextPath.length())
|
||||
{
|
||||
if (_contextPath.length() > 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监听器,并将"开始处理一个新请求"这个事件通知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,为什么需要这样做呢?
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
<audio id="audio" title="30 | 热点问题答疑(3):Spring框架中的设计模式" 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;
|
||||
<T> T getBean(String name, Class<T> requiredType);
|
||||
Object getBean(String name, Object... args);
|
||||
<T> T getBean(Class<T> requiredType);
|
||||
<T> T getBean(Class<T> 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<?> typeToMatch);
|
||||
Class<?> getType(String name);
|
||||
String[] getAliases(String name);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们可以通过它的具体实现类(比如ClassPathXmlApplicationContext)来获取Bean:
|
||||
|
||||
```
|
||||
BeanFactory bf = new ClassPathXmlApplicationContext("spring.xml");
|
||||
User userBean = (User) bf.getBean("userBean");
|
||||
|
||||
```
|
||||
|
||||
从上面代码可以看到,使用者不需要自己来new对象,而是通过工厂类的方法getBean来获取对象实例,这是典型的简单工厂模式,只不过Spring是用反射机制来创建Bean的。
|
||||
|
||||
## 工厂方法模式
|
||||
|
||||
工厂方法模式说白了其实就是简单工厂模式的一种升级或者说是进一步抽象,它可以应用于更加复杂的场景,灵活性也更高。在简单工厂中,由工厂类进行所有的逻辑判断、实例创建;如果不想在工厂类中进行判断,可以为不同的产品提供不同的工厂,不同的工厂生产不同的产品,每一个工厂都只对应一个相应的对象,这就是工厂方法模式。
|
||||
|
||||
Spring中的FactoryBean就是这种思想的体现,FactoryBean可以理解为工厂Bean,先来看看它的定义:
|
||||
|
||||
```
|
||||
public interface FactoryBean<T> {
|
||||
T getObject();
|
||||
Class<?> getObjectType();
|
||||
boolean isSingleton();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们定义一个类UserFactoryBean来实现FactoryBean接口,主要是在getObject方法里new一个User对象。这样我们通过getBean(id) 获得的是该工厂所产生的User的实例,而不是UserFactoryBean本身的实例,像下面这样:
|
||||
|
||||
```
|
||||
BeanFactory bf = new ClassPathXmlApplicationContext("user.xml");
|
||||
User userBean = (User) bf.getBean("userFactoryBean");
|
||||
|
||||
```
|
||||
|
||||
## 单例模式
|
||||
|
||||
单例模式是指一个类在整个系统运行过程中,只允许产生一个实例。在Spring中,Bean可以被定义为两种模式:Prototype(多例)和Singleton(单例),Spring Bean默认是单例模式。那Spring是如何实现单例模式的呢?答案是通过单例注册表的方式,具体来说就是使用了HashMap。请注意为了方便你阅读,我对代码进行了简化:
|
||||
|
||||
```
|
||||
public class DefaultSingletonBeanRegistry {
|
||||
|
||||
//使用了线程安全容器ConcurrentHashMap,保存各种单实例对象
|
||||
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>;
|
||||
|
||||
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("保存成功");
|
||||
}
|
||||
}
|
||||
|
||||
//代理对象
|
||||
public class StudentDaoProxy implements IStudentDao{
|
||||
//持有目标对象的引用
|
||||
private IStudentDao target;
|
||||
public StudentDaoProxy(IStudentDao target){
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
//在目标功能对象方法的前后加入事务控制
|
||||
public void save() {
|
||||
System.out.println("开始事务");
|
||||
target.save();//执行目标对象的方法
|
||||
System.out.println("提交事务");
|
||||
}
|
||||
}
|
||||
|
||||
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("开始事务");
|
||||
Object result = method.invoke(obj, args);
|
||||
System.out.println("开始事务");
|
||||
|
||||
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方法是如何实现的。
|
||||
|
||||
不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user