Spring Boot 嵌入式容器
我们都知道,默认情况下 SpringBoot 应用可以打jar包运行,在启动IOC容器时引导启动嵌入式Web容器(Tomcat),而且根据之前的IOC原理,我们知道在 ServletWebServerApplicationContext 的 onRefresh 方法中创建了嵌入式 Tomcat,这一篇咱来展开研究嵌入式 Tomcat 创建的过程。
0. 前置知识
Tomcat 的内部核心结构包含如下组件:
Service:一个 Tomcat-Server 可以有多个 Service , Service 中包含下面的所有组件。
Connector:用于与客户端交互,接收客户端的请求,并将结果响应给客户端。
Engine:负责处理来自 Service 中的 Connector 的所有请求。
Host:可理解为主机,一个主机绑定一个端口号。
Context:可理解为应用,一个主机下有多个应用,一个应用中有多个 Servlet (可以简单理解为 webapps 中一个文件夹代表一个 Context )。
1. ServletWebServerApplicationContext#onRefresh
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}在 ServletWebServerApplicationContext 的 onRefresh 方法会调用 createWebServer 方法创建嵌入式 Tomcat。
这部分源码我们之前也都看过,重点来看 getWebServer 中的创建嵌入式 Tomcat 的部分:
2. TomcatServletWebServerFactory#getWebServer
咱来看这个方法,首先它new了一个 Tomcat 对象(它还不是真正的嵌入式 Tomcat),之后下面的一大堆都是对 Tomcat 的配置,这里面着重看一眼这个最复杂的 prepareContext 方法:
2.1 prepareContext
回到 getWebServer 中,最下面执行最核心的方法: getTomcatWebServer
3. getTomcatWebServer
它只是new了一个 TomcatWebServer 而已,进入构造方法中:
构造方法中竟然还暗藏玄机,它在属性赋值后执行了 initialize 方法。
4. initialize
把整个启动过程分为几步来看:
4.1 addInstanceIdToEngineName:设置Engine的id
这部分在初始化时,containerCounter 的值是-1,调用 incrementAndGet 方法后返回0。因为是0,下面的if也就不进入了。

4.2 findContext:获取第一个Context
这一步它要拿到Tomcat中的 host,来获取它的 children 。通过Debug发现已经存在一组 Container 了,当然也只有一个:

拿到之后,返回去。
4.3 addLifecycleListener:添加监听器
这一步的单行注释是解释不让 Connector 初始化,可为什么人家组件都初始化了,就单单不让它初始化呢?这要回归到IOC容器的启动原理中。
创建嵌入式 Tomcat 的时机是 onRefresh 方法,此时还有很多单实例Bean没有被创建,此时如果直接初始化所有组件后,Connector 也被初始化,此时客户端就可以与 Tomcat 进行交互,但这个时候单实例Bean还没有初始化完毕(尤其是 DispatcherServlet),就会导致传入的请求 Tomcat 无法处理,出现异常。
所以 SpringBoot 为了避免这个问题,会在嵌入式 Tomcat 发布事件时检测此时的 **Context** 状态是否为 **"START_EVENT"** ,如果是则将这些 **Connector** 先移除掉。
4.4 this.tomcat.start:启动Tomcat
第一步的操作跟前面是一样的,下面是 server.start() ,它来真正启动嵌入式 Tomcat 。
4.5 server.start
来到 LifecycleBase :
这里启动时会根据当前的状态来走不同的分支,而刚启动的 Tomcat 状态为 "NEW" ,进入 init 方法:

4.5.1 StandardServer#init
来到父类 LifecycleBase 中(StandardServer 没有重写):
上面对于不为NEW的状态,会额外执行方法,当前状态为NEW,不进入,走下面的try块。try块在 initInternal 的前后设置了两次生命周期的状态(初始化中、初始化完成),说明 initInternal 方法中一定是真正的 Tomcat 组件初始化的过程。
4.5.2 StandardServer#initInternal
来到 StandardServer :
先来到父类 LifecycleBase 的 initInternal 方法:
默认情况下 oname 为null,走下面的初始化过程。初始化完成后的效果:

回到子类 StandardServer :
中间的一大段我们暂且不关心,主要来看最后一步:它要初始化这些 Server 中的 Service 。
4.5.3 又回到LifecycleBase
再走一遍流程,进到 initInternal 方法,这次初始化的是 StandardService:
4.5.4 StandardService#initInternal
发现了这里面要依次初始化几个组件:Engine,Executor,LifecycleMBeanBase(mapperListener),Connector 。
到这里咱应该有种意识:这几个家伙的初始化不会又回到 LifecycleBase 中了吧?可以很确定的回答:基本是的。。。所以接下来咱就不贴 LifecycleBase 的源码了,直接看这几个组件的实现吧:
4.5.5 StandardEngine#initInternal
很明显这里是初始化Realm的,且实现很简单,不再展开。
4.5.6 Executor#initInternal
它还是调的父类 LifecycleMBeanBase 的方法,不再赘述。
4.5.7 MapperListener#initInternal
MapperListener 没有重写 initInternal 方法,相当于也跟上面一样,不再赘述。
跟上面一样,不再赘述。
4.5.8 Connector#initInternal
中间部分它初始化了一个 CoyoteAdapter ,它负责连接 Connector 和 Container ,也就是 Coyote 和 Servlet容器。
最下面的try-catch中,它又调用了 protocolHandler.init :
4.5.9 protocolHandler.init
来到 AbstractHttp11Protocol :
Debug发现这个 upgradeProtocols 为空,直接走下面父类(AbstractProtocol)的 init 方法:
上面又是一堆初始化,这个咱暂且不关注,注意最底下有一个 endpoint.init :
4.5.10 endpoint.init
来到 AbstractEndPoint :
这里面又是初始化 oname ,又是配置 socketProperties 的,但这里面再也没见到 init 方法,证明这部分初始化过程已经结束了。
值得注意的是,Debug发现 bindOnInit 变量为false,说明嵌入式 Tomcat 不在初始化期间绑定端口号。
4.5.11 初始化小结
嵌入式 Tomcat 的组件初始化步骤顺序如下:
Server
Service
Engine
Executor
MapperListener
Connector
Protocol
EndPoint
至此,初始化过程完毕,回到 start 方法中:
接下来到了真正启动的部分了:startInternal
4.6 StandardServer#startInternal
startInternal 方法中有两部分启动:globalNamingResources 启动,services 启动。分别来看:
4.6.1 globalNamingResources.start
来到 NamingResourcesImpl ,因为它也实现了 LifecycleBase ,还是会来到上面的 startInternal 方法中:
这部分只是发布事件和设置状态而已,与之前一致,不再赘述。
4.6.2 StandardService#start
最终又来到 startInternal 方法了:
发现这部分与之前的初始化几乎一致!也是依次启动 Engine 、Executor 、MapperListener 、Connector 。
4.6.3 Engine#start
它直接调的父类 ContainerBase 的 startInternal 方法:
这里面又是嵌套的初始化了其他组件,一一来看:
4.6.4 new StartChild(children[i])
这部分比较有意思,它用了异步初始化,先来看看 StartChild 的定义:
它实现了带返回值的异步多线程接口 **Callable** !那里面的核心方法就是 **call** :
它在这里初始化 child,而通过Debug得知 child 的类型是 StandardHost,故来到 StandardHost 的 start 方法:
上面的一个大if结构是设置错误提示页面的,下面又调父类的 startInternal :
又回来了。。。因为一个 Host 包含一个 Context 。
Host 搜索children就会搜到它下面的 Context ,之后又是下面的初始化过程,进入 Context 的初始化:
4.6.5 TomcatEmbeddedContext#start
这个方法非常非常长(300行+),小册就不全部贴出来了,咱现在是启动阶段,那我们只关心里面的 start 相关代码。
借由前面的Debug过程,我们先总结一个规律:所有带生命周期性质的组件,都会在启动时走到 **startInternal** 方法。
那我们接下来Debug这个300行+的代码时,只需要在方法头和方法尾打上断点,中间部分只要走 startInternal 方法的,就是组件的初始化。通过Debug,发现有如下组件被调用了 start 方法:
StandardRoot
DirResourceSet
WebappLoader
JarResourceSet
StandardWrapper
StandardPineline
StandardWrapperValve
NonLoginAuthenticator
StandardContextValve
StandardManager
LazySessionIdGenerator
组件的 startInternal 方法就不一一列举了,小册把这些组件的核心功能列举一下,有兴趣的小伙伴可以深入研究一下,小册也只是引导小伙伴们对嵌入式 Tomcat 的底层原理有一个大概的认识和了解。
4.6.6 Executor#start
Engine 启动完成后,下一步到了 Executor 的启动。但由于 Executor 没有实现 startInternal 方法,故这一步不再展开。
4.6.7 MapperListener#start
Executor 启动完成后,接下来启动 MapperListener :
这里面它干了三件事请:获取主机名,将监听器注册到各组件中,将各组件注册到监听器(实现双向)。咱主要看一眼两方互相注册的动作:
4.6.7.1 addListeners
很明显这是递归调用,而且从 Engine 开始一层一层往下执行,都把当前监听器注册进去。
4.6.7.2 registerHost
注意这里面的for循环,又调了 registerContext ,可见这个思路也跟上面差不多,类似于递归,不过这是手动一步一步往里设置(毕竟类型不一样)。
4.6.8 Connector#start
最后一步是启动 Connector 。但通过Debug发现根本没有 Connector !

为什么之前还看到一个 Connector ,现在就没了呢?还记得在 this.tomcat.start(); 之前有一个监听器吗?
很明显就是这一步把 Connector 删了嘛!
Remove service connectors so that protocol binding doesn't happen when the service is started.
删除 Service 的 Connector ,以便在启动服务时不会发生协议绑定。
那大概率就是这一步让这个家伙给删了。注意这个监听器是在 context 中注册的,那我们猜想,应该是 Context 启动时触发的这个监听效果。
回过头来重新Debug一次,发现在 Engine 启动之后,Connector 就已经没了。

将断点打在上面 context 的监听器上,放行,发现果然 在 **TomcatEmbeddedContext** 的启动期间触发了这个监听器。

至于为什么要删除的原因,上面4.3章节也描述了,是为了防止 SpringBoot 应用还没有初始化完成时就已经可以接收客户端的请求。
至此,tomcat.start(); 方法彻底执行完成。
4.6.9 启动小结
启动过程依次启动了如下组件:
NamingResources
Service
Engine
Host
Context
Wrapper
MapperListener
4.7 回到initialize
嵌入式 Tomcat 启动完成后,在try块的最底下会起一个新的线程,阻止 Tomcat 结束。
4.8 startDaemonAwaitThread
这里面它会起一个新的 awaitThread 线程,并回调 Tomcat 中 Server 的 await 方法,并且它还设置 Daemon 为false。
先解释一下为什么设置 Daemon :Tomcat 中所有的进程都是 Daemon 线程,在Java应用中,只要有一个非 Daemon 线程还在运行,则 Daemon 线程就不会停止,整个应用也不会终止。既然要让 Tomcat 一直运行以监听客户端请求,就必须需要让 Tomcat 内部的 Daemon 线程都存活,根据前面的描述,就必须制造一个能卡住停止的非 Daemon 线程。于是上面新起的 awaitThread 线程就被设置为非 Daemon 线程。
下面看看线程中执行的 await 方法:(源码很长,只截取出跟 SpringBoot 嵌入式 Tomcat 有关的部分)
可以发现,如果设置的 Tomcat 的退出端口是 -1,则代表是嵌入式 Tomcat,它会每10秒会检查一次stopAwait的值,如果为true则停止卡线程,让 Tomcat 停止。
默认请款下 Tomcat 的退出端口是8005,为什么这里会变成 -1 呢?追踪源码,发现在 Tomcat 的 getServer 方法中有设置:
至此,嵌入式 Tomcat 已经成功创建好,但 Connector 还没有归还,还在被删除中。
5. ServletWebServerApplicationContext#startWebServer
当IOC容器的 onRefresh 方法执行完,单实例Bean初始化完成后,来到 finishRefresh 方法:
在这里它会真正启动嵌入式 Tomcat 容器:
可以看到它在这里调用了 TomcatWebServer 的 start 方法。
6. TomcatWebServer#start
源码中的注释已解释的比较清楚,下面分述源码中两个重要的环节:还原 Connector 和启动 Connector :
6.1 addPreviouslyRemovedConnectors:还原Connector
可以发现它将一个缓存区的 Connector 一个一个取出放入 Service 中。注意在 service.addConnector 中有顺便启动的部分:
6.1.1 service.addConnector
前面的部分是取出 Connector ,并与 Service 绑定,之后中间部分的try块,会启动 Connector :
6.1.2 connector.start
Connector 的启动会引发 ProtocolHandler 的启动:
6.1.3 protocolHandler.start
ProtocolHandler 的启动会引发 EndPoint 的启动,至此所有组件均已启动完毕。
6.2 performDeferredLoadOnStartup:延迟启动
发现这里面会延迟启动 TomcatEmbeddedContext ,此处它对比较老的表现层框架(如Struts)做了一些兼容支持,主要是替换类加载器,由于 SpringBoot 默认使用WebMvc或WebFlux,已不采用过老的表现层框架,故此处不再展开讨论。
至此,嵌入式 Tomcat 完整启动。
小结
嵌入式 Tomcat 与外置的 Tomcat 在核心组件上都是一样的,主要包括
Service、Connector、Engine、Host、Context。Tomcat 的启动过程分为初始化和启动两个步骤,分别按照核心组件的顺序启动。
嵌入式 Tomcat 在启动时要先移除掉
Connector,防止IOC容器还没有全部启动完成后就能接收客户端的请求。
最后更新于
这有帮助吗?