該系列文章是筆者在學習 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
包直接啟動我們的應用,如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
如果不想使用內嵌的 Tomcat,我們可以這樣做:
<packaging>war</packaging>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
然後啟動類這樣寫:
// 方式三
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
// 可不寫
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(Application.class);
}
}
這樣你打成 war
包就可以放入外部的 Servlet 容器中執行了,具體實現檢視下一篇文章,本文分析的主要是 Spring Boot 內嵌 Tomcat 的實現。
回顧
在上一篇 《SpringApplication 啟動類的啟動過程》 文章分析 SpringApplication#run(String... args)
啟動 Spring 應用的過程中講到,在建立好 Spring 應用上下文後,會呼叫其 AbstractApplication#refresh()
方法重新整理上下文,該方法涉及到 Spring IoC 的所有內容,參考 《死磕Spring之IoC篇 - Spring 應用上下文 ApplicationContext》
/**
* 重新整理上下文,在哪會被呼叫?
* 在 **Spring MVC** 中,{@link org.springframework.web.context.ContextLoader#initWebApplicationContext} 方法初始化上下文時,會呼叫該方法
*/
@Override
public void refresh() throws BeansException, IllegalStateException {
// <1> 來個鎖,不然 refresh() 還沒結束,你又來個啟動或銷燬容器的操作,那不就亂套了嘛
synchronized (this.startupShutdownMonitor) {
// <2> 重新整理上下文環境的準備工作,記錄下容器的啟動時間、標記'已啟動'狀態、對上下文環境屬性進行校驗
prepareRefresh();
// <3> 建立並初始化一個 BeanFactory 物件 `beanFactory`,會載入出對應的 BeanDefinition 元資訊們
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// <4> 為 `beanFactory` 進行一些準備工作,例如新增幾個 BeanPostProcessor,手動註冊幾個特殊的 Bean
prepareBeanFactory(beanFactory);
try {
// <5> 對 `beanFactory` 在進行一些後期的加工,交由子類進行擴充套件
postProcessBeanFactory(beanFactory);
// <6> 執行 BeanFactoryPostProcessor 處理器,包含 BeanDefinitionRegistryPostProcessor 處理器
invokeBeanFactoryPostProcessors(beanFactory);
// <7> 對 BeanPostProcessor 處理器進行初始化,並新增至 BeanFactory 中
registerBeanPostProcessors(beanFactory);
// <8> 設定上下文的 MessageSource 物件
initMessageSource();
// <9> 設定上下文的 ApplicationEventMulticaster 物件,上下文事件廣播器
initApplicationEventMulticaster();
// <10> 重新整理上下文時再進行一些初始化工作,交由子類進行擴充套件
onRefresh();
// <11> 將所有 ApplicationListener 監聽器新增至 `applicationEventMulticaster` 事件廣播器,如果已有事件則進行廣播
registerListeners();
// <12> 設定 ConversionService 型別轉換器,**初始化**所有還未初始化的 Bean(不是抽象、單例模式、不是懶載入方式)
finishBeanFactoryInitialization(beanFactory);
// <13> 重新整理上下文的最後一步工作,會發布 ContextRefreshedEvent 上下文完成重新整理事件
finishRefresh();
}
// <14> 如果上面過程出現 BeansException 異常
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// <14.1> “銷燬” 已註冊的單例 Bean
destroyBeans();
// <14.2> 設定上下文的 `active` 狀態為 `false`
cancelRefresh(ex);
// <14.3> 丟擲異常
throw ex;
}
// <15> `finally` 程式碼塊
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
// 清除相關快取,例如通過反射機制快取的 Method 和 Field 物件,快取的註解後設資料,快取的泛型型別物件,快取的類載入器
resetCommonCaches();
}
}
}
在該方法的第 10
步可以看到會呼叫 onRefresh()
方法再進行一些初始化工作,這個方法交由子類進行擴充套件,那麼在 Spring Boot 中的 ServletWebServerApplicationContext
重寫了該方法,會建立一個 Servlet 容器(預設為 Tomcat),也就是當前 Spring Boot 應用所執行的 Web 環境。
第 13
步會呼叫 onRefresh()
方法,ServletWebServerApplicationContext
重寫了該方法,啟動 WebServer,對 Servlet 進行載入並初始化
類圖
由於整個 ApplicationContext 體系比較龐大,下面列出了部分類
DispatcherServlet 自動配置類
在開始之前,我們先來看看 org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
這個自動配置類,部分如下:
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) // 最高優先順序的自動配置
@Configuration(proxyBeanMethods = false) // 作為一個配置類,不進行 CGLIB 提升
@ConditionalOnWebApplication(type = Type.SERVLET) // Servlet 應用的型別才注入當前 Bean
@ConditionalOnClass(DispatcherServlet.class) // 存在 DispatcherServlet 這個類才注入當前 Bean
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class) // 在 ServletWebServerFactoryAutoConfiguration 後面進行自動配置
public class DispatcherServletAutoConfiguration {
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";
// 作為一個配置類,不進行 CGLIB 提升
@Configuration(proxyBeanMethods = false)
// 滿足條件則注入當前 DispatcherServlet(需要 Spring 上下文中不存在)
@Conditional(DefaultDispatcherServletCondition.class)
// 存在 ServletRegistration 這個類才注入當前 Bean
@ConditionalOnClass(ServletRegistration.class)
// 注入兩個配置物件
@EnableConfigurationProperties({ HttpProperties.class, WebMvcProperties.class })
protected static class DispatcherServletConfiguration {
// 定義一個 DispatcherServlet 的 Bean
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails());
return dispatcherServlet;
}
@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
}
// 作為一個配置類,不進行 CGLIB 提升
@Configuration(proxyBeanMethods = false)
// 滿足條件則注入當前 DispatcherServletRegistrationBean
@Conditional(DispatcherServletRegistrationCondition.class)
// 存在 ServletRegistration 這個類才注入當前 Bean
@ConditionalOnClass(ServletRegistration.class)
// 注入一個配置物件
@EnableConfigurationProperties(WebMvcProperties.class)
// 先注入上面的 DispatcherServletConfiguration 物件
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {
// 為 DispatcherServlet 定義一個 RegistrationBean 物件,目的是往 ServletContext 上下文中新增 DispatcherServlet
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
// 需要存在名稱為 `dispatcherServlet` 型別為 DispatcherServlet 的 Bean
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
// 如果有 MultipartConfigElement 配置則進行設定
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
}
}
這個 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 方法
// ServletWebServerApplicationContext.java
@Override
protected void onRefresh() {
// 呼叫父類方法,初始化 ThemeSource 物件
super.onRefresh();
try {
/**
* 建立一個 WebServer 服務(預設 Tomcat),並初始化 ServletContext 上下文
* 會先建立一個 {@link Tomcat} 容器並啟動,同時會註冊各種 Servlet
* 例如 藉助 {@link org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration}
* 註冊 {@link DispatcherServlet} 物件到 ServletContext 上下文,這樣就可以通過 Spring MVC 的核心元件來實現一個 Web 應用
*/
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
首先會呼叫父類方法,初始化 ThemeSource 物件,然後呼叫自己的 createWebServer()
方法,建立一個 WebServer 服務(預設 Tomcat),並初始化 ServletContext 上下文,如下:
// ServletWebServerApplicationContext.java
private void createWebServer() {
// <1> 獲取當前 `WebServer` 容器物件,首次進來為空
WebServer webServer = this.webServer;
// <2> 獲取 `ServletContext` 上下文物件
ServletContext servletContext = getServletContext();
// <3> 如果 WebServer 和 ServletContext 都為空,則需要建立一個
// 使用 Spring Boot 內嵌 Tomcat 容器則會進入該分支
if (webServer == null && servletContext == null) {
// <3.1> 獲取 Servlet 容器工廠物件(預設為 Tomcat)`factory`
ServletWebServerFactory factory = getWebServerFactory();
/**
* <3.2> 先建立一個 {@link ServletContextInitializer} Servlet 上下文初始器,實現也就是當前類的 {@link this#selfInitialize(ServletContext)} 方法
* 至於為什麼不用 Servlet 3.0 新增的 {@link javax.servlet.ServletContainerInitializer} 這個類,我在
* [精盡Spring MVC原始碼分析 - 尋找遺失的 web.xml](https://www.cnblogs.com/lifullmoon/p/14122704.html)有提到過
*
* <3.3> 從 `factory` 工廠中建立一個 WebServer 容器物件
* 例如建立一個 {@link TomcatWebServer} 容器物件,並初始化 `ServletContext` 上下文,建立 {@link Tomcat} 容器並啟動
* 啟動過程非同步觸發了 {@link org.springframework.boot.web.embedded.tomcat.TomcatStarter#onStartup} 方法
* 也就會呼叫這個傳入的 {@link ServletContextInitializer} 的 {@link #selfInitialize(ServletContext)} 方法
*/
this.webServer = factory.getWebServer(getSelfInitializer());
}
// <4> 否則,如果 ServletContext 不為空,說明使用了外部的 Servlet 容器(例如 Tomcat)
else if (servletContext != null) {
try {
/** 那麼這裡主動呼叫 {@link this#selfInitialize(ServletContext)} 方法來註冊各種 Servlet、Filter */
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
// <5> 將 ServletContext 的一些初始化引數關聯到當前 Spring 應用的 Environment 環境中
initPropertySources();
}
過程如下:
-
獲取當前
WebServer
容器物件,首次進來為空 -
獲取
ServletContext
上下文物件@Override @Nullable public ServletContext getServletContext() { return this.servletContext; }
-
如果
WebServer
和ServletContext
都為空,則需要建立一個,此時使用 Spring Boot 內嵌 Tomcat 容器則會進入該分支-
獲取 Servlet 容器工廠物件(預設為 Tomcat)
factory
,如下:protected ServletWebServerFactory getWebServerFactory() { // Use bean names so that we don't consider the hierarchy // 獲取當前 BeanFactory 中型別為 ServletWebServerFactory 的 Bean 的名稱,不考慮層次性 // 必須存在一個,否則丟擲異常 // 所以想要切換 Servlet 容器得引入對應的 Starter 模組並排除 `spring-boot-starter-web` 中預設的 `tomcat` Starter 模組 String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class); if (beanNames.length == 0) { throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing " + "ServletWebServerFactory bean."); } if (beanNames.length > 1) { throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple " + "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames)); } // 獲取這個 ServletWebServerFactory 物件 return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class); }
在
spring-boot-autoconfigure
中有一個ServletWebServerFactoryConfiguration
配置類會註冊一個TomcatServletWebServerFactory
物件加上
TomcatServletWebServerFactoryCustomizer
自動配置類,可以將server.*
相關的配置設定到該物件中,這一步不深入分析,感興趣可以去看一看 -
先建立一個
ServletContextInitializer
Servlet 上下文初始器,實現也就是當前類的this#selfInitialize(ServletContext)
方法,如下:private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() { return this::selfInitialize; }
這個
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
容器並啟動
// TomcatServletWebServerFactory.java
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
// <1> 禁用 MBean 註冊中心
Registry.disableRegistry();
}
// <2> 建立一個 Tomcat 物件 `tomcat`
Tomcat tomcat = new Tomcat();
// <3> 建立一個臨時目錄(退出時刪除)
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
// <4> 將這個目錄作為 Tomcat 的目錄
tomcat.setBaseDir(baseDir.getAbsolutePath());
// <5> 建立一個 NIO 協議的 Connector 聯結器物件,並新增到第 `2` 步建立的 `tomcat` 中
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
// <6> 對 Connector 進行配置,設定 `server.port` 埠、編碼
// `server.tomcat.min-spare-threads` 最小空閒執行緒和 `server.tomcat.accept-count` 最大執行緒數
customizeConnector(connector);
tomcat.setConnector(connector);
// <7> 禁止自動部署
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
// <8> 同時支援多個 Connector 聯結器(預設沒有)
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
// <9> 建立一個 TomcatEmbeddedContext 上下文物件,並進行初始化工作,配置 TomcatStarter 作為啟動器
// 會將這個上下文物件設定到當前 `tomcat` 中去
prepareContext(tomcat.getHost(), initializers);
/**
* <10> 建立一個 TomcatWebServer 容器物件,是對 `tomcat` 的封裝,用於控制 Tomcat 伺服器
* 同時初始化 Tomcat 容器並啟動,這裡會非同步觸發了 {@link TomcatStarter#onStartup} 方法
* 也就會呼叫入參中幾個 {@link ServletContextInitializer#onStartup} 方法
* 例如 {@link org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#selfInitialize}
*/
return getTomcatWebServer(tomcat);
}
過程如下:
-
禁用 MBean 註冊中心
-
建立一個 Tomcat 物件
tomcat
-
建立一個臨時目錄(退出時刪除)
protected final File createTempDir(String prefix) { try { // 建立一個臨時目錄,臨時目錄下的 `tomcat.埠號` 目錄 File tempDir = Files.createTempDirectory(prefix + "." + getPort() + ".").toFile(); // 應用退出時會刪除 tempDir.deleteOnExit(); return tempDir; } catch (IOException ex) { throw new WebServerException( "Unable to create tempDir. java.io.tmpdir is set to " + System.getProperty("java.io.tmpdir"), ex); } }
-
將這個臨時目錄作為 Tomcat 的目錄
-
建立一個 NIO 協議的 Connector 聯結器物件,並新增到第
2
步建立的tomcat
中 -
對 Connector 進行配置,設定
server.port
埠、編碼、server.tomcat.min-spare-threads
最小空閒執行緒和server.tomcat.accept-count
最大執行緒數。這些配置就是我們自己配置的,在前面 1. onRefresh 方法 的第3
步有提到protected void customizeConnector(Connector connector) { // 獲取埠(也就是 `server.port`),並設定 int port = Math.max(getPort(), 0); connector.setPort(port); if (StringUtils.hasText(this.getServerHeader())) { connector.setAttribute("server", this.getServerHeader()); } if (connector.getProtocolHandler() instanceof AbstractProtocol) { customizeProtocol((AbstractProtocol<?>) connector.getProtocolHandler()); } invokeProtocolHandlerCustomizers(connector.getProtocolHandler()); // 設定編碼 if (getUriEncoding() != null) { connector.setURIEncoding(getUriEncoding().name()); } // Don't bind to the socket prematurely if ApplicationContext is slow to start connector.setProperty("bindOnInit", "false"); if (getSsl() != null && getSsl().isEnabled()) { customizeSsl(connector); } TomcatConnectorCustomizer compression = new CompressionConnectorCustomizer(getCompression()); compression.customize(connector); for (TomcatConnectorCustomizer customizer : this.tomcatConnectorCustomizers) { // 藉助 TomcatWebServerFactoryCustomizer 對 Connector 進行配置 // 例如設定 `server.tomcat.min-spare-threads` 最小空閒執行緒和 `server.tomcat.accept-count` 最大執行緒數 customizer.customize(connector); } }
-
禁止自動部署
-
同時支援多個 Connector 聯結器(預設沒有)
-
呼叫
prepareContext(..)
方法,建立一個TomcatEmbeddedContext
上下文物件,並進行初始化工作,配置TomcatStarter
作為啟動器,會將這個上下文物件設定到當前tomcat
中去 -
呼叫
getTomcatWebServer(Tomcat)
方法,建立一個TomcatWebServer
容器物件,是對tomcat
的封裝,用於控制 Tomcat 伺服器
整個 Tomcat 的初始化過程沒有特別的複雜,主要是因為這裡沒有深入分析,我們知道大致的流程即可,這裡我們重點關注第 9
和 10
步,接下來依次分析
1.1.1 prepareContext 方法
prepareContext(Host, ServletContextInitializer[])
方法,建立一個 TomcatEmbeddedContext 上下文物件,並進行初始化工作,配置 TomcatStarter 作為啟動器,會將這個上下文物件設定到 Tomcat 的 Host 中去,如下:
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
File documentRoot = getValidDocumentRoot();
// <1> 建立一個 TomcatEmbeddedContext 上下文物件 `context`
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
if (documentRoot != null) {
context.setResources(new LoaderHidingResourceRoot(context));
}
context.setName(getContextPath());
context.setDisplayName(getDisplayName());
// <2> 設定 `context-path`
context.setPath(getContextPath());
File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
// <3> 設定 Tomcat 根目錄
context.setDocBase(docBase.getAbsolutePath());
context.addLifecycleListener(new FixContextListener());
context.setParentClassLoader((this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
: ClassUtils.getDefaultClassLoader());
resetDefaultLocaleMapping(context);
addLocaleMappings(context);
try {
context.setCreateUploadTargets(true);
} catch (NoSuchMethodError ex) {
// Tomcat is < 8.5.39. Continue.
}
configureTldPatterns(context);
WebappLoader loader = new WebappLoader();
loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
loader.setDelegate(true);
context.setLoader(loader);
if (isRegisterDefaultServlet()) {
// <4> 註冊預設的 Servlet 為 `org.apache.catalina.servlets.DefaultServlet`
addDefaultServlet(context);
}
if (shouldRegisterJspServlet()) {
addJspServlet(context);
addJasperInitializer(context);
}
context.addLifecycleListener(new StaticResourceConfigurer(context));
ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
// <5> 將這個 `context` 上下文物件新增到 `tomcat` 中去
host.addChild(context);
// <6> 對 TomcatEmbeddedContext 進行配置,例如配置 TomcatStarter 啟動器,它是對 ServletContext 上下文物件的初始器 `initializersToUse` 的封裝
configureContext(context, initializersToUse);
postProcessContext(context);
}
整個過程我們挑主要的流程來看:
-
建立一個 TomcatEmbeddedContext 上下文物件
context
,接下來進行一系列的配置 -
設定
context-path
-
設定 Tomcat 根目錄
-
註冊預設的 Servlet 為
org.apache.catalina.servlets.DefaultServlet
private void addDefaultServlet(Context context) { Wrapper defaultServlet = context.createWrapper(); defaultServlet.setName("default"); defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet"); defaultServlet.addInitParameter("debug", "0"); defaultServlet.addInitParameter("listings", "false"); defaultServlet.setLoadOnStartup(1); // Otherwise the default location of a Spring DispatcherServlet cannot be set defaultServlet.setOverridable(true); context.addChild(defaultServlet); context.addServletMappingDecoded("/", "default"); }
-
將這個
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
啟動器,如下:
protected void configureContext(Context context, ServletContextInitializer[] initializers) {
// <1> 建立一個 TomcatStarter 啟動器,此時把 ServletContextInitializer 陣列傳入進去了
// 並設定到 TomcatEmbeddedContext 上下文中
TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
context.addServletContainerInitializer(starter, NO_CLASSES);
for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
context.addLifecycleListener(lifecycleListener);
}
for (Valve valve : this.contextValves) {
context.getPipeline().addValve(valve);
}
// <2> 設定錯誤頁面
for (ErrorPage errorPage : getErrorPages()) {
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
tomcatErrorPage.setLocation(errorPage.getPath());
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
context.addErrorPage(tomcatErrorPage);
}
for (MimeMappings.Mapping mapping : getMimeMappings()) {
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
// <3> 配置 TomcatEmbeddedContext 上下文的 Session 會話,例如超時會話時間
configureSession(context);
new DisableReferenceClearingContextCustomizer().customize(context);
// <4> 對 TomcatEmbeddedContext 上下文進行自定義處理,例如新增 WsContextListener 監聽器
for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
customizer.customize(context);
}
}
配置過程如下:
-
建立一個
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 伺服器,如下:
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
/**
* 建立一個 TomcatWebServer 容器物件
* 同時初始化 Tomcat 容器並啟動,這裡會非同步觸發了 {@link TomcatStarter#onStartup} 方法
*/
return new TomcatWebServer(tomcat, getPort() >= 0);
}
可以看到,這裡建立了一個 TomcatWebServer 物件,是對 tomcat
的封裝,用於控制 Tomcat 伺服器,但是,Tomcat 在哪啟動的呢?
別急,在它的構造方法中還有一些初始化工作
TomcatWebServer
org.springframework.boot.web.embedded.tomcat.TomcatWebServer
,對 Tomcat 的封裝,用於控制 Tomcat 伺服器
public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
/** 初始化 Tomcat 容器,並非同步觸發了 {@link TomcatStarter#onStartup} 方法 */
initialize();
}
當你建立該物件時,會呼叫 initialize()
方法進行一些初始化工作
1.1.4 initialize 方法
initialize()
方法,初始化 Tomcat 容器,並非同步觸發了 TomcatStarter#onStartup
方法
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
addInstanceIdToEngineName();
// 找到之前建立的 TomcatEmbeddedContext 上下文
Context context = findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
// Remove service connectors so that protocol binding doesn't
// happen when the service is started.
removeServiceConnectors();
}
});
// Start the server to trigger initialization listeners
/** 啟動 Tomcat 容器,這裡會觸發初始化監聽器,例如非同步觸發了 {@link TomcatStarter#onStartup} 方法 */
this.tomcat.start();
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
} catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
startDaemonAwaitThread();
} catch (Exception ex) {
stopSilently();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}
可以看到,這個方法的關鍵在於 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 物件
class TomcatStarter implements ServletContainerInitializer {
private final ServletContextInitializer[] initializers;
private volatile Exception startUpException;
TomcatStarter(ServletContextInitializer[] initializers) {
this.initializers = initializers;
}
@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
try {
/**
* 依次執行所有的 Servlet 上下文啟動器
* {@link ServletWebServerApplicationContext}
*/
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}
} catch (Exception ex) {
this.startUpException = ex;
}
}
Exception getStartUpException() {
return this.startUpException;
}
}
在實現方法 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
中,如下:
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}
思路是不是清晰明瞭了,前面一直沒有提到 Servlet 和 Filter 是在哪新增至 Servlet 上下文中的,答案將在這裡被揭曉
private void selfInitialize(ServletContext servletContext) throws ServletException {
// <1> 將當前 Spring 應用上下文設定到 ServletContext 上下文的屬性中
// 同時將 ServletContext 上下文設定到 Spring 應用上下文中
prepareWebApplicationContext(servletContext);
// <2> 向 Spring 應用上下文註冊一個 ServletContextScope 物件(ServletContext 的封裝)
registerApplicationScope(servletContext);
// <3> 向 Spring 應用上下文註冊 `contextParameters` 和 `contextAttributes` 屬性(會先被封裝成 Map)
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
/**
* <4> 【重點】先從 Spring 應用上下文找到所有的 {@link ServletContextInitializer}
* 也就會找到各種 {@link RegistrationBean},然後依次呼叫他們的 `onStartup` 方法,向 ServletContext 上下文註冊 Servlet、Filter 和 EventListener
* 例如 {@link DispatcherServletAutoConfiguration} 中的 {@link DispatcherServletRegistrationBean} 就會註冊 {@link DispatcherServlet} 物件
* 這也就是我們熟知的 Spring MVC 的核心元件,關於它可參考我的 [精盡Spring MVC原始碼分析 - 文章導讀](https://www.cnblogs.com/lifullmoon/p/14123963.html) 文章
* 所以這裡執行完了,也就啟動了 Tomcat,同時註冊了所有的 Servlet,那麼 Web 應用準備就緒了
*/
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
過程如下:
-
將當前 Spring 應用上下文設定到 ServletContext 上下文的屬性中,同時將 ServletContext 上下文設定到 Spring 應用上下文中
-
向 Spring 應用上下文註冊一個 ServletContextScope 物件(ServletContext 的封裝)
-
向 Spring 應用上下文註冊
contextParameters
和contextAttributes
屬性(會先被封裝成 Map) -
【重點】呼叫
getServletContextInitializerBeans()
方法,先從 Spring 應用上下文找到所有的ServletContextInitializer
物件,也就會找到各種 RegistrationBean,然後依次呼叫他們的onStartup
方法,向 ServletContext 上下文註冊 Servlet、Filter 和 EventListenerprotected Collection<ServletContextInitializer> getServletContextInitializerBeans() { return new ServletContextInitializerBeans(getBeanFactory()); }
重點在於上面的第 4
步,建立了一個 ServletContextInitializerBeans
物件,實現了 Collection 集合介面,所以可以遍歷
它會找到所有的 RegistrationBean
(實現了 ServletContextInitializer 介面),然後呼叫他們的 onStartup(ServletContext)
方法,也就會往 ServletContext 中新增他們對應的 Servlet 或 Filter 或 EventListener 物件,這個方法比較簡單,在後面講到的 RegistrationBean 小節中會提到
繼續往下看
ServletContextInitializerBeans
org.springframework.boot.web.servlet.ServletContextInitializerBeans
,對 ServletContextInitializer 實現類的封裝,會找到所有的 ServletContextInitializer 實現類
public class ServletContextInitializerBeans extends AbstractCollection<ServletContextInitializer> {
private static final String DISPATCHER_SERVLET_NAME = "dispatcherServlet";
/**
* Seen bean instances or bean names.
* 所有的 Servlet or Filter or EventListener or ServletContextInitializer 物件
* 也可能是該物件對應的 `beanName`
*/
private final Set<Object> seen = new HashSet<>();
/**
* 儲存不同型別的 ServletContextInitializer 物件
* key:Servlet or Filter or EventListener or ServletContextInitializer
* value:ServletContextInitializer 實現類
*/
private final MultiValueMap<Class<?>, ServletContextInitializer> initializers;
/**
* 指定 ServletContextInitializer 的型別,預設就是它
*/
private final List<Class<? extends ServletContextInitializer>> initializerTypes;
/**
* 排序後的所有 `initializers` 中的 ServletContextInitializer 實現類(不可被修改)
*/
private List<ServletContextInitializer> sortedList;
@SafeVarargs
public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
Class<? extends ServletContextInitializer>... initializerTypes) {
this.initializers = new LinkedMultiValueMap<>();
// <1> 設定型別為 `ServletContextInitializer`
this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
: Collections.singletonList(ServletContextInitializer.class);
// <2> 找到 IoC 容器中所有 `ServletContextInitializer` 型別的 Bean
// 並將這些資訊新增到 `seen` 和 `initializers` 集合中
addServletContextInitializerBeans(beanFactory);
// <3> 從 IoC 容器中獲取 Servlet or Filter or EventListener 型別的 Bean
// 適配成 RegistrationBean 物件,並新增到 `initializers` 和 `seen` 集合中
addAdaptableBeans(beanFactory);
// <4> 將 `initializers` 中的所有 ServletContextInitializer 進行排序,並儲存至 `sortedList` 集合中
List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
.flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
.collect(Collectors.toList());
this.sortedList = Collections.unmodifiableList(sortedInitializers);
// <5> DEBUG 模式下列印日誌
logMappings(this.initializers);
}
}
過程如下:
- 設定型別為
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
public abstract class RegistrationBean implements ServletContextInitializer, Ordered {
@Override
public final void onStartup(ServletContext servletContext) throws ServletException {
// 抽象方法,交由子類實現
String description = getDescription();
// 抽象方法,交由子類實現
register(description, servletContext);
}
}
類圖:
DynamicRegistrationBean
public abstract class DynamicRegistrationBean<D extends Registration.Dynamic> extends RegistrationBean {
@Override
protected final void register(String description, ServletContext servletContext) {
// 抽象方法,交由子類實現
D registration = addRegistration(description, servletContext);
// 設定初始化引數,也就是設定 `Map<String, String> initParameters` 引數
configure(registration);
}
protected void configure(D registration) {
registration.setAsyncSupported(this.asyncSupported);
if (!this.initParameters.isEmpty()) {
registration.setInitParameters(this.initParameters);
}
}
}
ServletRegistrationBean
public class ServletRegistrationBean<T extends Servlet> extends DynamicRegistrationBean<ServletRegistration.Dynamic> {
@Override
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
// 獲取 Servlet 的名稱
String name = getServletName();
// 將該 Servlet 新增至 ServletContext 上下文中
return servletContext.addServlet(name, this.servlet);
}
@Override
protected void configure(ServletRegistration.Dynamic registration) {
super.configure(registration);
// 設定需要攔截的 URL,預設 `/*`
String[] urlMapping = StringUtils.toStringArray(this.urlMappings);
if (urlMapping.length == 0 && this.alwaysMapUrl) {
urlMapping = DEFAULT_MAPPINGS;
}
if (!ObjectUtils.isEmpty(urlMapping)) {
registration.addMapping(urlMapping);
}
// 設定需要載入的優先順序
registration.setLoadOnStartup(this.loadOnStartup);
if (this.multipartConfig != null) {
registration.setMultipartConfig(this.multipartConfig);
}
}
}
DispatcherServletRegistrationBean
public class DispatcherServletRegistrationBean extends ServletRegistrationBean<DispatcherServlet>
implements DispatcherServletPath {
private final String path;
/**
* Create a new {@link DispatcherServletRegistrationBean} instance for the given
* servlet and path.
* @param servlet the dispatcher servlet
* @param path the dispatcher servlet path
*/
public DispatcherServletRegistrationBean(DispatcherServlet servlet, String path) {
super(servlet);
Assert.notNull(path, "Path must not be null");
this.path = path;
super.addUrlMappings(getServletUrlMapping());
}
@Override
public String getPath() {
return this.path;
}
@Override
public void setUrlMappings(Collection<String> urlMappings) {
throw new UnsupportedOperationException("URL Mapping cannot be changed on a DispatcherServlet registration");
}
@Override
public void addUrlMappings(String... urlMappings) {
throw new UnsupportedOperationException("URL Mapping cannot be changed on a DispatcherServlet registration");
}
}
3. finishRefresh 方法
// // ServletWebServerApplicationContext.java
@Override
protected void finishRefresh() {
// 呼叫父類方法,會發布 ContextRefreshedEvent 上下文重新整理事件
super.finishRefresh();
/**
* 啟動上面 {@link #onRefresh }建立的 WebServer,上面僅啟動 {@link Tomcat} 容器,Servlet 新增到了 ServletContext 上下文中
* 這裡啟動 {@link TomcatWebServer} 容器物件,對每一個 TomcatEmbeddedContext 中的 Servlet 進行載入並初始化
*/
WebServer webServer = startWebServer();
if (webServer != null) {
publishEvent(new ServletWebServerInitializedEvent(webServer, this));
}
}
首先會呼叫父類方法,會發布 ContextRefreshedEvent 上下文重新整理事件,然後呼叫自己的 startWebServer()
方法,啟動上面 2. onRefresh 方法 建立的 WebServer
因為上面僅啟動 Tomcat 容器,Servlet 新增到了 ServletContext 上下文中,這裡啟動 TomcatWebServer 容器物件,會對每一個 TomcatEmbeddedContext 中的 Servlet 進行載入並初始化,如下:
private WebServer startWebServer() {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.start();
}
return webServer;
}
TomcatWebServer
org.springframework.boot.web.embedded.tomcat.TomcatWebServer
,對 Tomcat 的封裝,用於控制 Tomcat 伺服器
3.1 start 方法
start()
方法,啟動 TomcatWebServer 伺服器,初始化前面已新增的 Servlet 物件們
@Override
public void start() throws WebServerException {
// 加鎖啟動
synchronized (this.monitor) {
// 已啟動則跳過
if (this.started) {
return;
}
try {
addPreviouslyRemovedConnectors();
Connector connector = this.tomcat.getConnector();
if (connector != null && this.autoStart) {
/**
* 對每一個 TomcatEmbeddedContext 中的 Servlet 進行載入並初始化,先找到容器中所有的 {@link org.apache.catalina.Wrapper}
* 它是對 {@link javax.servlet.Servlet} 的封裝,依次載入並初始化它們
*/
performDeferredLoadOnStartup();
}
checkThatConnectorsHaveStarted();
this.started = true;
logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '"
+ getContextPath() + "'");
} catch (ConnectorStartFailedException ex) {
stopSilently();
throw ex;
} catch (Exception ex) {
PortInUseException.throwIfPortBindingException(ex, () -> this.tomcat.getConnector().getPort());
throw new WebServerException("Unable to start embedded Tomcat server", ex);
} finally {
Context context = findContext();
ContextBindings.unbindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
}
}
}
加鎖啟動,已啟動則跳過
關鍵在於 performDeferredLoadOnStartup()
這個方法,對每一個 TomcatEmbeddedContext 中的 Servlet 進行載入並初始化,先找到容器中所有的 org.apache.catalina.Wrapper
,它是對 javax.servlet.Servlet
的封裝,依次載入並初始化它們
private void performDeferredLoadOnStartup() {
try {
for (Container child : this.tomcat.getHost().findChildren()) {
if (child instanceof TomcatEmbeddedContext) {
/**
* 找到容器中所有的 {@link org.apache.catalina.Wrapper},它是對 {@link javax.servlet.Servlet} 的封裝
* 那麼這裡將依次載入並初始化它們
*/
((TomcatEmbeddedContext) child).deferredLoadOnStartup();
}
}
} catch (Exception ex) {
if (ex instanceof WebServerException) {
throw (WebServerException) ex;
}
throw new WebServerException("Unable to start embedded Tomcat connectors", ex);
}
}
好了,到這裡 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 容器執行,這種方式的實現在哪裡呢?我們在下一篇文章進行分析