精盡Spring MVC原始碼分析 - 尋找遺失的 web.xml

月圓吖發表於2020-12-11

該系列文件是本人在學習 Spring MVC 的原始碼過程中總結下來的,可能對讀者不太友好,請結合我的原始碼註釋 Spring MVC 原始碼分析 GitHub 地址 進行閱讀

Spring 版本:5.2.4.RELEASE

尋找遺失的 web.xml

在開始 Spring MVC 的分析之前,先來聊一聊 Java 初學者接觸的最多的 Java Web 基礎。還記得我的第一個 Web 工程是由 Servlet、Velocity 和 Filter 來完成的,那時幾乎所有人都是根據 Servlet、JSP 和 Filter 來編寫自己的第一個 Hello World 工程。那時,還離不開 web.xml 配置檔案,需要對 Servlet 和 Filter 進行配置,相對來說比較繁瑣。隨著 Spring 體系的快速發展,配置逐漸演變成了 Java Configuration 和 XML 配置兩種方式的共存。現如今,Spring BootSpring Cloud 在許多中大型企業中被普及,Java Configuration 成為了主流,XML 配置的方式也逐漸“消失”在我們的視野裡面。不知道現在的小夥伴是否還記得那個 web.xml 檔案,這中間都發生過什麼變化,其中的 Servlet 和 Filter 配置項被什麼取代了?

  • Servlet:Java Servlet 為 Web 開發人員提供了一種簡單,一致的機制,以擴充套件 Web 伺服器的功能並訪問現有的業務系統。實現了 Servlet 介面的類在 Servlet 容器中可用於處理請求併傳送響應。

  • Tomcat:Tomcat 是 Web 應用伺服器,是一個 Servlet 容器,實現了對 Servlet 和 JSP 的支援。

如果應用程式是以 war 包的方式放入 Tomcat 的 webapps 資料夾下面,那麼在 Tomcat 啟動時會載入 war 包,生成對應的一個資料夾,Tomcat 則會去對 webapps 資料夾下面的每一個資料夾(我們的應用程式)生成一個部署任務,去解析對應的 WEB-INF/web.xml 檔案,將配置的 Servlet 載入到 Servlet 容器中。當 Tomcat 監聽到某埠的 HTTP 請求時,則會將請求解析成 Request 物件,然後交由相應的 Servlet 進行處理,最後將處理結果轉換成 HTTP 響應。

為什麼是 webapps 目錄和 WEB-INF/web.xml 檔案,可以看一下 Tomcat 的 conf/server.xml 和 conf/context.xml 兩個配置檔案,如下:

<!-- server.xml -->
<!-- appBase 屬性指定應用程式所在目錄 -->
<Host name="localhost"  appBase="webapps"  unpackWARs="true" autoDeploy="true">
    
<!-- context.xml -->
<Context>
    <!-- Default set of monitored resources. If one of these changes, the web application will be reloaded. -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>
</Context>

Servlet3.0 以前的時代

servlet

為了體現出整個演進過程,先來回顧下當初我們是怎麼寫 Servlet 和 Filter 程式碼來完成自己的第一個 Hello World 工程

專案結構

.
├── pom.xml
├── src
    ├── main
    │   ├── java
    │   │   └── cn
    │   │       └── edu
    │   │          └── shopping
    │   │              ├── filter
    │   │              │   └── HelloWorldFilter.java
    │   │              └── servlet
    │   │                  └── HelloWorldServlet.java
    │   └── webapp
    │       └── WEB-INF
    │           └── web.xml
    └── test
        └── java

cn.edu.shopping.servlet.HelloWorldServlet.java

public class HelloWorldServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setContentType("text/plain");
        PrintWriter writer = response.getWriter();
        writer.println("Hello World");
    }
}

cn.edu.shopping.filter.HelloWorldFilter.java

public class HelloWorldFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        System.out.println("觸發 Hello World 過濾器...");
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {

    }
}

web.xml 中配置 Servlet 和 Filter

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

  <servlet>
    <servlet-name>HelloWorldServlet</servlet-name>
    <servlet-class>cn.edu.shopping.servlet.HelloWorldServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>HelloWorldServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
  </servlet-mapping>
  
  <filter>
    <filter-name>HelloWorldFilter</filter-name>
    <filter-class>cn.edu.shopping.filter.HelloWorldFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>HelloWorldFilter</filter-name>
    <url-pattern>/hello</url-pattern>
  </filter-mapping>

</web-app>

上述就是我當初第一個 Hello World 工程,配置 Tomcat 後啟動,在瀏覽器裡面輸入 http://127.0.0.1:8080/hello 可看到 “Hello World”,在控制檯會列印“觸發 Hello World 過濾器...”

Servlet3.0 新特性

servlet_3.0

參考 IBM 的 Servlet 3.0 新特性詳解 文章

Servlet 3.0 作為 Java EE 6 規範體系中一員,隨著 Java EE 6 規範一起釋出。該版本在前一版本(Servlet 2.5)的基礎上提供了若干新特性用於簡化 Web 應用的開發和部署。其中有幾項特性的引入讓開發者感到非常興奮,同時也獲得了 Java 社群的一片讚譽之聲:

  1. 非同步處理支援:有了該特性,Servlet 執行緒不再需要一直阻塞,直到業務處理完畢才能再輸出響應,最後才結束該 Servlet 執行緒。在接收到請求之後,Servlet 執行緒可以將耗時的操作委派給另一個執行緒來完成,自己在不生成響應的情況下返回至容器。針對業務處理較耗時的情況,這將大大減少伺服器資源的佔用,並且提高併發處理速度。
  2. 新增的註解支援:該版本新增了若干註解,用於簡化 Servlet、過濾器(Filter)和監聽器(Listener)的宣告,這使得 web.xml 部署描述檔案從該版本開始不再是必選的了。
  3. 可插性支援:熟悉 Struts2 的開發者一定會對其通過外掛的方式與包括 Spring 在內的各種常用框架的整合特性記憶猶新。將相應的外掛封裝成 JAR 包並放在類路徑下,Struts2 執行時便能自動載入這些外掛。現在 Servlet 3.0 提供了類似的特性,開發者可以通過外掛的方式很方便的擴充已有 Web 應用的功能,而不需要修改原有的應用。

通過 Servlet3.0 首先提供了 @WebServlet@WebFilter@WebListener 等註解,可以替代 web.xml 檔案中的 Servlet 和 Filter 等配置項

除了以上的新特性之外,ServletContext 物件的功能在新版本中也得到了增強。現在,該物件支援在執行時動態部署 Servlet、過濾器、監聽器,以及為 Servlet 和過濾器增加 URL 對映等。以 Servlet 為例,過濾器與監聽器與之類似。ServletContext 為動態配置 Servlet 增加了如下方法:

  • ServletRegistration.Dynamic addServlet(String servletName,Class<? extends Servlet> servletClass)
  • ServletRegistration.Dynamic addServlet(String servletName, Servlet servlet)
  • ServletRegistration.Dynamic addServlet(String servletName, String className)
  • T createServlet(Class clazz)
  • ServletRegistration getServletRegistration(String servletName)
  • Map<string,? extends servletregistration> getServletRegistrations()

其中前三個方法的作用是相同的,只是引數型別不同而已;通過 createServlet() 方法建立的 Servlet,通常需要做一些自定義的配置,然後使用 addServlet() 方法來將其動態註冊為一個可以用於服務的 Servlet。兩個 getServletRegistration() 方法主要用於動態為 Servlet 增加對映資訊,這等價於在 web.xml( 抑或 web-fragment.xml) 中使用 標籤為存在的 Servlet 增加對映資訊。

以上 ServletContext 新增的方法要麼是在 ServletContextListener 的 contexInitialized 方法中呼叫,要麼是在 ServletContainerInitializeronStartup() 方法中呼叫。

ServletContainerInitializer 也是 Servlet 3.0 新增的一個介面,容器在啟動時使用 JAR 服務 API(JAR Service API) 來發現 ServletContainerInitializer 的實現類,並且容器將 WEB-INF/lib 目錄下 JAR 包中的類都交給該類的 onStartup() 方法處理,我們通常需要在該實現類上使用 @HandlesTypes 註解來指定希望被處理的類,過濾掉不希望給 onStartup() 處理的類。

一個典型的 Servlet3.0+ 的 Web 專案結構如下:

.
├── pom.xml
├── src
    ├── main
    │   ├── java
    │   │   └── cn
    │   │       └── edu
    │   │          └── shopping
    │   │              ├── CustomServletContainerInitializer.java
    │   │              ├── filter
    │   │              │   └── HelloWorldFilter.java
    │   │              └── servlet
    │   │                  └── HelloWorldServlet.java
    │   ├── resources
    │   │   └── META-INF
    │   │       └── services
    │   │           └── javax.servlet.ServletContainerInitializer
    │   └── webapp
    │       └── WEB-INF
    │           └── web.xml
    └── test
        └── java

HelloWorldFilter 和 HelloWorldServlet 沒有變動,新增了一個 CustomServletContainerInitializer 物件,它實現了 javax.servlet.ServletContainerInitializer 介面,用來在 Web 容器啟動時載入需要的 Servlet 和 Filter,程式碼如下:

public class CustomServletContainerInitializer implements ServletContainerInitializer {

    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        System.out.println("建立 Hello World Servlet...");
        javax.servlet.ServletRegistration.Dynamic servlet = ctx.addServlet(
            HelloWorldServlet.class.getSimpleName(), HelloWorldServlet.class);
        servlet.addMapping("/hello");

        System.out.println("建立 Hello World Filter...");
        javax.servlet.FilterRegistration.Dynamic filter = ctx.addFilter(HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class);
        EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
        dispatcherTypes.add(DispatcherType.REQUEST);
        dispatcherTypes.add(DispatcherType.FORWARD);
        filter.addMappingForUrlPatterns(dispatcherTypes, true, "/hello");
    }
}

在實現的 onStartup 方法中向 ServletContext 物件(Servlet 上下文)新增之前在 web.xml 中配置的 HelloWorldFilter 和 HelloWorldServlet,這樣一來就可以去除 web.xml 檔案了。

方法入參中的 Set<Class<?>> c 是和 @HandlesTypes 註解結合使用的,指定需要處理的 Calss 類,可以參考 Spring 中的 SpringServletContainerInitializer 使用方法

這麼宣告一個 ServletContainerInitializer 的實現類,Web 容器並不會識別它,需要藉助 SPI 機制來指定該初始化類,通過在專案 ClassPath 路徑下建立 META-INF/services/javax.servlet.ServletContainerInitializer 檔案來做到的,內容如下:

cn.edu.shopping.CustomServletContainerInitializer

這樣一來,使用 ServletContainerInitializer 和 SPI 機制則可以拜託 web.xml 了。

Spring 是如何支援 Servlet3.0

回到 Spring 全家桶,你可能已經忘什麼時候開始不寫 web.xml 了,現在的專案基本看不到它了,Spring 又是如何支援 Servlet3.0 規範的呢?

在 Spring 的 spring-web 子工程的 ClassPath 下面的有一個 META-INF/services/javax.servlet.ServletContainerInitializer 檔案,如下:

org.springframework.web.SpringServletContainerInitializer

org.springframework.web.SpringServletContainerInitializer 類,程式碼如下:

/**
 * Servlet 3.0 {@link ServletContainerInitializer} designed to support code-based
 * configuration of the servlet container using Spring's {@link WebApplicationInitializer}
 * SPI as opposed to (or possibly in combination with) the traditional
 * {@code web.xml}-based approach.
 */
@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...
                // <1>
				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) {
            // <2>
			initializer.onStartup(servletContext);
		}
	}

}

注意我在原始碼中標註兩個序號,這對於我們理解 Spring 裝配 Servlet 的流程來說非常重要

<1> 提示我們由於 Servlet 廠商實現的差異,onStartup 方法會載入我們本不想處理的 Class 物件,所以進行了特判。

<2> Spring 與我們上述提供的 Demo 不同,並沒有在 SpringServletContainerInitializer 中直接對 Servlet 和 Filter 進行註冊,而是委託給了一個陌生的類 WebApplicationInitializer ,這個類便是 Spring 用來初始化 Web 環境的委託者類,它的實現類:

WebApplicationInitializer

你一定不會對 DispatcherServlet 感到陌生,他就是 Spring MVC 中的核心類,AbstractDispatcherServletInitializer 便是無 web.xml 前提下,建立 DispatcherServlet 的關鍵類,程式碼如下:

public abstract class AbstractDispatcherServletInitializer extends AbstractContextLoaderInitializer {

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		// 呼叫父類啟動的邏輯
		super.onStartup(servletContext);
		// 註冊 DispacherServlt
		registerDispatcherServlet(servletContext);
	}

	protected void registerDispatcherServlet(ServletContext servletContext) {
		// 獲得 Servlet 名
		String servletName = getServletName();
		Assert.hasLength(servletName, "getServletName() must not return null or empty");

		// <1> 建立 WebApplicationContext 物件
		WebApplicationContext servletAppContext = createServletApplicationContext();
		Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");

		// <2> 建立 FrameworkServlet 物件
		FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
		Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
		dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

		ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
		if (registration == null) {
			throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
					"Check if there is another servlet registered under the same name.");
		}

		registration.setLoadOnStartup(1);
		registration.addMapping(getServletMappings());
		registration.setAsyncSupported(isAsyncSupported());

		// <3> 註冊過濾器
		Filter[] filters = getServletFilters();
		if (!ObjectUtils.isEmpty(filters)) {
			for (Filter filter : filters) {
				registerServletFilter(servletContext, filter);
			}
		}

		customizeRegistration(registration);
	}
}

<1> 處,呼叫 createServletApplicationContext() 方法,建立 WebApplicationContext 物件,程式碼如下:

// AbstractAnnotationConfigDispatcherServletInitializer.java
@Override
protected WebApplicationContext createServletApplicationContext() {
    AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
    Class<?>[] configClasses = getServletConfigClasses();
    if (!ObjectUtils.isEmpty(configClasses)) {
        context.register(configClasses);
    }
    return context;
}
  • 該方法由子類 AbstractAnnotationConfigDispatcherServletInitializer 重寫,並且建立的 WebApplicationContext 的子類 AnnotationConfigWebApplicationContext 物件

<2> 處,呼叫 createDispatcherServlet(WebApplicationContext servletAppContext) 方法,建立 FrameworkServlet 物件,程式碼如下:

// AbstractDispatcherServletInitializer.java
protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
    return new DispatcherServlet(servletAppContext);
}
  • 建立 FrameworkServlet 的子類 DispatcherServlet 物件
  • 另外,比較有趣的是傳入的 servletAppContext 方法引數,這就是該 DispatcherServlet 的 Servlet WebApplicationContext 容器

注意,上述這一切特性從 Spring 3 就已經存在了,而如今 Spring 5 已經伴隨 SpringBoot 2.0 一起發行了

SpringBoot 如何配置 Servlet

讀到這兒,你已經閱讀了全文的 1/2。SpringBoot 對於 Servlet 的處理才是重頭戲,因為 SpringBoot 使用範圍很廣,很少有人用 Spring 而不用 SpringBoot 了

是的,前面所講述的 Servlet 的規範,無論是 web.xml 中的配置,還是 Servlet3.0 中的 ServletContainerInitializer 和 SpringBoot 的載入流程都沒有太大的關聯。按照慣例,先賣個關子,先看看如何在 SpringBoot 中註冊 Servlet 和 Filter,再來解釋下 SpringBoot 的獨特之處

註冊方式一:Servlet3.0 註解 +@ServletComponentScan

SpringBoot 依舊相容 Servlet 3.0 一系列以 @Web* 開頭的註解:@WebServlet@WebFilter@WebListener

@WebServlet("/hello")
public class HelloWorldServlet extends HttpServlet{}
@WebFilter("/hello/*")
public class HelloWorldFilter implements Filter {}

在啟動類上面新增 @ServletComponentScan 註解去掃描到這些註解

@SpringBootApplication
@ServletComponentScan
public class SpringBootServletApplication {
   public static void main(String[] args) {
      SpringApplication.run(SpringBootServletApplication.class, args);
   }
}

這種方式相對來說比較簡介直觀,其中 org.springframework.boot.web.servlet.@ServletComponentScan 註解通過 @Import(ServletComponentScanRegistrar.class) 方式,它會將掃描到的 @WebServlet@WebFilter@WebListener 的註解對應的類,最終封裝成 FilterRegistrationBean、ServletRegistrationBean、ServletListenerRegistrationBean 物件,註冊到 Spring 容器中。也就是說,和註冊方式二:RegistrationBean統一了

註冊方式二:RegistrationBean

@Configuration
public class WebConfig {
    @Bean
    public ServletRegistrationBean<HelloWorldServlet> helloWorldServlet() {
        ServletRegistrationBean<HelloWorldServlet> servlet = new ServletRegistrationBean<>();
        servlet.addUrlMappings("/hello");
        servlet.setServlet(new HelloWorldServlet());
        return servlet;
    }

    @Bean
    public FilterRegistrationBean<HelloWorldFilter> helloWorldFilter() {
        FilterRegistrationBean<HelloWorldFilter> filter = new FilterRegistrationBean<>();
        filter.addUrlPatterns("/hello/*");
        filter.setFilter(new HelloWorldFilter());
        return filter;
    }
}

ServletRegistrationBean 和 FilterRegistrationBean 都繼成 RegistrationBean,它是 SpringBoot 中廣泛應用的一個註冊類,負責把 Servlet,Filter,Listener 給容器化,使它們被 Spring 託管,並且完成自身對 Web 容器的註冊,這種註冊方式值得推崇

RegistrationBean

從圖中可以看出 RegistrationBean 的地位,它的幾個實現類作用分別是:

  • 其中最底層有三個類分別幫助 Spring 容器註冊 Filter,Servlet,Listener 物件
  • 還有一個 DelegatingFilterProxyRegistrationBean,熟悉 Spring Security 的朋友應該不會感到陌生,SpringSecurityFilterChain 就是通過這個代理類來呼叫的
  • 另外 RegistrationBean 實現了 ServletContextInitializer 介面,這個介面將會是下面分析的核心介面,大家先混個眼熟,瞭解下它有一個抽象實現 RegistrationBean 即可

SpringBoot 載入 Servlet 的流程

暫時只介紹上面兩種方式,接下來開始討論 SpringBoot 中 Servlet 的載入流程,討論的前提是 SpringBoot 環境下使用內嵌的容器,比如最典型的 Tomcat

Initializer 被替換為 TomcatStarter

當使用內嵌的 Tomcat 時,你在 SpringServletContainerInitializer 上面打斷點,會發現根本不會進入該類的內部,因為 SpringBoot 完全走了另一套初始化流程,而是進入了 org.springframework.boot.web.embedded.tomcat.TomcatStarter 這個類

ServletContainerInitializer

仔細掃一眼原始碼包,並沒有發現有 SPI 檔案對應到 TomcatStarter,也就是說沒有通過 SPI 機制載入這個類,為什麼沒有這麼做呢?可以翻閱 Spring Github 中的 issue,其中有 Spring 作者肯定的答覆:https://github.com/spring-projects/spring-boot/issues/321

This was actually an intentional design decision. The search algorithm used by the containers was problematic. It also causes problems when you want to develop an executable WAR as you often want a javax.servlet.ServletContainerInitializer for the WAR that is not executed when you run java -jar.

See the org.springframework.boot.context.embedded.ServletContextInitializer for an option that works with Spring Beans.

SpringBoot 這麼做是有意而為之,我們在使用 SpringBoot 時,開發階段一般都是使用內嵌 Tomcat 容器,但部署時卻存在兩種選擇:一種是打成 jar 包,使用 java -jar 的方式執行;另一種是打成 war 包,交給外接容器去執行。

前者就會導致容器搜尋演算法出現問題,因為這是 jar 包的執行策略,不會按照 Servlet 3.0 的策略去載入 ServletContainerInitializer

最後作者還提供了一個替代選項:ServletContextInitializer,它和 ServletContainerInitializer 長得特別像,別搞混淆了!

  • 前者 ServletContextInitializer 是 org.springframework.boot.web.servlet.ServletContextInitializer
  • 後者 ServletContainerInitializer 是 javax.servlet.ServletContainerInitializer,前文提到的 RegistrationBean 就實現了 ServletContextInitializer 介面

TomcatStarter 中的 ServletContextInitializer 是關鍵

TomcatStarter 中 org.springframework.boot.context.embedded.ServletContextInitializer[] initializers 屬性,是 SpringBoot 初始化 Servlet,Filter,Listener 的關鍵,程式碼如下:

class TomcatStarter implements ServletContainerInitializer {

	private static final Log logger = LogFactory.getLog(TomcatStarter.class);

	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 {
			for (ServletContextInitializer initializer : this.initializers) {
				initializer.onStartup(servletContext);
			}
		}
		catch (Exception ex) {
			this.startUpException = ex;
			// Prevent Tomcat from logging and re-throwing when we know we can
			// deal with it in the main thread, but log for information here.
			if (logger.isErrorEnabled()) {
				logger.error("Error starting Tomcat context. Exception: "
						+ ex.getClass().getName() + ". Message: " + ex.getMessage());
			}
		}
	}

	public Exception getStartUpException() {
		return this.startUpException;
	}

}

onStartup(Set<Class<?>> classes, ServletContext servletContext) 方法中,負責呼叫一系列的 ServletContextInitializer 物件的 onStartup 方法

那麼在 debug 的過程中,構造方法中的 ServletContextInitializer[] initializers 入參到底包含了哪些類呢?會不會有我們前面介紹的 RegistrationBean 呢?

TomcatStarter-01

RegistrationBean 並沒有出現在 TomcatStarter 的 debug 資訊中,initializers 包含了三個類,其中只有第 3 個類看上去比較核心,ServletWebServerApplicationContext 的 子類 AnnotationConfigServletWebServerApplicationContext 物件,為了搞清楚 SpringBoot 如何載入 Filter、Servlet、Listener ,看來還得研究下 ServletWebServerApplicationContext 物件

上面是基於 SpringBoot 2.0.3.RELEASE 版本做的整體分析,如果是其他版本,可能會存在部分差異,不過原理都相同,不會有太大的變化

ServletWebServerApplicationContext 中的6層迭代載入

ApplicationContext 大家應該是比較熟悉的,這是 Spring 一個比較核心的類,一般我們可以從中獲取到那些註冊在容器中的託管 Bean,而這篇文章,主要分析的便是它在內嵌容器中的實現類:org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext ,重點分析它載入 Filter、Servlet 和 Listener 這部分的程式碼。

這裡是整個程式碼中迭代層次最深的部分,做好心理準備起航,來看看 ServletWebServerApplicationContext 是怎麼獲取到所有的 Filter、Servlet 和 Listener 物件的,以下方法大部分出自於 ServletWebServerApplicationContext

第一層:onRefresh()

onRefresh() 方法,是 ApplicationContext 的生命週期方法,ServletWebServerApplicationContext 的實現非常簡單,只幹了一件事:

@Override
protected void onRefresh() {
    super.onRefresh();
    try {
        createWebServer(); //第二層的入口
    }
    catch (Throwable ex) {
        throw new ApplicationContextException("Unable to start web server", ex);
    }
}
第二層:createWebServer()

看名字 Spring 是想建立一個內嵌的 Web 容器,程式碼如下:

private void createWebServer() {
    WebServer webServer = this.webServer;
    ServletContext servletContext = getServletContext();
    if (webServer == null && servletContext == null) {
        ServletWebServerFactory factory = getWebServerFactory();
        this.webServer = factory.getWebServer(getSelfInitializer()); // 第三層的入口
    }
    else if (servletContext != null) {
        try {
            getSelfInitializer().onStartup(servletContext);
        }
        catch (ServletException ex) {
            throw new ApplicationContextException("Cannot initialize servlet context",
                    ex);
        }
    }
    initPropertySources();
}

凡是帶有 Servlet,Initializer 字樣的方法,都是我們需要留意的。其中 getSelfInitializer() 方法,便涉及到了我們最為關心的初始化流程,所以接著連線到了第三層

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

private void selfInitialize(ServletContext servletContext) throws ServletException {
    prepareWebApplicationContext(servletContext);
    ConfigurableListableBeanFactory beanFactory = getBeanFactory();
    ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes(
            beanFactory);
    WebApplicationContextUtils.registerWebApplicationScopes(beanFactory,
            getServletContext());
    existingScopes.restore();
    WebApplicationContextUtils.registerEnvironmentBeans(beanFactory,
            getServletContext());
    // 第四層的入口
    for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
        beans.onStartup(servletContext);
    }
}

還記得前面 TomcatStarter 的 debug 資訊中,第 3 個 ServletContextInitializer 就是在 ServletWebServerApplicationContext 這裡的 getSelfInitializer() 方法中建立的

解釋下這裡的 getSelfInitializer()selfInitialize(ServletContext servletContext) 方法,為什麼要這麼設計

這是典型的回撥式方式,當匿名 ServletContextInitializer 類被 TomcatStarter 的 onStartup() 方法呼叫,設計上是觸發了 selfInitialize(ServletContext servletContext) 方法的呼叫

所以這下就清晰了,為什麼 TomcatStarter 中沒有出現 RegistrationBean ,其實是隱式觸發了 ServletWebServerApplicationContext 中的 selfInitialize(ServletContext servletContext) 方法。這樣,在 selfInitialize(ServletContext servletContext) 方法中,呼叫 getServletContextInitializerBeans() 方法,獲得 ServletContextInitializer 陣列就成了關鍵

第四層:getServletContextInitializerBeans()
/**
 * Returns {@link ServletContextInitializer}s that should be used with the embedded web server. 
 * By default this method will first attempt to find {@link ServletContextInitializer}, 
 * {@link Servlet}, {@link Filter} and certain {@link EventListener} beans.
 * @return the servlet initializer beans
 */
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
    return new ServletContextInitializerBeans(getBeanFactory()); //第五層的入口
}

從註釋中可以知曉這個 ServletContextInitializerBeans 類,就是用來載入 Servlet 和 Filter 的

第五層:ServletContextInitializerBeans

org.springframework.boot.web.servlet.ServletContextInitializerBeans

public ServletContextInitializerBeans(ListableBeanFactory beanFactory) {
    this.initializers = new LinkedMultiValueMap<>();
    addServletContextInitializerBeans(beanFactory); // 第六層的入口
    addAdaptableBeans(beanFactory);
    List<ServletContextInitializer> sortedInitializers = this.initializers.values()
            .stream()
            .flatMap((value) -> value.stream()
                    .sorted(AnnotationAwareOrderComparator.INSTANCE))
            .collect(Collectors.toList());
    this.sortedList = Collections.unmodifiableList(sortedInitializers);
}
第六層:addServletContextInitializerBeans(beanFactory)
// ServletContextInitializerBeans.java
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
    for (Entry<String, ServletContextInitializer> initializerBean :  getOrderedBeansOfType(
        beanFactory, ServletContextInitializer.class)) {
        addServletContextInitializerBean(initializerBean.getKey(),
                initializerBean.getValue(), beanFactory);
    }
}

getOrderedBeansOfType(beanFactory, ServletContextInitializer.class) 方法,便是去 Spring 容器中尋找註冊過的 ServletContextInitializer 物件們,這時候就可以把之前那些 RegistrationBean 全部載入出來了,並且 RegistrationBean 還實現了 Ordered 介面,在這兒用於排序

ServletWebServerApplicationContext 載入流程總結

如果你對具體的程式碼流程不感興趣,可以跳過上述的 6 層分析,直接看本節的結論,總結如下:

  • ServletWebServerApplicationContext 的 onRefresh() 方法觸發配置了一個匿名的 ServletContextInitializer
  • 這個匿名的 ServletContextInitializer 的 onStartup 方法會去容器中搜尋到了所有的 RegisterBean 並按照順序載入到 ServletContext 中
  • 這個匿名的 ServletContextInitializer 最終傳遞給 TomcatStarter,由 TomcatStarter 的 onStartup 方法去觸發 ServletContextInitializer 的 onStartup 方法,最終完成裝配
TomcatStarter-02

從上圖中可以看到,我們配置的 Filter 和 Servlet 註冊類都獲取到了,然後呼叫其 onStartup 方法,進去後你會發現呼叫 ServletContext 物件的 addServlet 方法註冊 Servlet,可以返回 Servlet3.0 新特性小節中回顧一下

第三種註冊 Servlet 的方式

研究完了上述 SpringBoot 載入 Servlet 的內部原理,可以發現 ServletContextInitializer 其實是 Spring 中 ServletContainerInitializer 的代理,雖然 SpringBoot 中 Servlet3.0 不起作用了,但它的代理還是會被載入的,於是我們有了第三種方式註冊 servlet

@Configuration
public class CustomServletContextInitializer implements ServletContextInitializer {

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        System.out.println("建立 Hello World Servlet...");
        javax.servlet.ServletRegistration.Dynamic servlet = ctx.addServlet(
            HelloWorldServlet.class.getSimpleName(), HelloWorldServlet.class);
        servlet.addMapping("/hello");

        System.out.println("建立 Hello World Filter...");
        javax.servlet.FilterRegistration.Dynamic filter = ctx.addFilter(HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class);
        EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
        dispatcherTypes.add(DispatcherType.REQUEST);
        dispatcherTypes.add(DispatcherType.FORWARD);
        filter.addMappingForUrlPatterns(dispatcherTypes, true, "/hello");
    }
}

雖然 ServletCantainerInitializer 不能被內嵌容器載入,ServletContextInitializer 卻能被 SpringBoot 的 ServletWebServerApplicationContext 載入到,從而裝配其中的 Servlet 和 Filter。實際開發中,還是以一,二兩種方式來註冊為主,這裡只是提供一個可能性,來讓我們理解 SpringBoot 的載入流程

載入流程拾遺

TomcatStarter 既然不是通過 SPI 機制裝配的,那是怎麼被 Spring 使用的?

自然是被 new 出來的,在 TomcatServletWebServerFactory#configureContext 中可以看到,TomcatStarter 是被主動例項化出來的,並且還傳入了 ServletContextInitializer 的陣列,和上面分析的一樣,一共有三個 ServletContextInitializer,包含了 ServletWebServerApplicationContext 中的匿名實現

protected void configureContext(Context context, ServletContextInitializer[] initializers) {
    // <1>
    TomcatStarter starter = new TomcatStarter(initializers);
    // <2>
    if (context instanceof TomcatEmbeddedContext) {
        // Should be true
        ((TomcatEmbeddedContext) context).setStarter(starter);
    }
    // ... 省略相關程式碼
}
  • <1> 處,建立了 TomcatStarter 物件。
  • <2> 處,通過 context instanceof TomcatEmbeddedContext 判斷使用的是內嵌的 Tomcat ,所以將 TomcatStarter 作為 Initializer

如果對 <2> 處的邏輯感興趣的胖友,可以在以下方法上打斷點進行除錯

  1. TomcatServletWebServerFactory#getWebServer(ServletContextInitializer... initializers)

  2. TomcatStarter#onStartup(Set<Class<?>> classes, ServletContext servletContext)

  3. ServletWebServerApplicationContext#createWebServer

執行順序:3、1、2

TomcatServletWebServerFactory 又是如何被宣告的?
@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
// 這個就是我們 SpringBoot 中 application.yml 配置檔案中 server.* 配置類,也就是 Tomcat 相關配置
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
		ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
		ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
		ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {

	@Bean
	public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(
			ServerProperties serverProperties) {
		return new ServletWebServerFactoryCustomizer(serverProperties);
	}

	@Bean
    // 保證存在 Tomcat 的 Class 物件
	@ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
	public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(
			ServerProperties serverProperties) {
		return new TomcatServletWebServerFactoryCustomizer(serverProperties);
	}
    // 省略 WebServerFactoryCustomizerBeanPostProcessor 類
}

其中 @Import 註解會注入 ServletWebServerFactoryConfiguration 的幾個靜態內部類,如下:

class ServletWebServerFactoryConfiguration {

	@Configuration
	@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedTomcat {

		@Bean
		public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
			return new TomcatServletWebServerFactory();
		}
	}
    // 省略 EmbeddedJetty、EmbeddedUndertow
}

這樣一來,只要 classpath 下存在 javax.servlet.Servletorg.apache.catalina.startup.Tomcatorg.apache.coyote.UpgradeProtocol 類,並且不存在 ServletWebServerFactory 型別的 Bean 則會注入 EmbeddedTomcat 配置類,也就建立一個 TomcatServletWebServerFactory 型別的 Bean

總結

存在 web.xml 配置的 Java Web 專案,Servlet3.0 的 Java Web 專案,Spring Boot 內嵌容器的 Java Web 專案載入 Servlet,這三種專案,Servlet,Filter,Listener 的流程都是有所差異的。理解清楚這其中的由來,其實並不容易,至少得搞懂 Servlet3.0 的規範,SpringBoot 內嵌容器的載入流程等等前置邏輯

簡化了整個 SpringBoot 載入 Servlet 的流程,如下圖所示:

SpringBoot-LoadServlet-Process

參考文章:芋道原始碼《精盡 MyBatis 原始碼分析》

參考文章:Spring 揭祕 -- 尋找遺失的 web.xml

相關文章