精盡Spring Boot原始碼分析 - 內嵌Tomcat容器的實現

月圓吖發表於2021-07-01

該系列文章是筆者在學習 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 體系比較龐大,下面列出了部分類

精盡Spring Boot原始碼分析 - 內嵌Tomcat容器的實現

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 mvctomcat 相關依賴,關於 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();
}

過程如下:

  1. 獲取當前 WebServer 容器物件,首次進來為

  2. 獲取 ServletContext 上下文物件

    @Override
    @Nullable
    public ServletContext getServletContext() {
        return this.servletContext;
    }
    
  3. 如果 WebServerServletContext 都為空,則需要建立一個,此時使用 Spring Boot 內嵌 Tomcat 容器則會進入該分支

    1. 獲取 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.* 相關的配置設定到該物件中,這一步不深入分析,感興趣可以去看一看

    2. 先建立一個 ServletContextInitializer Servlet 上下文初始器,實現也就是當前類的 this#selfInitialize(ServletContext) 方法,如下:

      private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
          return this::selfInitialize;
      }
      

      這個 ServletContextInitializer 在後面會被呼叫,請記住這個方法

    3. factory 工廠中建立一個 WebServer 容器物件,例如建立一個 TomcatWebServer 容器物件,並初始化 ServletContext 上下文,該過程會建立一個 Tomcat 容器並啟動,啟動過程非同步觸發了 TomcatStarter#onStartup 方法,也就會呼叫第 2 步的 ServletContextInitializer#selfInitialize(ServletContext) 方法

  4. 否則,如果 ServletContext 不為空,說明使用了外部的 Servlet 容器(例如 Tomcat)

    1. 那麼這裡主動呼叫 this#selfInitialize(ServletContext) 方法來註冊各種 Servlet、Filter
  5. 將 ServletContext 的一些初始化引數關聯到當前 Spring 應用的 Environment 環境中

整個過程有點繞,如果獲取到的 WebServerServletContext 都為空,說明需要使用內嵌的 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);
}

過程如下:

  1. 禁用 MBean 註冊中心

  2. 建立一個 Tomcat 物件 tomcat

  3. 建立一個臨時目錄(退出時刪除)

    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);
        }
    }
    
  4. 將這個臨時目錄作為 Tomcat 的目錄

  5. 建立一個 NIO 協議的 Connector 聯結器物件,並新增到第 2 步建立的 tomcat

  6. 對 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);
        }
    }
    
  7. 禁止自動部署

  8. 同時支援多個 Connector 聯結器(預設沒有)

  9. 呼叫 prepareContext(..) 方法,建立一個 TomcatEmbeddedContext 上下文物件,並進行初始化工作,配置 TomcatStarter 作為啟動器,會將這個上下文物件設定到當前 tomcat 中去

  10. 呼叫 getTomcatWebServer(Tomcat) 方法,建立一個 TomcatWebServer 容器物件,是對 tomcat 的封裝,用於控制 Tomcat 伺服器

整個 Tomcat 的初始化過程沒有特別的複雜,主要是因為這裡沒有深入分析,我們知道大致的流程即可,這裡我們重點關注第 910 步,接下來依次分析

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);
}

整個過程我們挑主要的流程來看:

  1. 建立一個 TomcatEmbeddedContext 上下文物件 context,接下來進行一系列的配置

  2. 設定 context-path

  3. 設定 Tomcat 根目錄

  4. 註冊預設的 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");
    }
    
  5. 將這個 context 上下文物件新增到 tomcat 中去

  6. 呼叫 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);
    }
}

配置過程如下:

  1. 建立一個 TomcatStarter 啟動器,此時把 ServletContextInitializer 陣列傳入進去了,並設定到 context 上下文中

  2. 設定錯誤頁面

  3. 配置 context 上下文的 Session 會話,例如超時會話時間

  4. 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.ServletContainerInitializeronStartup(..) 方法

在上面的 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);
    }
}

過程如下:

  1. 將當前 Spring 應用上下文設定到 ServletContext 上下文的屬性中,同時將 ServletContext 上下文設定到 Spring 應用上下文中

  2. 向 Spring 應用上下文註冊一個 ServletContextScope 物件(ServletContext 的封裝)

  3. 向 Spring 應用上下文註冊 contextParameterscontextAttributes 屬性(會先被封裝成 Map)

  4. 【重點】呼叫 getServletContextInitializerBeans() 方法,先從 Spring 應用上下文找到所有的 ServletContextInitializer 物件,也就會找到各種 RegistrationBean,然後依次呼叫他們的 onStartup 方法,向 ServletContext 上下文註冊 Servlet、Filter 和 EventListener

    protected 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);
	}
}

過程如下:

  1. 設定型別為 ServletContextInitializer
  2. 找到 IoC 容器中所有 ServletContextInitializer 型別的 Bean,並將這些資訊新增到 seeninitializers 集合中
  3. 從 IoC 容器中獲取 Servlet or Filter or EventListener 型別的 Bean,適配成 RegistrationBean 物件,並新增到 initializersseen 集合中
  4. initializers 中的所有 ServletContextInitializer 進行排序,並儲存至 sortedList 集合中
  5. 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);
	}
}

類圖:

精盡Spring Boot原始碼分析 - 內嵌Tomcat容器的實現

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() 兩個地方,可以跳到前面的 回顧 小節中看看,分別做了以下事情:

  1. 建立一個 WebServer 服務物件,例如 TomcatWebServer 物件,對 Tomcat 的封裝,用於控制 Tomcat 伺服器
    1. 先建立一個 org.apache.catalina.startup.Tomcat 物件 tomcat,使用臨時目錄作為基礎目錄(tomcat.埠號),退出時刪除,同時會設定埠、編碼、最小空閒執行緒和最大執行緒數
    2. tomcat 建立一個 TomcatEmbeddedContext 上下文物件,會新增一個 TomcatStarter(實現 javax.servlet.ServletContainerInitializer 介面)到這個上下文物件中
    3. tomcat 封裝到 TomcatWebServer 物件中,例項化過程會啟動 tomcat,啟動後會觸發 javax.servlet.ServletContainerInitializer 實現類的回撥,也就會觸發 TomcatStarter 的回撥,在其內部會呼叫 Spring Boot 自己的 ServletContextInitializer 初始器,例如 ServletWebServerApplicationContext#selfInitialize(ServletContext) 匿名方法
    4. 在這個匿名方法中會找到所有的 RegistrationBean,執行他們的 onStartup 方法,將其關聯的 Servlet、Filter 和 EventListener 新增至 Servlet 上下文中,包括 Spring MVC 的 DispatcherServlet 物件
  2. 啟動上一步建立的 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 容器執行,這種方式的實現在哪裡呢?我們在下一篇文章進行分析

相關文章