Spring 源碼解析——SpringMVC 源碼解析(Spring 整合 Spring MVC )(一)

目錄

一、概述

二、涉及技術

2.1 SPI 機制

2.2 Servlet 中的 ServletContainerInitializer 接口

2.3 Spring MVC 中的層級容器(Context Hierarchy)及其配置

三、源碼解析(基於註解配置)

3.1 Spring Web 容器初始化器(示例代碼僅提供思路引導)

3.2 調用 Spring Web 容器初始化器

3.3 創建根容器 

3.4 創建 DispatcherServlet 和子容器

3.5 初始化根容器

3.6 初始化子容器並關聯父子容器

四、方法調用時序圖(UML 序列圖)

4.1 父子容器和 DispatcherServlet 的創建

4.2 父子容器初始化

五、內容總結


寫文章不易,時序圖均爲自研,轉載請標明出處。

同時,如果你喜歡我的文章,請關注我,讓我們一起進步。

一、概述

在這篇博文中我們首先將大概瞭解一下 SPI 機制和 Servlet 以及 SpringMVC 中的基礎技術,然後通過對於源碼的分析來梳理 Spring 整合 SpringMVC 的過程,即探究 Spring 是怎樣完成在 Tomcat 啓動時完成 Spring web 父子容器的創建和初始化工作,最後會通過兩張方法調用時序圖來對整篇博文的內容進行梳理。

 

二、涉及技術

2.1 SPI 機制

因爲 SPI 機制是 Spring web 容器得以自動創建並初始化的核心所在,所以我們先來大概瞭解一下什麼是 SPI 機制。首先 SPI  機制是一種 服務發現機制,全稱爲 Service Provider Interface ,它通過在 ClassPath 路徑下的 resource \ META-INF \ services 目錄中查找符合條件的文件來自動加載文件裏所定義的類。這一機制在許多的框架中都有應用比如在 Dubbo、JDBC 等,SPI 機制的使用讓框架具備了更強的動態擴展能力,同時在 Tomcat 的後續版本中即 Servlet 3.x 中也增加了對於 SPI 機制的支持,也正因如此 SpringMVC 才能夠更加輕易的整合 Spring 和 Servlet 。

對於 SPI 機制的使用有以下幾個重點:

(1)首先我們需要定義一個接口,並讓我們需要被掃描到類實現此接口;

(2)然後在項目的 resource \ META-INF \ services 路徑下創建一個以 接口全限定類名 命名的文件;

(3)在剛剛創建的文件中添加我們需要被掃描到的類的 類全限定類名

(4)最後我們就可以在代碼中通過 ServiceLoader.load 或者 Service.providers 方法來獲取到我們需要被掃描到的 (兩個方法的參數都爲接口的類型,並且第一個方法返回的是一個集合包裝類,需要通過獲取到它的迭代器來對它進行掃描,而第二個方法返回的是一個可以直接使用的 迭代器);

 

2.2 Servlet 中的 ServletContainerInitializer 接口

public interface ServletContainerInitializer {
    public void onStartup(Set<Class<?>> c, ServletContext ctx)
        throws ServletException; 
}

 老規矩還是直接先上代碼,這個接口的代碼十分的簡單,只有一個 onStartup 方法,但這個接口正是 Servlet 3.0 之後提供對 Java SPI 機制擴展支持的核心,首先我還是帶大家先來了解一下這個接口和其中的方法。

首先對於這個接口,它允許庫或運行時(runtime)在應用程序的啓動階段收到通知,並執行任何需要的代碼來響應它對servlet、filter 和 listener 的註冊。該接口的實現類必須被聲明在 jar 包文件源碼(resources)中 META-INF/services 目錄下文件名爲該接口全限定類名的文件中,這樣它才能夠被運行時服務根據限定規則所查找到,並且發現這些服務(實現類)的順序必須遵循應用程序的類加載委託模型。

其次該接口的實現類可以通過使用 @HandlesTypes 註解來指定需要被注入到 Set 集合參數中來執行 onStartup 方法的類型,例如 @HandlesTypes(WebApplicationInitializer.class),就是指定了需要將掃描到的 WebApplicationInitializer 類型的子類或者實現類添加到 onStartup 方法的第一個參數的 Set 集合中。但是如果這個接口的實現沒有使用這個註解或者沒有任何可以匹配指定類型的實現類的時候,容器必須傳遞給 onStartup 方法一個空的 Set 集合作爲首參。

當應用正在搜索該接口的實現類中 @HandlesTypes 註解指定的類型時,容器可能會因爲類加載時缺少相應的 jar 包而出現問題,這主要是因爲容器不能夠判斷當前類的加載失敗會不會影響到容器的正常工作,所以它必須忽略掉這些異常,但是同時會通過日誌來記錄它們。

下面再來說一說 onStartup 方法,該接口的實現類(滿足上述規範)可以在應用程序啓動時接收到來自 ServletContext 的通知,如果該實現類在應用程序 jar 包的 WEB-INF/lib 目錄下,那麼它的的 onStartup 方法只會被調用一次,但是如果它位於 jar 包中但位於 WEB-INF/lib 目錄之外,那麼每次應用程序啓動的時候都會調用 onStartup 方法。

onStartup 方法的第一個參數是一個 set 集合,包含所有的實現或繼承 @HandlesTypes 註解中所指明類型的類,而當實現類沒有使用 @HandlesTypes 註解或者沒有找到匹配類的時候該參數值爲 null 。而第二個參數代表在其中找到了第一個參數中所包含的類,且正在啓動的 Web 應用程序的 ServletContext 。

 

2.3 Spring MVC 中的層級容器(Context Hierarchy)及其配置

在 Spring MVC 當中存在一個層級容器的概念,也可以簡單的理解爲我們通常所說的父子容器,一般來說在根容器(父容器)中會保存一些相對比較基礎的 Bean 實例,比如數據層或業務層等,而在子容器中一般會保存一些比較特殊的 Bean,比如 Controller 或者 ViewResolver 等,而父子容器的關係通過上圖我們也比較好理解,子容器可以使用(另一種形式的繼承)父容器中的基礎 Bean,而同時這兩個 WebApplicationContext 又是各自存在於其對應的 DispatcherServlet 中的(DispatcherServlet 會對應持有唯一的 WebApplicationContext),所以從整體來看 WebApplicationContext 是存在於 DispatcherServlet 這個大範圍當中的。

public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        // 返回根容器的配置類
        return new Class<?>[] { RootConfig.class };
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        // 返回子容器的配置類
        return new Class<?>[] { App1Config.class };
    }

    @Override
    protected String[] getServletMappings() {
        // 返回 Servlet 的映射關係
        return new String[] { "/app1/*" };
    }
}

 

<web-app>

    <!-- 監聽器配置 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- 根容器配置 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>

    <!-- 子容器配置 -->
    <servlet>
        <servlet-name>app1</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/app1-context.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- Servlet 映射關係配置 -->
    <servlet-mapping>
        <servlet-name>app1</servlet-name>
        <url-pattern>/app1/*</url-pattern>
    </servlet-mapping>

</web-app>

而對於 SpringMVC 當中實現層級容器的配置方式一般有以上兩種,第一種是通過 Java 類來進行配置,在對應的方法中返回對應的配置類,在實際使用中我們一般繼承 AbstractAnnotationConfigDispatcherServletInitializer 來進行更多參數的配置(如 filter 或者 listener 等),對於 AbstractAnnotationConfigDispatcherServletInitializer 和 WebApplicationInitializer 的繼承關係可以參考下圖,而第二種方式就是常用的 xml 文件配置的方式。

需要注意的是當我們不需要適用層級容器時,我們在使用 Java 類進行配置時可以讓 getRootConfigClasses 方法返回 null ,而將所有的配置類通過 getServletConfigClasses 方法返回。

 

三、源碼解析(基於註解配置)

3.1 Spring Web 容器初始化器(示例代碼僅提供思路引導)

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // 加載 Spring web 應用程序配置
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        // 將配置類註冊到容器中
        ac.register(AppConfig.class);
        // 刷新容器
        ac.refresh();

        // 創建並註冊 DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        // 將創建完成的 DispatcherServlet 添加到 Servlet 容器中
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        // 設置啓動順序
        registration.setLoadOnStartup(1);
        // 添加映射關係
        registration.addMapping("/app/*");
    }
}

在開始對源碼的正式分析之前,我們先通過官方文檔來了解一下怎樣通過代碼快速創建一個 Spring Web 容器並完成 DispatcherServlet 的創建和註冊。上面的代碼是官方文檔中給的示例,整體的啓動流程非常的簡單並且可以分爲兩大部分,第一部分完成了 Spring Web 容器的創建和註冊(類似於 ApplicationContext,只不過這裏是 WebApplicationContext),第二部分完成了 DispatcherServlet 的創建、配置並添加到 Servlet 容器中的操作。

雖然僅有這麼幾段代碼卻已經實現了最基本的 Spring Web 的容器啓動和相關的配置工作,那麼接下來我們需要思考的問題就是 Spring 是通過什麼樣的機制來保證 Tomcat 在啓動的時候會自動調用這段代碼來進行 Spring Web 容器和 DIspatcherServlet 的創建和初始化工作。在這裏其實一般最容易想到的方式就是讓 Tomcat 單純通過反射機制來動態調用 Spring 初始化邏輯,但這樣存在的一個問題就是會將 Spring 和 Tomcat 的代碼僅僅的耦合在一起(因爲反射的邏輯是被寫死在代碼中的)。換一種思路,我們可以將需要被掃描類型的類信息寫到自己 jar 包的特定文件中,然後讓 Tomcat 來完成自動掃描並加載相關類,而這也就是 Tomcat 在 Servlet 3.x 實現的一種動態擴展方式(SPI),正是通過這種機制 Spring 可以保證在 Tomcat 啓動時自動加載並調用初始化器的相關代碼(onStartup 方法),從而完成 Spring Web 容器和 DIspatcherServlet 的創建和初始化工作。

3.2 調用 Spring Web 容器初始化器

首先,對於 Spring 整合 Spring MVC 使用的很關鍵的一個技術就是我們剛剛介紹過的 SPI 機制,通過 SPI 機制 Spring 得以在 Servlet 容器啓動的時候動態的將 Spring Web 容器的初始化器創建出來並完成容器的部分初始化工作,而對於這一點的佐證我們可以直接在 spring-web 的源碼模塊中找到如下圖一這樣的一個文件。同時我們也可以發現它的存放路徑規則、文件命名以及文件內容(下圖二)也完全滿足 SPI 的規範(存放在 resources \ META_INF \ services 路徑下,文件命名爲接口的全限定類名,內容爲實現類的全限定類名),因此我們可以更加的確定 Spring 正是通過這種 SPI 的機制完成了與 Servlet 的整合來實現 Spring MVC。

 

 接下來既然我們已經明確了 Spring 與 Servlet 整合實現 Spring MVC 的機制,因此我們就直切要點直接進入到這個 SpringServletContainerInitializer 類中,這個類實現了 ServletContainerInitializer 接口(這個接口是 Servlet 的一個接口),但這個類中正如我們下面的代碼所示其整個類只實現了一個 onStartup 方法。

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

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

                // 獲取所有的 Spring Web 應用容器初始化器
		List<WebApplicationInitializer> initializers = new LinkedList<>();
                // 當成功獲取到所有的初始化器後對它們依次遍歷
		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
			        // 過濾掉一些 servlet 容器爲我們提供的無效類
                                // 即初始化器不能爲接口也不能爲抽象類,並且應當是 WebApplicationInitializer 的子類
				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);
                // 對初始化器依次調用它們(父類 AbstractDispatcherServletInitializer)的 onStartup 方法
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

}

public void onStartup(ServletContext servletContext) throws ServletException {
        // 調用父類 AbstractContextLoaderInitializer 中的 onStartup 方法
	super.onStartup(servletContext);
        // 創建並註冊 registerDispatcherServlet
	registerDispatcherServlet(servletContext);
}

// AbstractContextLoaderInitializer.java
public void onStartup(ServletContext servletContext) throws ServletException {
        // 創建、初始化根容器並註冊容器加載監聽器
	registerContextLoaderListener(servletContext);
}

因爲上面這兩個 onStartup 方法代碼邏輯相對來說比較簡單,根據註釋就可以理解了 ,我們就再大概總結一下兩個 onStartup 方法的功能。

首先對於第一個 onStartup 方法因爲它滿足 Servlet 3.x 中對 SPI 的擴展,因此根據它的 @HandlesTypes 註解中的屬性值,Servlet 容器會將掃描到的所有 WebApplicationInitializer 抽象類的子類都添加到第一個參數 webAppInitializerClasses 的 Set 集合中(同時子類具有 @Order 註解或實現了 Order 接口,那麼這些子類還會被根據優先級進行排序),並且當如果在類路徑下沒有找到匹配的實現類時它也會通過打印日誌的方式來提醒開發者 ServletContainerInitializer 接口的相關實現類已經確實被調用了,但是沒有發現 WebApplicationInitializer 抽象類的子類。在這個方法中會對符合條件的(非接口、非抽象類並且是 WebApplicationInitializer 類型)子類通過反射調用構造方法來創建它們的實例,併爲它們註冊和配置 Servlet(比如 Spring 當中的 DispatcherServlet)、listener(比如 ContextLoaderListener)和 filter 等。

而第二個 onStartup 方法雖然調用的是父類 WebApplicationInitializer 中的 onStartup 方法,但其實它的最終調用是在子類 AbstractDispatcherServletInitializer 中的實現,而這個方法的實現的功能也比較簡單,第一部分就是調用父類的 onStartup 方法來創建並初始化配置根容器(RootWebApplicationContext),而第二部分就是創建並初始化配置 DispatcherServlet 和子容器(ServletWebApplicationContext),並對它們進行關聯。但是這裏需要注意的是,這兩個部分都是僅對容器進行了創建和部分初始化的配置,沒有對容器進行刷新操作(refrain 方法,掃描 Bean 等)。

3.3 創建根容器 

// AbstractContextLoaderInitializer.class
protected void registerContextLoaderListener(ServletContext servletContext) {
        // 創建根容器(RootApplicationContext)
	WebApplicationContext rootAppContext = createRootApplicationContext();
        // 如果成功創建根容器(外部傳入根容器配置類)
	if (rootAppContext != null) {
                // 爲根容器創建一個容器加載監聽器(ContextLoaderListener)
                // 保證後面根容器的初始化邏輯得以被調用
		ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
                // 獲取並設置根容器初始化器
		listener.setContextInitializers(getRootApplicationContextInitializers());
                // 將創建好的監聽器添加到 Servlet 容器中
		servletContext.addListener(listener);
	}
	else {
		logger.debug("No ContextLoaderListener registered, as " +
				"createRootApplicationContext() did not return an application context");
	}
}

 完成了容器初始化器的 onStartup 方法的調用後,會首先調用它父類 AbstractContextLoaderInitializer 中的 registerContextLoaderListener 方法進行根容器的創建容器加載監聽器的創建和註冊。這裏需要注意的是對於 ContextLoaderListener 的註冊,因爲這個 ContextLoaderListener 直接關乎根容器後面的初始化邏輯調用。

// AbstractAnnotationConfigDispatcherServletInitializer.class
protected WebApplicationContext createRootApplicationContext() {
        // 獲取根配置類
	Class<?>[] configClasses = getRootConfigClasses();
        // 如果存在根配置類時
	if (!ObjectUtils.isEmpty(configClasses)) {
                // 創建一個 AnnotationConfigWebApplicationContext 容器
		AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
                // 將配置類註冊到容器中
		context.register(configClasses);
                // 返回配置完成的根容器
		return context;
	}
	else {
                // 如果不存在根配置類則直接返回空
		return null;
	}
}

// 該方法由我們自己實現,將根配置類(具有 @Configuration 的 Java 類)傳入
protected abstract Class<?>[] getRootConfigClasses();

// AnnotationConfigWebApplicationContext.class
public void register(Class<?>... annotatedClasses) {
	Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified");
        // 將根配置類全部添加到容器的 annotatedClasses 集合中
	Collections.addAll(this.annotatedClasses, annotatedClasses);
}

 對於根容器的創建工作這裏會調用 createRootApplicationContext 方法,方法的邏輯也比較簡單,主要就是通過 getRootConfigClasses 方法獲取我們從外部傳入的 Java 配置類,然後在創建完根 WebApplicationContext 容器後再將相關的配置類註冊到容器中即可。再提一點,這裏面調用的 context.register 方法其實就是我們在 Spring IOC 源碼分析中經常分析到的容器註冊流程(容器初始化一般分爲 register 和 refresh 兩大部分),並且在 createRootApplicationContext 方法中我們可以看到如果外部沒有傳入配置類,那麼它是不會創建根容器的,這裏其實對應層次容器結構(父子容器),正如 Spring 官方文檔所述當我們不需要層次容器結構時,可以讓 getRootConfigClasses 方法返回 null,而將所有的 Servlet 配置類通過 getServletConfigClasses 方法返回,這樣產生的 Spring Web 容器都將是平級的關係。

3.4 創建 DispatcherServlet 和子容器

protected void registerDispatcherServlet(ServletContext servletContext) {
        // 獲取 DispatcherServlet 將要註冊的名稱(默認爲 dispatcher)
	String servletName = getServletName();
	Assert.hasLength(servletName, "getServletName() must not return null or empty");

        // 創建子容器(ServletApplicationContext)
	WebApplicationContext servletAppContext = createServletApplicationContext();
	Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");

        // 創建一個綁定當前 WebApplicationContext 的 DispatcherServlet 實例
	FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);
	Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
        // 添加容器初始化器
	dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());

        // 將創建完成的 DispatcherServlet 實例添加到 WebApplicationContext 中
	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.");
	}

        // 設置啓動順序(同 xml 文件中配置)
	registration.setLoadOnStartup(1);
        // 添加映射關係(getServletMappings 方法由我們自己實現,重寫方法將 Servlet 映射關係傳入)
	registration.addMapping(getServletMappings());
        // 設置是否支持異步
	registration.setAsyncSupported(isAsyncSupported());

        // 獲取過濾器(getServletFilters 方法也由我們自己實現,重寫方法將 Filter 傳入)
	Filter[] filters = getServletFilters();
	if (!ObjectUtils.isEmpty(filters)) {
		for (Filter filter : filters) {
                        // 註冊過濾器
			registerServletFilter(servletContext, filter);
		}
	}

        // 可選再進行一次自定義註冊(空實現)
	customizeRegistration(registration);
}

// AbstractAnnotationConfigDispatcherServletInitializer.class
protected WebApplicationContext createServletApplicationContext() {
        // 創建一個 AnnotationConfigWebApplicationContext 容器
	AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
        // 獲取子容器配置類(當不需要層次結構容器時該方法將返回 null)
	Class<?>[] configClasses = getServletConfigClasses();
        // 如果存在子容器配置類就將其註冊到容器中
	if (!ObjectUtils.isEmpty(configClasses)) {
		context.register(configClasses);
	}
        // 將創建好的子容器返回
	return context;
}

// 該方法由我們自己實現,將子配置類(具有 @Configuration 的 Java 類)傳入
protected abstract Class<?>[] getServletConfigClasses();

protected FrameworkServlet createDispatcherServlet(WebApplicationContext servletAppContext) {
        // 創建一個綁定當前 WebApplicationContext 的 DispatcherServlet
	return new DispatcherServlet(servletAppContext);
}

 當 onStartup 方法調用父類中的 registerContextLoaderListener 方法完成根容器的創建、初始化以及容器加載監聽器的註冊後,接着它會調用 registerDispatcherServlet 方法來創建、初始化子容器和 DispatcherServlet,具體的流程大概如下:

(1)獲取 DispatcherServlet 名稱;

(2)通過外部傳入的配置類(getServletConfigClasses)創建 Spring web 子容器(WebApplicationContext);

(3)創建一個綁定當前 Spring web 子容器的 DispatcherServlet 實例,並讓 DispatcherServlet 持有子容器引用

(4)爲剛剛創建的 DispatcherServlet 添加容器初始化器(ContextInitializer),完成後將其註冊到 Servlet 容器中;

(5)對剛剛創建的 DispatcherServlet 進行相關的參數配置;

(6)獲取外部傳入的 filter(getServletFilters)並將其註冊給 Servlet 容器;

(7)調用 customizeRegistration 方法給外部一次自定義註冊的機會;

3.5 初始化根容器

// ContextLoaderListener.calss
public void contextInitialized(ServletContextEvent event) {
	initWebApplicationContext(event.getServletContext());
}

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
        // 判斷當前 Servlet 容器中是否已經設置了根容器,如果已經設置了則拋異常
	if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
		throw new IllegalStateException(
				"Cannot initialize context because there is already a root application context present - " +
				"check whether you have multiple ContextLoader* definitions in your web.xml!");
	}

	servletContext.log("Initializing Spring root WebApplicationContext");
	Log logger = LogFactory.getLog(ContextLoader.class);
	if (logger.isInfoEnabled()) {
		logger.info("Root WebApplicationContext: initialization started");
	}
	long startTime = System.currentTimeMillis();

	try {
		// 將 Spring web 容器(這裏是指根容器)存儲在本地實例變量中來保證它在 ServletContext 關閉時可用
		if (this.context == null) {
                        // 如果當前容器不存在則創建
			this.context = createWebApplicationContext(servletContext);
		}
		if (this.context instanceof ConfigurableWebApplicationContext) {
                        // 對父容器進行類型強轉並賦值給變量 cwac
			ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
                        // 如果當前容器還未被激活(未被刷新)
			if (!cwac.isActive()) {
				// 如果當前容器未設置父容器
				if (cwac.getParent() == null) {
					// 上下文實例在沒有顯式設置父實例的情況下被注入則嘗試加載它的父容器
                                        // 對於沒有特殊需要的根容器一般返回 null
					ApplicationContext parent = loadParentContext(servletContext);
                                        // 爲當前容器設置獲取到的父容器 
					cwac.setParent(parent);
				}
                                // 配置並刷新當前 Spring Web容器
				configureAndRefreshWebApplicationContext(cwac, servletContext);
			}
		}	    
                // 設置當前 Spring Web 容器爲 Servlet 的根容器
                // 當初始化子容器的時候使用到這個值
                servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
                // 獲取類加載器
		ClassLoader ccl = Thread.currentThread().getContextClassLoader();
		if (ccl == ContextLoader.class.getClassLoader()) {
			currentContext = this.context;
		}
		else if (ccl != null) {
			currentContextPerThread.put(ccl, this.context);
		}

		if (logger.isInfoEnabled()) {
			long elapsedTime = System.currentTimeMillis() - startTime;
			logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
		}

		return this.context;
	}
	catch (RuntimeException | Error ex) {
		logger.error("Context initialization failed", ex);
                servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
		throw ex;
	}
}

對於根容器的初始化工作主要是通過 ContextLoaderListener 來完成的,剛剛在創建根容器的過程中我們創建了一個 ContextLoaderListener 實例,並將根容器保存在實例當中,最終將其註冊到 Servlet 容器中。同時 ContextLoaderListener 實現了 ServletContextListener 接口這也就意味着它能夠監聽到 Servlet 容器的生命週期,當 Servlet 容器啓動 web 應用時會調用到其中的 contextInitialized 方法,也就是在這個方法中通過調用 initWebApplicationContext 完成了對根容器的初始化工作。

上面對於 initWebApplicationContext 方法的註釋已經解釋的比較清楚了,需要注意的點就是它最終通過調用 configureAndRefreshWebApplicationContext 方法(這個方法最核心的就是其最後調用 refresh 方法來刷新容器,也就是從這裏進入到了 Spring IOC 的相關邏輯中,完成了對於根容器中相關 Bean 的實例化等工作)來實現根容器的配置和刷新,同時它在完成根容器的初始化工作後會通過設置 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 的屬性值來將根容器的引用進行保存,這主要是用於後面子容器的初始化工作。

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
	if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
		// 如果應用程序上下文 id 仍然設置爲其原始默認值
		// 在這裏根據可用信息分配更有用的 id
		String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
		if (idParam != null) {
			wac.setId(idParam);
		}
		else {
			// 生成默認id
			wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
					ObjectUtils.getDisplayString(sc.getContextPath()));
		}
	}

        // 將 Spring web 容器與 Servlet 容器進行綁定
	wac.setServletContext(sc);
        // 獲取 Servlet 容器的初始化參數
	String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
	if (configLocationParam != null) {
                // 將 Servlet 容器配置參數設置給 Spring Web 容器
		wac.setConfigLocation(configLocationParam);
	}

	// 在任何情況下,當上下文被刷新時,都會調用 Spring Web 環境的 initPropertySources 方法
	// 確保 servlet 屬性源在 refresh 方法之前的任何後續處理或初始化中都處於適當的位置
	ConfigurableEnvironment env = wac.getEnvironment();
	if (env instanceof ConfigurableWebEnvironment) {
		((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
	}

        // 處理自定義的 ContextInitializer
	customizeContext(sc, wac);
        // 刷新容器
	wac.refresh();
}

3.6 初始化子容器並關聯父子容器

經過上面的幾個步驟我們已經完成了根容器、子容器和 DispatcherServlet 的創建工作,並且已經通過 Servlet 監聽器完成了根容器的初始化工作,那麼接下來要做的就是最後兩步:初始化子容器並關聯父子容器。首先如果想要搞清楚這部分的調用邏輯,就需要先從 DispatcherServlet 的繼承結構說起。

通過上圖繼承樹中的藍色實現我們可以發現,其實 DispatcherServlet 的本質就是一個 HttpServlet,同時你也可以發現我們上面調用到的很多方法其實也都是在 DIspatcherServlet 的父容器中進行實現的。那麼言歸正傳,如果 DispatcherServlet 的本質就是一個 HttpServlet,那根據 Servlet 的規範就會在初始化這個 Servlet 的時候調用到它的 init 方法,又因爲我們在創建 DispatcherServlet 的時候就已經注入了一個 WebApplicationContext 實例(子容器),因此我們就可以推斷出對於子容器的初始化工作應當是在 DispatcherServlet 被調用 init 初始化方法時進行的。

但是需要注意的是,對於使用 Java 註解進行配置和使用 xml 文件進行配置都會調用到這個方法中,因爲篇幅的原因本文不會過多的涉及 xml 配置的 DIspatcherServlet 進入到該方法時的邏輯,僅會在顯著區別的邏輯處進行註釋。

// HttpServletBean.class
public final void init() throws ServletException {

	// 使用 init 參數設置 Bean 屬性
	PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
	if (!pvs.isEmpty()) {
		try {
			BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
			ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
			bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
			initBeanWrapper(bw);
			bw.setPropertyValues(pvs, true);
		}
		catch (BeansException ex) {
			throw ex;
		}
	}
	// 初始化 ServletBean
	initServletBean();
}

// FrameworkServlet.class
protected final void initServletBean() throws ServletException {
	long startTime = System.currentTimeMillis();
	try {
                // 初始化 WebApplicationContext 容器
		this.webApplicationContext = initWebApplicationContext();
                // 初始化 FrameworkServlet(空實現)
		initFrameworkServlet();
	}
	catch (ServletException | RuntimeException ex) {
		throw ex;
	}
}

當調用到 DispatcherServlet 的 init 方法後,首先會使用 init 中配置的參數(Java 註解或 xml 中設置的 init 屬性)對 Bean 的屬性進行賦值,然後會調用 initServletBean 方法來初始化 ServletBean,但是需要注意的是這個 init 方法是在 DispatcherServlet 的父類 HttpServletBean 中實現的,而它所調用的 initServletBean 方法又是由 DispatcherServlet 的直接父類、HttpServletBean的子類 FrameworkServlet 來實現的(這裏其實是使用了模板方法設計模式,即父類只負責提供規範,具體的內容交由子類來實現)。而進入到 initServletBean 方法後纔會調用到關鍵的 initWebApplicationContext 方法來初始化 Spring web 容器。

// FrameworkServlet.class
protected WebApplicationContext initWebApplicationContext() {
        // 獲取 Spring web 根容器
	WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        // 創建子容器變量且初始值爲 null
	WebApplicationContext wac = null;

        // 如果當前 DispatcherServlet 已經綁定了 webApplicationContext 容器
        // 那說明在構建時已經向 DispatcherServlet 中注入了 webApplicationContext 容器
        // 這裏主要是用來區分註解初始化和 xml 文件初始化(下面詳細分析)
	if (this.webApplicationContext != null) {
		// 因爲已經綁定,所以直接將綁定的 webApplicationContext 賦給 wac 變量
		wac = this.webApplicationContext;
		if (wac instanceof ConfigurableWebApplicationContext) {
			ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
                        // 如果當前 webApplicationContext 容器還未刷新(未調用 refresh 方法)
			if (!cwac.isActive()) {
				if (cwac.getParent() == null) {
					// 容器實例在沒有顯式父實例的情況下被注入
					// 如果根容器存在,就使用根容器作爲其父容器(當然根容器也可能爲空)
					cwac.setParent(rootContext);
				}
                                // 調用方法配置容器並調用其 refresh 方法進行刷新
                                // 同時讓容器持有 Servlet 容器的引用
				configureAndRefreshWebApplicationContext(cwac);
			}
		}
	}
	if (wac == null) {
		// 如果進入到此判斷意味着在構建時沒有爲其注入 webApplicationContext
		// 那麼先查看在 servlet 容器中是否已經註冊了 webApplicationContext(檢查 contextAttribute 屬性值)
		// 如果存在則假定已經設置了父容器(如果存在根容器)
		// 並且已經執行了所有的初始化操作,例如設置容器 id 等
		wac = findWebApplicationContext();
	}
	if (wac == null) {
		// 如果進到這個判斷說明還是沒有爲 DispatcherServlet 註冊 webApplicationContext 實例(一般使用 xml 配置 DispatcherServlet 會走到這裏)
                // 那麼就直接調用 createWebApplicationContext 方法創建一個 webApplicationContext 實例
                // 在這個方法中同時會將傳入的根容器作爲新創建容器的父容器
		wac = createWebApplicationContext(rootContext);
	}

	if (!this.refreshEventReceived) {
		// 如果當前 Spring web 容器不是具有刷新支持的 ConfigurableApplicationContext
		// 或者在構造時注入的容器已經被刷新
		// 在這裏手動觸發初始 onRefresh 方法
		synchronized (this.onRefreshMonitor) {
			onRefresh(wac);
		}
	}

	if (this.publishContext) {
		// 將當前 Spring web 容器(webApplicationContext)作爲 servlet 容器的屬性發布
		String attrName = getServletContextAttributeName();
                // 也就是在這裏將 webApplicationContext 暴露給了 Servlet 容器(便於其它的 DispatcherServlet 獲取)
		getServletContext().setAttribute(attrName, wac);
	}

	return wac;
}

 這裏對於 initWebApplicationContext 方法實現邏輯概括一下:

(1)首先獲取 Servlet 容器中的 根 Spring web 容器

(2)判斷當前的 DispatcherServlet 中的 webApplicationContext 屬性是否已經被賦值,如果被賦值則說明在前面構造 DispatcherServlet 的過程中就已經完成了 webApplicationContext 的創建、初始化和註冊工作(即上面 3.2 的情況),換句話說此時的配置方式是使用 Java 註解配置的;

(3)如果已經確定了當前 DispatcherServlet 已經完成了 webApplicationContext 的註冊,那麼就直接獲取已經註冊了的 webApplicationContext 實例,首先判斷它是否已經進行了容器刷新,如果沒有且其當前不存在父容器,那麼就先將根容器設置爲其父容器(如果根容器存在的話),然後再對它進行必要的配置和回調後,最後調用它的 refresh 方法來刷新激活容器(代碼見下方),反之如果存在父容器那麼就只進行容器的配置和激活刷新;

(4)那如果當前 DispatcherServlet 沒有註冊 webApplicationContext 實例的話,就需要分兩種情況來討論(但這兩種情況一般都是針對使用 xml 進行配置時),一種是當前 DispatcherServlet 的 contextAttribute 屬性值已經被設置了(可以理解爲已經預定好了 webApplicationContext ,但還沒有進行註冊),那麼這個時候我們就可以直接通過 WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName) 方法根據容器名來獲取指定的 webApplicationContext 實例,然後再返回給調用者就可以了(以上邏輯主要集中在 findWebApplicationContext 方法中,代碼如下);

另一種情況是當前 DispatcherServlet 的 contextAttribute 屬性也未被賦值(使用 xml 配置就會這樣),那麼此時就只能現場創建一個 webApplicationContext 實例了(createWebApplicationContext 方法代碼實現如下);

(5)然後再進行判斷,如果剛剛獲取到的 webApplicationContext 實例不是具有刷新支持的 ConfigurableApplicationContext 或者在構造時注入的容器已經被刷新則手動觸發初始 onRefresh 方法;

(6)最後如果當前 DispatcherServlet 支持將 webApplicationContext 作爲 Servlet 容器屬性發布發佈的話,那麼就將剛剛獲取到的 webApplicationContext 實例存儲到 Servlet 容器的屬性中,且存儲時的鍵爲 SERVLET_CONTEXT_PREFIX + servlet name ,值爲 webApplicationContext 實例,到這裏也就完成了子容器的初始化和父子容器關聯的工作了; 

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
	if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
		// 如果容器 id 仍然設置爲其原始默認值
		// 則根據可用信息設置更有用的 id
		if (this.contextId != null) {
			wac.setId(this.contextId);
		}
		else {
			// 使用默認 id
			wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
						ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
		}
	}
        
        // 將 Servlet 容器註冊到當前 Spring web 容器中
	wac.setServletContext(getServletContext());
        // 獲取 Servlet 容器中的配置信息
	wac.setServletConfig(getServletConfig());
        // 設置命名空間
	wac.setNamespace(getNamespace());
        // 添加監聽器
	wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

	ConfigurableEnvironment env = wac.getEnvironment();
	if (env instanceof ConfigurableWebEnvironment) {
		((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
	}

        // 調用生命週期回調
	postProcessWebApplicationContext(wac);
        // 應用容器的所有初始化器
	applyInitializers(wac);
        // 刷新容器
	wac.refresh();
}

// 查看在 servlet 容器中是否已經註冊了 webApplicationContext
protected WebApplicationContext findWebApplicationContext() {
        // 獲取 DispatcherServlet 實例的 contextAttribute 屬性值
	String attrName = getContextAttribute();
	if (attrName == null) {
		return null;
	}
        // 確定已經註冊後直接將註冊的 webApplicationContext 返回給調用者
	WebApplicationContext wac =
			WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
	if (wac == null) {
		throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
	}
	return wac;
}

// 創建一個 WebApplicationContext 實例並返回 
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
	Class<?> contextClass = getContextClass();
	if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
		throw new ApplicationContextException(
				"Fatal initialization error in servlet with name '" + getServletName() +
				"': custom WebApplicationContext class [" + contextClass.getName() +
				"] is not of type ConfigurableWebApplicationContext");
	}
	ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

	wac.setEnvironment(getEnvironment());
        // 設置根容器爲父容器
	wac.setParent(parent);
	String configLocation = getContextConfigLocation();
	if (configLocation != null) {
		wac.setConfigLocation(configLocation);
	}
        // 配置並激活刷新當前容器
	configureAndRefreshWebApplicationContext(wac);
	
	return wac;
}

四、方法調用時序圖(UML 序列圖)

4.1 父子容器和 DispatcherServlet 的創建

 

4.2 父子容器初始化

 

五、內容總結

本文主要完成了對於 Spring 整合 SpringMVC 源碼的一些分析,大概探究了 Spring 是怎樣在 Tomcat 啓動時完成 Spring web 父子容器的創建和初始化工作。

從沒有白費的努力,也沒有碰巧的成功。只要認真對待生活,終有一天,你的每一份努力,都將絢爛成花。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章