精尽Spring Boot源码分析 - 内嵌Tomcat容器的实现
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读
Spring Boot 版本:2.2.x
最好对 Spring 源码有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 - 文章导读》 系列文章
如果该篇内容对您有帮助,麻烦点击一下“推荐”,也可以关注博主,感激不尽~
该系列其他文章请查看:《精尽 Spring Boot 源码分析 - 文章导读》
概述
我们知道 Spring Boot 能够创建独立的 Spring 应用,内部嵌入 Tomcat 容器(Jetty、Undertow),让我们的 jar
无需放入 Servlet 容器就能直接运行。那么对于 Spring Boot 内部嵌入 Tomcat 容器的实现你是否深入的学习过?或许你可以通过这篇文章了解到相关内容。
在上一篇 《SpringApplication 启动类的启动过程》 文章分析了 SpringApplication#run(String... args)
启动 Spring 应用的主要流程,不过你是不是没有看到和 Tomcat 相关的初始化工作呢?
别急,在刷新 Spring 应用上下文的过程中会调用 onRefresh()
方法,在 Spring Boot 的 ServletWebServerApplicationContext
中重写了该方法,此时会创建一个 Servlet 容器(默认为 Tomcat),并添加 IoC 容器中的 Servlet、Filter 和 EventListener 至 Servlet 上下文。
例如 Spring MVC 中的核心组件 DispatcherServlet
对象会添加至 Servlet 上下文,不熟悉 Spring MVC 的小伙伴可查看我前面的 《精尽Spring MVC源码分析 - 一个请求的旅行过程》 这篇文章。同时,在 《精尽Spring MVC源码分析 - 寻找遗失的 web.xml》 这篇文章中有提到过 Spring Boot 是如何加载 Servlet 的,感兴趣的可以先去看一看,本文会做更加详细的分析。
接下来,我们一起来看看 Spring Boot 内嵌 Tomcat 的实现。
文章的篇幅有点长,处理过程有点绕,每个小节我都是按照优先顺序来展述的,同时,主要的流程也标注了序号,请耐心查看📝
如何使用
在我们的 Spring Boot 项目中通常会引入 spring-boot-starter-web
这个依赖,该模块提供全栈的 WEB 开发特性,包括 Spring MVC 依赖和 Tomcat 容器,这样我们就可以打成 jar
包直接启动我们的应用,如下:
如果不想使用内嵌的 Tomcat,我们可以这样做:
然后启动类这样写:
这样你打成 war
包就可以放入外部的 Servlet 容器中运行了,具体实现查看下一篇文章,本文分析的主要是 Spring Boot 内嵌 Tomcat 的实现。
回顾
在上一篇 《SpringApplication 启动类的启动过程》 文章分析 SpringApplication#run(String... args)
启动 Spring 应用的过程中讲到,在创建好 Spring 应用上下文后,会调用其 AbstractApplication#refresh()
方法刷新上下文,该方法涉及到 Spring IoC 的所有内容,参考 《死磕Spring之IoC篇 - Spring 应用上下文 ApplicationContext》
在该方法的第 10
步可以看到会调用 onRefresh()
方法再进行一些初始化工作,这个方法交由子类进行扩展,那么在 Spring Boot 中的 ServletWebServerApplicationContext
重写了该方法,会创建一个 Servlet 容器(默认为 Tomcat),也就是当前 Spring Boot 应用所运行的 Web 环境。
第 13
步会调用 onRefresh()
方法,ServletWebServerApplicationContext
重写了该方法,启动 WebServer,对 Servlet 进行加载并初始化
类图
由于整个 ApplicationContext 体系比较庞大,下面列出了部分类
DispatcherServlet 自动配置类
在开始之前,我们先来看看 org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
这个自动配置类,部分如下:
这个 DispatcherServletAutoConfiguration
自动配置类,会在你引入 spring-boot-starter-web
模块后生效,因为该模块引入了 spring mvc
和 tomcat
相关依赖,关于 Spring Boot 的自动配置功能在后续文章进行分析。
在这里会注入 DispatcherServletRegistrationBean
(继承 RegistrationBean )对象,它关联着一个 DispatcherServlet
对象。在后面会讲到 Spring Boot 会找到所有 RegistrationBean对象,然后往 Servlet 上下文中添加 Servlet 或者 Filter。
ServletWebServerApplicationContext
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext
,Spring Boot 应用 SERVLET 类型(默认)对应的 Spring 上下文对象
接下来,我们一起来看看它重写的 onRefresh()
和 finishRefresh()
方法
1. onRefresh 方法
首先会调用父类方法,初始化 ThemeSource 对象,然后调用自己的 createWebServer()
方法,创建一个 WebServer 服务(默认 Tomcat),并初始化 ServletContext 上下文,如下:
过程如下:
-
获取当前
WebServer
容器对象,首次进来为空 -
获取
ServletContext
上下文对象 -
如果
WebServer
和ServletContext
都为空,则需要创建一个,此时使用 Spring Boot 内嵌 Tomcat 容器则会进入该分支-
获取 Servlet 容器工厂对象(默认为 Tomcat)
factory
,如下:在
spring-boot-autoconfigure
中有一个ServletWebServerFactoryConfiguration
配置类会注册一个TomcatServletWebServerFactory
对象加上
TomcatServletWebServerFactoryCustomizer
自动配置类,可以将server.*
相关的配置设置到该对象中,这一步不深入分析,感兴趣可以去看一看 -
先创建一个
ServletContextInitializer
Servlet 上下文初始器,实现也就是当前类的this#selfInitialize(ServletContext)
方法,如下:这个
ServletContextInitializer
在后面会被调用,请记住这个方法 -
从
factory
工厂中创建一个WebServer
容器对象,例如创建一个TomcatWebServer
容器对象,并初始化ServletContext
上下文,该过程会创建一个Tomcat
容器并启动,启动过程异步触发了TomcatStarter#onStartup
方法,也就会调用第2
步的ServletContextInitializer#selfInitialize(ServletContext)
方法
-
-
否则,如果
ServletContext
不为空,说明使用了外部的 Servlet 容器(例如 Tomcat)- 那么这里主动调用
this#selfInitialize(ServletContext)
方法来注册各种 Servlet、Filter
- 那么这里主动调用
-
将 ServletContext 的一些初始化参数关联到当前 Spring 应用的 Environment 环境中
整个过程有点绕,如果获取到的 WebServer
和 ServletContext
都为空,说明需要使用内嵌的 Tomcat 容器,那么第 3
步就开始进行 Tomcat 的初始化工作;
这里第 4
步的分支也很关键,如果 ServletContext
不为空,说明使用了外部的 Servlet 容器(例如 Tomcat),关于 Spring Boot 应用打成 war
包支持放入外部的 Servlet 容器运行的原理在下一篇文章进行分析。
TomcatServletWebServerFactory
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
,Tomcat 容器工厂,用于创建 TomcatWebServer 对象
1.1 getWebServer 方法
getWebServer(ServletContextInitializer... initializers)
方法,创建一个 TomcatWebServer 容器对象,并初始化 ServletContext
上下文,创建 Tomcat
容器并启动
过程如下:
-
禁用 MBean 注册中心
-
创建一个 Tomcat 对象
tomcat
-
创建一个临时目录(退出时删除)
-
将这个临时目录作为 Tomcat 的目录
-
创建一个 NIO 协议的 Connector 连接器对象,并添加到第
2
步创建的tomcat
中 -
对 Connector 进行配置,设置
server.port
端口、编码、server.tomcat.min-spare-threads
最小空闲线程和server.tomcat.accept-count
最大线程数。这些配置就是我们自己配置的,在前面 1. onRefresh 方法 的第3
步有提到 -
禁止自动部署
-
同时支持多个 Connector 连接器(默认没有)
-
调用
prepareContext(..)
方法,创建一个TomcatEmbeddedContext
上下文对象,并进行初始化工作,配置TomcatStarter
作为启动器,会将这个上下文对象设置到当前tomcat
中去 -
调用
getTomcatWebServer(Tomcat)
方法,创建一个TomcatWebServer
容器对象,是对tomcat
的封装,用于控制 Tomcat 服务器
整个 Tomcat 的初始化过程没有特别的复杂,主要是因为这里没有深入分析,我们知道大致的流程即可,这里我们重点关注第 9
和 10
步,接下来依次分析
1.1.1 prepareContext 方法
prepareContext(Host, ServletContextInitializer[])
方法,创建一个 TomcatEmbeddedContext 上下文对象,并进行初始化工作,配置 TomcatStarter 作为启动器,会将这个上下文对象设置到 Tomcat 的 Host 中去,如下:
整个过程我们挑主要的流程来看:
-
创建一个 TomcatEmbeddedContext 上下文对象
context
,接下来进行一系列的配置 -
设置
context-path
-
设置 Tomcat 根目录
-
注册默认的 Servlet 为
org.apache.catalina.servlets.DefaultServlet
-
将这个
context
上下文对象添加到tomcat
中去 -
调用
configureContext(..)
方法,对context
进行配置,例如配置TomcatStarter
启动器,它是对 ServletContext 上下文对象的初始器initializersToUse
的封装
可以看到 Tomcat 上下文对象设置了 context-path
,也就是我们的配置的 server.servlet.context-path
属性值。
同时,在第 6
步会调用方法对 Tomcat 上下文对象进一步配置
1.1.2 configureContext 方法
configureContext(Context, ServletContextInitializer[])
方法,对 Tomcat 上下文对象,主要配置 TomcatStarter
启动器,如下:
配置过程如下:
-
创建一个
TomcatStarter
启动器,此时把ServletContextInitializer
数组传入进去了,并设置到context
上下文中 -
设置错误页面
-
配置
context
上下文的 Session 会话,例如超时会话时间 -
对
context
上下文进行自定义处理,例如添加 WsContextListener 监听器
重点来了,这里设置了一个 TomcatStarter
对象,它实现了 javax.servlet.ServletContainerInitializer
接口,目的就是触发 Spring Boot 自己的 ServletContextInitializer
这个对象。
注意,入参中的 ServletContextInitializer
数组是什么,你可以一直往回跳,有一个对象就是 ServletWebServerApplicationContext#selfInitialize(ServletContext)
这个方法,到时候会触发它。关键!!!
javax.servlet.ServletContainerInitializer
是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的onStartup(..)
方法处理,我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类,过滤掉不希望给onStartup(..)
处理的类。至于为什么这样做,可参考我的 《精尽Spring MVC源码分析 - 寻找遗失的 web.xml》 这篇文章的说明
1.1.3 getTomcatWebServer 方法
getTomcatWebServer(Tomcat)
方法,创建一个 TomcatWebServer 容器对象,是对 tomcat
的封装,用于控制 Tomcat 服务器,如下:
可以看到,这里创建了一个 TomcatWebServer 对象,是对 tomcat
的封装,用于控制 Tomcat 服务器,但是,Tomcat 在哪启动的呢?
别急,在它的构造方法中还有一些初始化工作
TomcatWebServer
org.springframework.boot.web.embedded.tomcat.TomcatWebServer
,对 Tomcat 的封装,用于控制 Tomcat 服务器
当你创建该对象时,会调用 initialize()
方法进行一些初始化工作
1.1.4 initialize 方法
initialize()
方法,初始化 Tomcat 容器,并异步触发了 TomcatStarter#onStartup
方法
可以看到,这个方法的关键在于 this.tomcat.start()
这一步,启动 Tomcat 容器,那么会触发 javax.servlet.ServletContainerInitializer
的 onStartup(..)
方法
在上面的 1.1.2 configureContext 方法 和 1.1.3 getTomcatWebServer 方法 小节中也讲到过,有一个 TomcatStarter
对象,也就会触发它的 onStartup(..)
方法
那么 TomcatStarter
内部封装了一些 Spring Boot 的 ServletContextInitializer
对象,其中有一个实现类是ServletWebServerApplicationContext#selfInitialize(ServletContext)
匿名方法
TomcatStarter
org.springframework.boot.web.embedded.tomcat.TomcatStarter
,实现 javax.servlet.ServletContainerInitializer
接口,用于触发 Spring Boot 的 ServletContextInitializer 对象
在实现方法 onStartup(..)
中逻辑比较简单,就是调用 Spring Boot 自己的 ServletContextInitializer
实现类,例如 ServletWebServerApplicationContext#selfInitialize(ServletContext)
匿名方法
至于
TomcatStarter
为什么这做,是 Spring Boot 有意而为之,我们在使用 Spring Boot 时,开发阶段一般都是使用内嵌 Tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用java -jar
的方式运行;另一种是打成 war 包,交给外置容器去运行。前者就会导致容器搜索算法出现问题,因为这是 jar 包的运行策略,不会按照 Servlet 3.0 的策略去加载 ServletContainerInitializer!
所以 Spring Boot 提供了 ServletContextInitializer 去替代。
2. selfInitialize 方法
该方法在 org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext
中,如下:
思路是不是清晰明了了,前面一直没有提到 Servlet 和 Filter 是在哪添加至 Servlet 上下文中的,答案将在这里被揭晓
过程如下:
-
将当前 Spring 应用上下文设置到 ServletContext 上下文的属性中,同时将 ServletContext 上下文设置到 Spring 应用上下文中
-
向 Spring 应用上下文注册一个 ServletContextScope 对象(ServletContext 的封装)
-
向 Spring 应用上下文注册
contextParameters
和contextAttributes
属性(会先被封装成 Map) -
【重点】调用
getServletContextInitializerBeans()
方法,先从 Spring 应用上下文找到所有的ServletContextInitializer
对象,也就会找到各种 RegistrationBean,然后依次调用他们的onStartup
方法,向 ServletContext 上下文注册 Servlet、Filter 和 EventListener
重点在于上面的第 4
步,创建了一个 ServletContextInitializerBeans
对象,实现了 Collection 集合接口,所以可以遍历
它会找到所有的 RegistrationBean
(实现了 ServletContextInitializer 接口),然后调用他们的 onStartup(ServletContext)
方法,也就会往 ServletContext 中添加他们对应的 Servlet 或 Filter 或 EventListener 对象,这个方法比较简单,在后面讲到的 RegistrationBean 小节中会提到
继续往下看
ServletContextInitializerBeans
org.springframework.boot.web.servlet.ServletContextInitializerBeans
,对 ServletContextInitializer 实现类的封装,会找到所有的 ServletContextInitializer 实现类
过程如下:
- 设置类型为
ServletContextInitializer
- 找到 IoC 容器中所有
ServletContextInitializer
类型的 Bean,并将这些信息添加到seen
和initializers
集合中 - 从 IoC 容器中获取 Servlet or Filter or EventListener 类型的 Bean,适配成
RegistrationBean
对象,并添加到initializers
和seen
集合中 - 将
initializers
中的所有ServletContextInitializer
进行排序,并保存至sortedList
集合中 - DEBUG 模式下打印日志
比较简单,这里就不继续往下分析源码了,感兴趣可以看一看 ServletContextInitializerBeans.java
这里你要知道 RegistrationBean
实现了 ServletContextInitializer
接口,我们的 Spring Boot 应用如果要添加 Servlet 或者 Filter,可以注入一个 ServletRegistrationBean<T extends Servlet>
或者 FilterRegistrationBean<T extends Filter>
类型的 Bean
RegistrationBean
org.springframework.boot.web.servlet.RegistrationBean
,基于 Servlet 3.0+,往 ServletContext 注册 Servlet、Filter 和 EventListener
类图:
DynamicRegistrationBean
ServletRegistrationBean
DispatcherServletRegistrationBean
3. finishRefresh 方法
首先会调用父类方法,会发布 ContextRefreshedEvent 上下文刷新事件,然后调用自己的 startWebServer()
方法,启动上面 2. onRefresh 方法 创建的 WebServer
因为上面仅启动 Tomcat 容器,Servlet 添加到了 ServletContext 上下文中,这里启动 TomcatWebServer 容器对象,会对每一个 TomcatEmbeddedContext 中的 Servlet 进行加载并初始化,如下:
TomcatWebServer
org.springframework.boot.web.embedded.tomcat.TomcatWebServer
,对 Tomcat 的封装,用于控制 Tomcat 服务器
3.1 start 方法
start()
方法,启动 TomcatWebServer 服务器,初始化前面已添加的 Servlet 对象们
加锁启动,已启动则跳过
关键在于 performDeferredLoadOnStartup()
这个方法,对每一个 TomcatEmbeddedContext 中的 Servlet 进行加载并初始化,先找到容器中所有的 org.apache.catalina.Wrapper
,它是对 javax.servlet.Servlet
的封装,依次加载并初始化它们
好了,到这里 Spring Boot 内嵌的 Tomcat 容器差不多准备就绪了,继续往下追究就涉及到 Tomcat 底层的东西了,所以这里点到为止
总结
本文分析了 Spring Boot 内嵌 Tomcat 容器的实现,主要是 Spring Boot 的 Spring 应用上下文(ServletWebServerApplicationContext
)在 refresh()
刷新阶段进行了扩展,分别在 onRefresh()
和 finishRefresh()
两个地方,可以跳到前面的 回顾 小节中看看,分别做了以下事情:
- 创建一个 WebServer 服务对象,例如 TomcatWebServer 对象,对 Tomcat 的封装,用于控制 Tomcat 服务器
- 先创建一个
org.apache.catalina.startup.Tomcat
对象tomcat
,使用临时目录作为基础目录(tomcat.端口号
),退出时删除,同时会设置端口、编码、最小空闲线程和最大线程数 - 为
tomcat
创建一个TomcatEmbeddedContext
上下文对象,会添加一个TomcatStarter
(实现javax.servlet.ServletContainerInitializer
接口)到这个上下文对象中 - 将
tomcat
封装到 TomcatWebServer 对象中,实例化过程会启动tomcat
,启动后会触发javax.servlet.ServletContainerInitializer
实现类的回调,也就会触发TomcatStarter
的回调,在其内部会调用 Spring Boot 自己的ServletContextInitializer
初始器,例如ServletWebServerApplicationContext#selfInitialize(ServletContext)
匿名方法 - 在这个匿名方法中会找到所有的
RegistrationBean
,执行他们的onStartup
方法,将其关联的 Servlet、Filter 和 EventListener 添加至 Servlet 上下文中,包括 Spring MVC 的 DispatcherServlet 对象
- 先创建一个
- 启动上一步创建的 TomcatWebServer 对象,上面仅启动 Tomcat 容器,Servlet 添加到了 ServletContext 上下文中,这里会将这些 Servlet 进行加载并初始化
这样一来就完成 Spring Boot 内嵌的 Tomcat 就启动完成了,关于 Spring MVC 相关内容可查看 《精尽 Spring MVC 源码分析 - 文章导读》 这篇文章。
ServletContainerInitializer 也是 Servlet 3.0 新增的一个接口,容器在启动时使用 JAR 服务 API(JAR Service API) 来发现 ServletContainerInitializer 的实现类,并且容器将 WEB-INF/lib 目录下 JAR 包中的类都交给该类的
onStartup()
方法处理,我们通常需要在该实现类上使用 @HandlesTypes 注解来指定希望被处理的类,过滤掉不希望给onStartup()
处理的类。
你是否有一个疑问,Spring Boot 不也是支持打成 war
包,然后放入外部的 Tomcat 容器运行,这种方式的实现在哪里呢?我们在下一篇文章进行分析