精盡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 包直接啟動我們的應用,如下:

<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](//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. 如果 WebServer 和 ServletContext 都為空,則需要創建一個,此時使用 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 環境中

整個過程有點繞,如果獲取到的 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);
}

過程如下:

  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 的初始化過程沒有特別的複雜,主要是因為這裡沒有深入分析,我們知道大致的流程即可,這裡我們重點關注第 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);
}

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

  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.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源碼分析 - 文章導讀](//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 應用上下文註冊 contextParameters 和 contextAttributes 屬性(會先被封裝成 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,並將這些資訊添加到 seen 和 initializers 集合中
  3. 從 IoC 容器中獲取 Servlet or Filter or EventListener 類型的 Bean,適配成 RegistrationBean 對象,並添加到 initializers 和 seen 集合中
  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);
	}
}

類圖:

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 容器運行,這種方式的實現在哪裡呢?我們在下一篇文章進行分析