Spring Boot 源碼分析 - 內嵌Tomcat容器的實現


參考 知識星球芋道源碼 星球的源碼解析,一個活躍度非常高的 Java 技術社群,感興趣的小伙伴可以加入 芋道源碼 星球,一起學習😄

該系列文章是筆者在學習 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 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);
	}
}

類圖:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM