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) 方法,最終完成裝配!

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