主頁 > 後端開發 > 精盡Spring Boot原始碼分析 - 內嵌Tomcat容器的實作

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

2021-07-02 06:09:27 後端開發

該系列文章是筆者在學習 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 = https://www.cnblogs.com/lifullmoon/p/DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
				WebMvcProperties webMvcProperties, ObjectProvider 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 容器運行,這種方式的實作在哪里呢?我們在下一篇文章進行分析

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/288741.html

標籤:Java

上一篇:為什么不建議用 equals 判斷物件相等?

下一篇:現在作業流都用什么? activiti 和 flowable 哪個好

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more