精盡Spring Boot源碼分析 – 支援外部 Tomcat 容器的實現

該系列文章是筆者在學習 Spring Boot 過程中總結下來的,裡面涉及到相關源碼,可能對讀者不太友好,請結合我的源碼注釋 Spring Boot 源碼分析 GitHub 地址 進行閱讀

Spring Boot 版本:2.2.x

最好對 Spring 源碼有一定的了解,可以先查看我的 《死磕 Spring 之 IoC 篇 – 文章導讀》 系列文章

如果該篇內容對您有幫助,麻煩點擊一下「推薦」,也可以關注部落客,感激不盡~

該系列其他文章請查看:《精盡 Spring Boot 源碼分析 – 文章導讀》

概述

我們知道 Spring Boot 應用能夠被打成 war 包,放入外部 Tomcat 容器中運行。你是否知道 Spring Boot 是如何整合 Spring MVC 的呢?

在上一篇 《Spring Boot 內嵌 Tomcat 容器的實現》 文章中分析了 Spring Boot 白打成 jar 包後是如何創建 Tomcat 容器並啟動的,那麼這篇文章主要告訴你 Spring Boot 應用被打成 war 包後放入外部 Tomcat 容器是如何運行的。

如何使用

在我們的 Spring Boot 項目中通常會引入 spring-boot-starter-web 這個依賴,該模組提供全棧的 WEB 開發特性,包括 Spring MVC 依賴和 Tomcat 容器,我們將內部 Tomcat 的 Starter 模組排除掉,如下:

<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 MVC 源碼的時候講過,參考我的 《精盡Spring MVC源碼分析 – 尋找遺失的 web.xml》 這篇文章

藉助於 Servlet 3.0 的一個新特性,新增的一個 javax.servlet.ServletContainerInitializer 介面,在 Servlet 容器啟動時會通過 Java 的 SPI 機制從 META-INF/services/javax.servlet.ServletContainerInitializer 文件中找到這個介面的實現類,然後調用它的 onStartup(..) 方法。

在 Spring 的 spring-web 模組中該文件是這麼配置的:

org.springframework.web.SpringServletContainerInitializer

一起來看看這個類:

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

	@Override
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<>();

		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					} catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}
}

通過 @HandlesTypes 註解指定只處理 WebApplicationInitializer 類型的類

這個過程很簡單,實例化所有 WebApplicationInitializer 類型的對象,然後依次調用它們的 onStartup(ServletContext) 方法

通過打斷點你會發現,有一個 DemoApplication 就是我們的啟動類

這也就是為什麼如果你的 Spring Boot 應用需要打成 war 包放入外部 Tomcat 容器運行的時候,你的啟動類需要繼承 SpringBootServletInitializer 這個抽象類,因為這個抽象類實現類 WebApplicationInitializer 介面,我們只需要繼承它即可

SpringBootServletInitializer

org.springframework.boot.web.servlet.support.SpringBootServletInitializer 抽象類,實現了 WebApplicationInitializer 介面,目的就是支援你將 Spring Boot 應用打包成 war 包放入外部的 Servlet 容器中運行

public abstract class SpringBootServletInitializer implements WebApplicationInitializer {

	protected Log logger; // Don't initialize early

	private boolean registerErrorPageFilter = true;

	protected final void setRegisterErrorPageFilter(boolean registerErrorPageFilter) {
		this.registerErrorPageFilter = registerErrorPageFilter;
	}

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		// Logger initialization is deferred in case an ordered
		// LogServletContextInitializer is being used
		this.logger = LogFactory.getLog(getClass());
		// <1> 創建一個 WebApplicationContext 作為 Root Spring 應用上下文
		WebApplicationContext rootAppContext = createRootApplicationContext(servletContext);
		if (rootAppContext != null) {
			// <2> 添加一個 ContextLoaderListener 監聽器,會監聽到 ServletContext 的啟動事件
			// 因為 Spring 應用上下文在上面第 `1` 步已經準備好了,所以這裡什麼都不用做
			servletContext.addListener(new ContextLoaderListener(rootAppContext) {
				@Override
				public void contextInitialized(ServletContextEvent event) {
					// no-op because the application context is already initialized
				}
			});
		} else {
			this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not "
					+ "return an application context");
		}
	}
}

onStartup(ServletContext) 方法中就兩步:

  1. 調用 createRootApplicationContext(ServletContext) 方法,創建一個 WebApplicationContext 作為 Root Spring 應用上下文
  2. 添加一個 ContextLoaderListener 監聽器,會監聽到 ServletContext 的啟動事件,因為 Spring 應用上下文在上面第 1 步已經準備好了,所以這裡什麼都不用做

1 步是不是和 Spring MVC 類似,同樣創建一個 Root WebApplicationContext 作為 Spring 應用上下文的父對象

createRootApplicationContext 方法

createRootApplicationContext(ServletContext) 方法,創建一個 Root WebApplicationContext 對象,如下:

protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
    // <1> 創建一個 SpringApplication 構造器
    SpringApplicationBuilder builder = createSpringApplicationBuilder();
    // <2> 設置 `mainApplicationClass`,主要用於列印日誌
    builder.main(getClass());
    // <3> 從 ServletContext 上下文中獲取最頂部的 Root ApplicationContext 應用上下文
    ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
    // <4> 如果已存在 Root ApplicationContext,則先置空,因為這裡會創建一個 ApplicationContext 作為 Root
    if (parent != null) {
        this.logger.info("Root context already created (using as parent).");
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
        // <4.1> 添加一個 ApplicationContextInitializer 初始器,
        // 用於設置現在要創建的 Root ApplicationContext 應用上下文的父容器為 `parent`
        builder.initializers(new ParentContextApplicationContextInitializer(parent));
    }
    /**
     * <5> 添加一個 ApplicationContextInitializer 初始器
     * 目的是往 ServletContext 上下文中設置 Root ApplicationContext 為現在要創建的 Root ApplicationContext 應用上下文
     * 並將這個 ServletContext 保存至 ApplicationContext 中,參考 {@link ServletWebServerApplicationContext#createWebServer()} 方法,
     * 如果獲取到了 ServletContext 那麼直接調用其 {@link ServletWebServerApplicationContext#selfInitialize} 方法來註冊各個 Servlet、Filter
     * 例如 {@link DispatcherServlet}
     */
    builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
    // <6> 設置要創建的 Root ApplicationContext 應用上下文的類型(Servlet)
    builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
    // <7> 對 SpringApplicationBuilder 進行擴展
    builder = configure(builder);
    // <8> 添加一個 ApplicationListener 監聽器
    // 用於將 ServletContext 中的相關屬性關聯到 Environment 環境中
    builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
    // <9> 構建一個 SpringApplication 對象,用於啟動 Spring 應用
    SpringApplication application = builder.build();
    // <10> 如果沒有設置 `source` 源對象,那麼這裡嘗試設置為當前 Class 對象,需要有 `@Configuration` 註解
    if (application.getAllSources().isEmpty()
            && MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
        application.addPrimarySources(Collections.singleton(getClass()));
    }
    // <11> 因為 SpringApplication 在創建 ApplicationContext 應用上下文的過程中需要優先註冊 `source` 源對象,如果為空則拋出異常
    Assert.state(!application.getAllSources().isEmpty(),
            "No SpringApplication sources have been defined. Either override the "
                    + "configure method or add an @Configuration annotation");
    // Ensure error pages are registered
    if (this.registerErrorPageFilter) {
        // <12> 添加一個錯誤頁面 Filter 作為 `sources`
        application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
    }
    // <13> 調用 `application` 的 `run` 方法啟動整個 Spring Boot 應用
    return run(application);
}

過程如下:

  1. 創建一個 SpringApplication 構造器,目的就是啟動 Spring 應用咯

    protected SpringApplicationBuilder createSpringApplicationBuilder() {
        return new SpringApplicationBuilder();
    }
    
  2. 設置 mainApplicationClass,也就是你的啟動類,主要用於列印日誌

  3. 從 ServletContext 上下文中獲取最頂部的 Root ApplicationContext 應用上下文 parent,通常這裡沒有父對象,所以為空

  4. 如果 parent 不為空,則先 ServletContext 中的該屬性置空,因為這裡會創建一個 ApplicationContext 作為 Root

    1. 添加一個 ApplicationContextInitializer 初始器,用於設置現在要創建的 Root ApplicationContext 應用上下文的父容器為 parent
  5. 添加一個 ApplicationContextInitializer 初始器,目的是往 ServletContext 上下文中設置 Root ApplicationContext 為現在要創建的 Root ApplicationContext 應用上下文,並將這個 ServletContext 保存至 ApplicationContext 中

    注意,這個對象很關鍵,會將當前 ServletContext 上下文對象設置到 ApplicationContext 對象裡面,那麼後續就不會再創建 Spring Boot 內嵌的 Tomcat 了

  6. 設置要創建的 Root ApplicationContext 應用上下文的類型(Servlet)

  7. 對 SpringApplicationBuilder 進行擴展,調用 configure(SpringApplicationBuilder) 方法,這也就是為什麼我們的啟動類可以重寫該方法,通常不用做什麼

  8. 添加一個 ApplicationListener 監聽器,用於將 ServletContext 中的相關屬性關聯到 Environment 環境中

  9. 構建一個 SpringApplication 對象 application,用於啟動 Spring 應用

  10. 如果沒有設置 source 源對象,那麼這裡嘗試設置為當前 Class 對象,需要有 @Configuration 註解

  11. 因為 SpringApplication 在創建 ApplicationContext 應用上下文的過程中需要優先註冊 source 源對象,如果為空則拋出異常

  12. 添加一個錯誤頁面 Filter 作為 sources

  13. 調用 applicationrun 方法啟動整個 Spring Boot 應用

整個過程不複雜,SpringApplication 相關的內容在前面的 《SpringApplication 啟動類的啟動過程》文章中已經分析過,這裡的關鍵在於第 5

添加的 ServletContextApplicationContextInitializer 會將當前 ServletContext 上下文對象設置到 ApplicationContext 對象裡面

ServletContextApplicationContextInitializer

public class ServletContextApplicationContextInitializer
		implements ApplicationContextInitializer<ConfigurableWebApplicationContext>, Ordered {

	private int order = Ordered.HIGHEST_PRECEDENCE;

	private final ServletContext servletContext;

	private final boolean addApplicationContextAttribute;

	public ServletContextApplicationContextInitializer(ServletContext servletContext) {
		this(servletContext, false);
	}

	public ServletContextApplicationContextInitializer(ServletContext servletContext,
			boolean addApplicationContextAttribute) {
		this.servletContext = servletContext;
		this.addApplicationContextAttribute = addApplicationContextAttribute;
	}

	public void setOrder(int order) {
		this.order = order;
	}

	@Override
	public int getOrder() {
		return this.order;
	}

	@Override
	public void initialize(ConfigurableWebApplicationContext applicationContext) {
		// 將這個 ServletContext 上下文對象設置到 ApplicationContext 中
		applicationContext.setServletContext(this.servletContext);
		if (this.addApplicationContextAttribute) {
			this.servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,
					applicationContext);
		}
	}
}

可以看到會將這個 ServletContext 上下文對象設置到 ApplicationContext 中

那麼我們回顧到上一篇 《Spring Boot 內嵌 Tomcat 容器的實現》 文章的 1. onRefresh 方法小節調用的 createWebServer() 方法,如下:

// ServletWebServerApplicationContext.java
private void createWebServer() {
    // <1> 獲取當前 `WebServer` 容器對象,首次進來為空
    WebServer webServer = this.webServer;
    // <2> 獲取 `ServletContext` 上下文對象
    ServletContext servletContext = getServletContext();
    // <3> 如果 WebServer 和 ServletContext 都為空,則需要創建一個
    // 使用 Spring Boot 內嵌 Tomcat 容器則會進入該分支
    if (webServer == null && servletContext == null) {
        // <3.1> 獲取 Servlet 容器工廠對象(默認為 Tomcat)`factory`
        ServletWebServerFactory factory = getWebServerFactory();
        /**
         * <3.2> 先創建一個 {@link ServletContextInitializer} Servlet 上下文初始器,實現也就是當前類的 {@link this#selfInitialize(ServletContext)} 方法
         * 至於為什麼不用 Servlet 3.0 新增的 {@link javax.servlet.ServletContainerInitializer} 這個類,我在
         * [精盡Spring MVC源碼分析 - 尋找遺失的 web.xml](//www.cnblogs.com/lifullmoon/p/14122704.html)有提到過
         *
         * <3.3> 從 `factory` 工廠中創建一個 WebServer 容器對象
         * 例如創建一個 {@link TomcatWebServer} 容器對象,並初始化 `ServletContext` 上下文,創建 {@link Tomcat} 容器並啟動
         * 啟動過程非同步觸發了 {@link org.springframework.boot.web.embedded.tomcat.TomcatStarter#onStartup} 方法
         * 也就會調用這個傳入的 {@link ServletContextInitializer} 的 {@link #selfInitialize(ServletContext)} 方法
         */
        this.webServer = factory.getWebServer(getSelfInitializer());
    }
    // <4> 否則,如果 ServletContext 不為空,說明使用了外部的 Servlet 容器(例如 Tomcat)
    else if (servletContext != null) {
        try {
            /** 那麼這裡主動調用 {@link this#selfInitialize(ServletContext)} 方法來註冊各種 Servlet、Filter */
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context", ex);
        }
    }
    // <5> 將 ServletContext 的一些初始化參數關聯到當前 Spring 應用的 Environment 環境中
    initPropertySources();
}

我們看到上面第 4 步,如果從當前 Spring 應用上下文獲取到了 ServletContext 對象,不會走上面的第 3 步,也就是不創建 Spring Boot 內嵌的 Tomcat

主動調用它的 getSelfInitializer() 方法來往這個 ServletContext 對象中註冊各種 Servlet、Filter 和 EventListener 對象,包括 Spring MVC 中的 DispatcherServlet 對象,該方法參考上一篇 《Spring Boot 內嵌 Tomcat 容器的實現》 文章的 2. selfInitialize 方法 小節

總結

本文分析了 Spring Boot 應用被打成 war 包後是如何支援放入外部 Tomcat 容器運行的,原理也比較簡單,藉助 Spring MVC 中的 SpringServletContainerInitializer 這個類,它實現了 Servlet 3.0 新增的 javax.servlet.ServletContainerInitializer 介面

  1. 通過 Java 的 SPI 機制,在 META-INF/services/javax.servlet.ServletContainerInitializer 文件中寫入 SpringServletContainerInitializer 這個類,那麼在 Servlet 容器啟動的時候會調用這個類的 onStartup(..) 方法,會找到 WebApplicationInitializer 類型的對象,並調用他們的 onStartup(ServletContext) 方法

  2. 在我們的 Spring Boot 應用中,如果需要打成 war 包放入外部 Tomcat 容器運行,啟動類則需要繼承 SpringBootServletInitializer 抽象類,它實現了 WebApplicationInitializer 介面

  3. SpringBootServletInitializer 中會創建一個 WebApplicationContext 作為 Root Spring 應用上下文,同時會將 ServletContext 對象設置到 Spring 應用上下文中

  4. 這樣一來,因為已經存在 ServletContext 對象,那麼不會再創建 Spring Boot 內嵌的 Tomcat 容器,而是對 ServletContext 進行一些初始化工作

好了,到這裡關於 Spring Boot 啟動 Spring 應用的整個主流程,包括內嵌 Tomcat 容器的實現,以及支援運行在外部 Servlet 容器的實現都分析完了

那麼接下來,我們一起來看看 @SpringBootApplication 這個註解,也就是 @EnableAutoConfiguration 自動配置註解的實現原理