Spring Boot 嵌入式容器

我们都知道,默认情况下 SpringBoot 应用可以打jar包运行,在启动IOC容器时引导启动嵌入式Web容器(Tomcat),而且根据之前的IOC原理,我们知道在 ServletWebServerApplicationContextonRefresh 方法中创建了嵌入式 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);
    }
}

ServletWebServerApplicationContextonRefresh 方法会调用 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也就不进入了。

img

4.2 findContext:获取第一个Context

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

img

拿到之后,返回去。

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 方法:

img

4.5.1 StandardServer#init

来到父类 LifecycleBase 中(StandardServer 没有重写):

上面对于不为NEW的状态,会额外执行方法,当前状态为NEW,不进入,走下面的try块。try块在 initInternal 的前后设置了两次生命周期的状态(初始化中、初始化完成),说明 initInternal 方法中一定是真正的 Tomcat 组件初始化的过程。

4.5.2 StandardServer#initInternal

来到 StandardServer

先来到父类 LifecycleBaseinitInternal 方法:

默认情况下 oname 为null,走下面的初始化过程。初始化完成后的效果:

img

回到子类 StandardServer

中间的一大段我们暂且不关心,主要来看最后一步:它要初始化这些 Server 中的 Service

4.5.3 又回到LifecycleBase

再走一遍流程,进到 initInternal 方法,这次初始化的是 StandardService

4.5.4 StandardService#initInternal

发现了这里面要依次初始化几个组件:EngineExecutorLifecycleMBeanBase(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 ,它负责连接 ConnectorContainer ,也就是 CoyoteServlet容器

最下面的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 的组件初始化步骤顺序如下:

  1. Server

  2. Service

  3. Engine

  4. Executor

  5. MapperListener

  6. Connector

  7. Protocol

  8. EndPoint


至此,初始化过程完毕,回到 start 方法中:

接下来到了真正启动的部分了:startInternal

4.6 StandardServer#startInternal

startInternal 方法中有两部分启动:globalNamingResources 启动,services 启动。分别来看:

4.6.1 globalNamingResources.start

来到 NamingResourcesImpl ,因为它也实现了 LifecycleBase ,还是会来到上面的 startInternal 方法中:

这部分只是发布事件和设置状态而已,与之前一致,不再赘述。

4.6.2 StandardService#start

最终又来到 startInternal 方法了:

发现这部分与之前的初始化几乎一致!也是依次启动 EngineExecutorMapperListenerConnector

4.6.3 Engine#start

它直接调的父类 ContainerBasestartInternal 方法:

这里面又是嵌套的初始化了其他组件,一一来看:

4.6.4 new StartChild(children[i])

这部分比较有意思,它用了异步初始化,先来看看 StartChild 的定义:

它实现了带返回值的异步多线程接口 **Callable** !那里面的核心方法就是 **call**

它在这里初始化 child,而通过Debug得知 child 的类型是 StandardHost,故来到 StandardHoststart 方法:

上面的一个大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

img

为什么之前还看到一个 Connector ,现在就没了呢?还记得在 this.tomcat.start(); 之前有一个监听器吗?

很明显就是这一步把 Connector 删了嘛!

Remove service connectors so that protocol binding doesn't happen when the service is started.

删除 ServiceConnector ,以便在启动服务时不会发生协议绑定。

那大概率就是这一步让这个家伙给删了。注意这个监听器是在 context 中注册的,那我们猜想,应该是 Context 启动时触发的这个监听效果。

回过头来重新Debug一次,发现在 Engine 启动之后,Connector 就已经没了。

img

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

img

至于为什么要删除的原因,上面4.3章节也描述了,是为了防止 SpringBoot 应用还没有初始化完成时就已经可以接收客户端的请求


至此,tomcat.start(); 方法彻底执行完成。

4.6.9 启动小结

启动过程依次启动了如下组件:

  1. NamingResources

  2. Service

  3. Engine

  4. Host

  5. Context

  6. Wrapper

  7. MapperListener

4.7 回到initialize

嵌入式 Tomcat 启动完成后,在try块的最底下会起一个新的线程,阻止 Tomcat 结束。

4.8 startDaemonAwaitThread

这里面它会起一个新的 awaitThread 线程,并回调 Tomcat 中 Serverawait 方法,并且它还设置 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 呢?追踪源码,发现在 TomcatgetServer 方法中有设置:

至此,嵌入式 Tomcat 已经成功创建好,但 Connector 还没有归还,还在被删除中。

5. ServletWebServerApplicationContext#startWebServer

当IOC容器的 onRefresh 方法执行完,单实例Bean初始化完成后,来到 finishRefresh 方法:

在这里它会真正启动嵌入式 Tomcat 容器:

可以看到它在这里调用了 TomcatWebServerstart 方法。

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 完整启动。

小结

  1. 嵌入式 Tomcat 与外置的 Tomcat 在核心组件上都是一样的,主要包括 ServiceConnectorEngineHostContext

  2. Tomcat 的启动过程分为初始化和启动两个步骤,分别按照核心组件的顺序启动。

  3. 嵌入式 Tomcat 在启动时要先移除掉 Connector ,防止IOC容器还没有全部启动完成后就能接收客户端的请求。

最后更新于

这有帮助吗?