SpringBoot 中 Servlet 加载流程的源码分析

1. Initializer 被替换为 TomcatStarter

当使用内嵌的 Tomcat 时,你会发现 Spring Boot 完全走了另一套初始化流程,完全没有使用前面提到的 SpringServletContainerInitializer ,实际上一开始我在各种 ServletContainerInitializer 的实现类中打了断点,最终定位到,根本没有运行到 SpringServletContainerInitializer 内部,而是进入了 org.springframework.boot.web.embedded.tomcat.TomcatStarter 这个类中

并且,仔细扫了一眼源码的包,并没有发现有 SPI 文件对应到 TomcatStarter。于是我猜想,内嵌 Tomcat 的加载可能不依赖于 Servlet3.0 规范和 SPI !它完全走了一套独立的逻辑。为了验证这一点,我翻阅了 Spring Github 中的 issue,得到了 Spring 作者肯定的答复:https://github.com/spring-projects/spring-boot/issues/321

This was actually an intentional design decision. The search algorithm used by the containers was problematic. It also causes problems when you want to develop an executable WAR as you often want a javax.servlet.ServletContainerInitializer for the WAR that is not executed when you run java -jar.

See the org.springframework.boot.context.embedded.ServletContextInitializer for an option that works with Spring Beans.

Spring Boot 这么做是有意而为之。Spring Boot 考虑到了如下的问题,我们在使用 Spring Boot 时,开发阶段一般都是使用内嵌 Tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用 java -jar 的方式运行;另一种是打成 war 包,交给外置容器去运行。

前者就会导致容器搜索算法出现问题,因为这是 jar 包的运行策略,不会按照 Servlet 3.0 的策略去加载 ServletContainerInitializer

最后作者还提供了一个替代选项:ServletContextInitializer,注意是 ServletContextInitializer !它和 ServletContainerInitializer 长得特别像,别搞混淆了!

  1. 前者 ServletContextInitializer 是 org.springframework.boot.web.servlet.ServletContextInitializer 
  2. 后者 ServletContainerInitializer 是 javax.servlet.ServletContainerInitializer 。前文还提到 RegistrationBean 实现了 ServletContextInitializer 接口。

2. TomcatStarter 中的 ServletContextInitializer 是关键

TomcatStarter 中 org.springframework.boot.context.embedded.ServletContextInitializer[] initializers 属性,是 Spring Boot 初始化 servlet,filter,listener 的关键。代码如下:

可以看出 TomcatStarter 的主要逻辑,它其实就是负责调用一系列 ServletContextInitializer 的 #onStartup(ServletContext servletContext) 方法,那么在 debug 中,ServletContextInitializer[] initializers 到底包含了哪些类呢?会不会有我们前面介绍的 RegistrationBean 呢?

RegistrationBean 并没有出现在 TomcatStarter 的 debug 信息中,initializers 只包含了三个类,其中只有第一个类看上去比较核心,注意第一个类不是 EmbeddedWebApplicationContext !而是这个类中的 $1 匿名类,为了搞清楚 Spring Boot 如何加载 filter、servlet、listener ,看来还得研究下 EmbeddedWebApplicationContext 的结构。

3. EmbeddedWebApplicationContext 中的 6 层迭代加载

ApplicationContext 大家应该是比较熟悉的,这是 spring 一个比较核心的类,一般我们可以从中获取到那些注册在容器中的托管 Bean,而这篇文章,主要分析的便是它在内嵌容器中的实现类:org.springframework.boot.context.embedded.EmbeddedWebApplicationContext ,重点分析它加载 filter servlet listener 这部分的代码。这里是整个代码中迭代层次最深的部分,做好心理准备起航,来看看 EmbeddedWebApplicationContext 是怎么获取到所有的 servlet、filter、listener 的!以下方法均出自于 EmbeddedWebApplicationContext 。

注:入口在SpringBoot启动流程里的refreshContext(context)

第一层:onRefresh()

#onRefresh() 方法,是 ApplicationContext 的生命周期方法,EmbeddedWebApplicationContext 的实现非常简单,只干了一件事:

调用 #createEmbeddedServletContainer() 方法,连接到了第二层。

第二层:createEmbeddedServletContainer()

看名字 Spring 是想创建一个内嵌的 Servlet 容器,ServletContainer 其实就是 servlet、filter、listener 的总称。

凡是带有 servlet,initializer 字样的方法,都是我们需要留意的。其中 #getSelfInitializer() 方法,便涉及到了我们最为关心的初始化流程,所以接着连接到了第三层。

第三层:getSelfInitializer()

还记得前面 TomcatStarter 的 debug 信息中,第一个 ServletContextInitializer 就是出现在 EmbeddedWebApplicationContext 中的一个匿名类,没错了,就是这里的 #getSelfInitializer() 方法创建的!

解释下这里的 #getSelfInitializer() 和 #selfInitialize(ServletContext servletContext) 方法,为什么要这么设计

这是典型的回调式方式,当匿名 ServletContextInitializer 类被 TomcatStarter 的 #onStartup() 方法调用,设计上是触发了 #selfInitialize(ServletContext servletContext) 方法的调用。

所以这下就清晰了,为什么 TomcatStarter 中没有出现 RegistrationBean ,其实是隐式触发了 EmbeddedWebApplicationContext 中的 #selfInitialize(ServletContext servletContext) 方法。这样,#selfInitialize(ServletContext servletContext) 方法中,调用 #getServletContextInitializerBeans() 方法,获得 ServletContextInitializer 数组就成了关键。所以接着连接到了第四层。

第四层:getServletContextInitializerBeans()

第五层:ServletContextInitializerBeans 的构造方法

第六层:addServletContextInitializerBeans(beanFactory)

调用 #getOrderedBeansOfType( beanFactory, ServletContextInitializer.class) 方法,便是去容器中寻找注册过得 ServletContextInitializer ,这时候就可以把之前那些 RegistrationBean 全部加载出来了。并且 RegistrationBean 还实现了 Ordered 接口,在这儿用于排序。

后续的 #addServletContextInitializerBean(ListableBeanFactory beanFactory) 方法。代码如下:

粗略看了一眼,各种 RegistrationBean 的处理。

EmbeddedWebApplicationContext加载流程总结

如果你对具体的代码流程不感兴趣,可以跳过上述的 6 层分析,直接看本节的结论。总结如下:

  1. EmbeddedWebApplicationContext 的 #onRefresh() 方法,触发配置了一个匿名的 ServletContextInitializer 。
  2. 这个匿名的 ServletContextInitializer 的 onStartup(ServletContext servletContext) 方法,会去容器中搜索到了所有的 RegistrationBean ,并按照顺序加载到 ServletContext 中。
  3. 这个匿名的 ServletContextInitializer 最终传递给 TomcatStarter,由 TomcatStarter 的 onStartup 方法去触发 ServletContextInitializer 的 #onStartup(ServletContext servletContext) 方法,最终完成装配!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章