精盡Spring MVC原始碼分析 - WebApplicationContext 容器的初始化

月圓吖發表於2020-12-14

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

Spring 版本:5.2.4.RELEASE

隨著 Spring BootSpring Cloud 在許多中大型企業中被普及,可能你已經忘記當年經典的 Servlet + Spring MVC 的組合,是否還記得那個 web.xml 配置檔案。在開始本文之前,請先拋開 Spring Boot 到一旁,回到從前,一起來看看 Servlet 是怎麼和 Spring MVC 整合,怎麼來初始化 Spring 容器的,在開始閱讀本文之前,最好有一定的 Servlet 和 Spring IOC 容器方面的知識,比較容易理解

概述

在開始看具體的原始碼實現之前,我們先一起來看看現在“陌生”的 web.xml 檔案,可以檢視我的另一篇 MyBatis 使用手冊 文件中整合 Spring小節涉及到的 web.xml 的檔案,部分內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">
    <display-name>Archetype Created Web Application</display-name>

    <!-- 【1】 Spring 配置 -->
    <!-- 在容器(Tomcat、Jetty)啟動時會被 ContextLoaderListener 監聽到,
         從而呼叫其 contextInitialized() 方法,初始化 Root WebApplicationContext 容器 -->
    <!-- 宣告 Spring Web 容器監聽器 -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <!-- Spring 和 MyBatis 的配置檔案 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mybatis.xml</param-value>
    </context-param>

    <!-- 【2】 Spring MVC 配置 -->
    <!-- 1.SpringMVC 配置 前置控制器(SpringMVC 的入口)
         DispatcherServlet 是一個 Servlet,所以可以配置多個 DispatcherServlet -->
    <servlet>
        <!-- 在 DispatcherServlet 的初始化過程中,框架會在 web 應用 的 WEB-INF 資料夾下,
             尋找名為 [servlet-name]-servlet.xml 的配置檔案,生成檔案中定義的 Bean. -->
        <servlet-name>SpringMVC</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- 配置需要載入的配置檔案 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-mvc.xml</param-value>
        </init-param>
        <!-- 程式執行時從 web.xml 開始,載入順序為:context-param -> Listener -> Filter -> Structs -> Servlet
             設定 web.xml 檔案啟動時載入的順序(1 代表容器啟動時首先初始化該 Servlet,讓這個 Servlet 隨 Servlet 容器一起啟動)
             load-on-startup 是指這個 Servlet 是在當前 web 應用被載入的時候就被建立,而不是第一次被請求的時候被建立  -->
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <!-- 這個 Servlet 的名字是 SpringMVC,可以有多個 DispatcherServlet,是通過名字來區分的
             每一個 DispatcherServlet 有自己的 WebApplicationContext 上下文物件,同時儲存在 ServletContext 中和 Request 物件中
             ApplicationContext(Spring 容器)是 Spring 的核心
             Context 我們通常解釋為上下文環境,Spring 把 Bean 放在這個容器中,在需要的時候,可以 getBean 方法取出-->
        <servlet-name>SpringMVC</servlet-name>
        <!-- Servlet 攔截匹配規則,可選配置:*.do、*.action、*.html、/、/xxx/* ,不允許:/* -->
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

【1】 處,配置了 org.springframework.web.context.ContextLoaderListener 物件,它實現了 Servlet 的 javax.servlet.ServletContextListener 介面,能夠監聽 ServletContext 物件的生命週期,也就是監聽 Web 應用的生命週期,當 Servlet 容器啟動或者銷燬時,會觸發相應的 ServletContextEvent 事件,ContextLoaderListener 監聽到啟動事件,則會初始化一個Root Spring WebApplicationContext 容器,監聽到銷燬事件,則會銷燬該容器

【2】 處,配置了 org.springframework.web.servlet.DispatcherServlet 物件,它繼承了 javax.servlet.http.HttpServlet 抽象類,也就是一個 Servlet。Spring MVC 的核心類,處理請求,會初始化一個屬於它的 Spring WebApplicationContext 容器,並且這個容器是以 【1】 處的 Root 容器作為父容器

  • 為什麼有了 【2】 建立了容器,還需要 【1】 建立 Root 容器呢?因為可以配置多個 【2】 呀,當然,實際場景下,不太會配置多個 【2】 ?
  • 再總結一次,【1】【2】 分別會建立其對應的 Spring WebApplicationContext 容器,並且它們是父子容器的關係

Root WebApplicationContext 容器

概述web.xml中,我們已經看到,Root WebApplicationContext 容器的初始化,通過 ContextLoaderListener 來實現。在 Servlet 容器啟動時,例如 Tomcat、Jetty 啟動後,則會被 ContextLoaderListener 監聽到,從而呼叫 contextInitialized(ServletContextEvent event) 方法,初始化 Root WebApplicationContext 容器

而 ContextLoaderListener 的類圖如下:

ContextLoader

ContextLoaderListener

org.springframework.web.context.ContextLoaderListener 類,實現 javax.servlet.ServletContextListener 介面,繼承 ContextLoader 類,實現 Servlet 容器啟動和關閉時,分別初始化和銷燬 WebApplicationContext 容器,程式碼如下:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

	public ContextLoaderListener() {
	}

    /**
     * As of Spring 3.1, supports injecting the root web application context
     */
	public ContextLoaderListener(WebApplicationContext context) {
		super(context);
	}


	/**
	 * Initialize the root web application context.
	 */
	@Override
	public void contextInitialized(ServletContextEvent event) {
        // <1> 初始化 Root WebApplicationContext
		initWebApplicationContext(event.getServletContext());
	}


	/**
	 * Close the root web application context.
	 */
	@Override
	public void contextDestroyed(ServletContextEvent event) {
        // <2> 銷燬 Root WebApplicationContext
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}

}
  1. 監聽到 Servlet 容器啟動事件,則呼叫父類 ContextLoader 的 initWebApplicationContext(ServletContext servletContext) 方法,初始化 WebApplicationContext 容器
  2. 監聽到 Servlet 銷燬啟動事件,則呼叫父類 ContextLoader 的 closeWebApplicationContext(ServletContext servletContext) 方法,銷燬 WebApplicationContext 容器

ContextLoader

org.springframework.web.context.ContextLoader 類,真正實現初始化和銷燬 WebApplicationContext 容器的邏輯的類

靜態程式碼塊
public class ContextLoader {

	/**
	 * Name of the class path resource (relative to the ContextLoader class)
	 * that defines ContextLoader's default strategy names.
	 */
	private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties";

    /**
     * 預設的配置 Properties 物件
     */
	private static final Properties defaultStrategies;

	static {
		// Load default strategy implementations from properties file.
		// This is currently strictly internal and not meant to be customized by application developers.
		try {
			ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class);
			defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
		}
		catch (IOException ex) {
			throw new IllegalStateException("Could not load 'ContextLoader.properties': " + ex.getMessage());
		}
	}
}

ContextLoader.properties 中,讀取預設的配置 Properties 物件。實際上,正如 Load default strategy implementations from properties file. This is currently strictly internal and not meant to be customized by application developers. 所註釋,這是一個應用開發者無需關心的配置,而是 Spring 框架自身所定義的

開啟來該檔案瞅瞅,程式碼如下:

# Default WebApplicationContext implementation class for ContextLoader.
# Used as fallback when no explicit context implementation has been specified as context-param.
# Not meant to be customized by application developers.

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

這意味著什麼呢?如果我們沒有在 <context-param /> 標籤中指定 WebApplicationContext,則預設使用 XmlWebApplicationContext 類,我們在使用 Spring 的過程中一般情況下不會主動指定

構造方法
public class ContextLoader {
    
    /**
	 * Name of servlet context parameter (i.e., {@value}) that can specify the
	 * config location for the root context, falling back to the implementation's default otherwise.
	 * @see org.springframework.web.context.support.XmlWebApplicationContext#DEFAULT_CONFIG_LOCATION
	 */
	public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
    
	/** Map from (thread context) ClassLoader to corresponding 'current' WebApplicationContext. */
	private static final Map<ClassLoader, WebApplicationContext> currentContextPerThread = new ConcurrentHashMap<>(1);

	/** The 'current' WebApplicationContext, if the ContextLoader class is deployed in the web app ClassLoader itself. */
	@Nullable
	private static volatile WebApplicationContext currentContext;


	/** The root WebApplicationContext instance that this loader manages. */
	@Nullable
	private WebApplicationContext context;

	/**
	 * Create a new {@code ContextLoader} that will create a web application context
	 * based on the "contextClass" and "contextConfigLocation" servlet context-params.
	 * See class-level documentation for details on default values for each.
	 */
	public ContextLoader() {
	}

	/**
	 * Create a new {@code ContextLoader} with the given application context.
     * This constructor is useful in Servlet 3.0+ environments where instance-based
	 * registration of listeners is possible through the {@link ServletContext#addListener} API.
	 */
	public ContextLoader(WebApplicationContext context) {
		this.context = context;
	}
    
    // ... 省略其他相關配置屬性
}
  • 概述web.xml 檔案中可以看到定義的 contextConfigLocation 引數為 spring-mybatis.xml 配置檔案路徑
  • currentContextPerThread:用於儲存當前 ClassLoader 類載入器與 WebApplicationContext 物件的對映關係
  • currentContext:如果當前執行緒的類載入器就是 ContextLoader 類所在的類載入器,則該屬性用於儲存 WebApplicationContext 物件
  • context:WebApplicationContext 例項物件

關於類載入器涉及到 JVM 的“雙親委派機制”,在《精盡MyBatis原始碼分析 - 基礎支援層》 有簡單的講述到,可以參考一下

initWebApplicationContext

initWebApplicationContext(ServletContext servletContext) 方法,初始化 WebApplicationContext 物件,程式碼如下:

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    // <1> 若已經存在 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 對應的 WebApplicationContext 物件,則丟擲 IllegalStateException 異常。
    // 例如,在 web.xml 中存在多個 ContextLoader
    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!");
    }

    // <2> 列印日誌
    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 {
        // Store context in local instance variable, to guarantee that
        // it is available on ServletContext shutdown.
        if (this.context == null) {
            // <3> 初始化 context ,即建立 context 物件
            this.context = createWebApplicationContext(servletContext);
        }
        // <4> 如果是 ConfigurableWebApplicationContext 的子類,如果未重新整理,則進行配置和重新整理
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
            if (!cwac.isActive()) { // <4.1> 未重新整理( 啟用 )
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) { // <4.2> 無父容器,則進行載入和設定。
                    // The context instance was injected without an explicit parent ->
                    // determine parent for root web application context, if any.
                    ApplicationContext parent = loadParentContext(servletContext);
                    cwac.setParent(parent);
                }
                // <4.3> 配置 context 物件,並進行重新整理
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }
        // <5> 記錄在 servletContext 中
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);

        // <6> 記錄到 currentContext 或 currentContextPerThread 中
        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");
        }

        // <7> 返回 context
        return this.context;
    }
    catch (RuntimeException | Error ex) {
        logger.error("Context initialization failed", ex);
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
        throw ex;
    }
}
  1. 若 ServletContext(Servlet 的上下文)已存在 Root WebApplicationContext 物件,則丟擲異常,因為不能再初始化該物件

  2. 列印日誌,在啟動 SSM 專案的時候,是不是都會看到這個日誌“Initializing Spring root WebApplicationContext”

  3. 如果context為空,則呼叫createWebApplicationContext(ServletContext sc)方法,初始化一個 Root WebApplicationContext 物件,方法如下:

    protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
        // <1> 獲得 context 的類(預設情況是從 ContextLoader.properties 配置檔案讀取的,為 XmlWebApplicationContext)
        Class<?> contextClass = determineContextClass(sc);
        // <2> 判斷 context 的類,是否符合 ConfigurableWebApplicationContext 的型別
        if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
            throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                    "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
        }
        // <3> 建立 context 的類的物件
        return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
    }
    
  4. 如果是 ConfigurableWebApplicationContext 的子類,並且未重新整理,則進行配置和重新整理

    1. 如果未重新整理(啟用),預設情況下,是符合這個條件的,所以會往下執行
    2. 如果無父容器,則進行載入和設定。預設情況下,loadParentContext(ServletContext servletContext) 方法返回一個空物件,也就是沒有父容器了
    3. 呼叫configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc)方法,配置context物件,並進行重新整理
  5. context物件儲存在 ServletContext 中

  6. context物件設定到currentContext或者currentContextPerThread物件中,差異就是類載入器是否相同,具體用途目前不清楚?

  7. 返回已經初始化的context物件

configureAndRefreshWebApplicationContext

configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) 方法,配置 ConfigurableWebApplicationContext 物件,並進行重新整理,方法如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    // <1> 如果 wac 使用了預設編號,則重新設定 id 屬性
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        // 情況一,使用 contextId 屬性
        String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
        if (idParam != null) {
            wac.setId(idParam);
        }
        else { // 情況二,自動生成
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(sc.getContextPath()));
        }
    }

    // <2>設定 context 的 ServletContext 屬性
    wac.setServletContext(sc);
    // <3> 設定 context 的配置檔案地址
    String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
    if (configLocationParam != null) {
        wac.setConfigLocation(configLocationParam);
    }

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
    }

    // <4> 對 context 進行定製化處理
	customizeContext(sc, wac);
	// <5> 重新整理 context ,執行初始化
	wac.refresh();
}
  1. 如果 wac 使用了預設編號,則重新設定 id 屬性。預設情況下,我們不會對 wac 設定編號,所以會執行進去。而實際上,id 的生成規則,也分成使用 contextId<context-param /> 標籤中由使用者配置,和自動生成兩種情況。? 預設情況下,會走第二種情況

  2. 設定 wac 的 ServletContext 屬性

  3. 【關鍵】設定 context 的配置檔案地址。例如我們在概述中的 web.xml 中所看到的

    <!-- Spring 和 MyBatis 的配置檔案 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mybatis.xml</param-value>
    </context-param>
    
  4. wac 進行定製化處理,暫時忽略

  5. 【關鍵】觸發 wac 的重新整理事件,執行初始化。此處,就會進行一些的 Spring 容器的初始化工作,涉及到 Spring IOC 相關內容

closeWebApplicationContext

closeWebApplicationContext(ServletContext servletContext) 方法,關閉 WebApplicationContext 容器物件,方法如下:

public void closeWebApplicationContext(ServletContext servletContext) {
    servletContext.log("Closing Spring root WebApplicationContext");
    try {
        // 關閉 context
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ((ConfigurableWebApplicationContext) this.context).close();
        }
    }
    finally {
        // 移除 currentContext 或 currentContextPerThread
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = null;
        }
        else if (ccl != null) {
            currentContextPerThread.remove(ccl);
        }
        // 從 ServletContext 中移除
        servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
    }
}

在 Servlet 容器銷燬時被呼叫,用於關閉 WebApplicationContext 物件,以及清理相關資源物件

Servlet WebApplicationContext 容器

概述web.xml中,我們已經看到,除了會初始化一個 Root WebApplicationContext 容器外,還會往 Servlet 容器的 ServletContext 上下文中注入一個 DispatcherServlet 物件,初始化該物件的過程也會初始化一個 Servlet WebApplicationContext 容器

DispatcherServlet 的類圖如下:

DispatcherServlet

可以看到 DispatcherServlet 是一個 Servlet 物件,在注入至 Servlet 容器會呼叫其 init 方法,完成一些初始化工作

  • HttpServletBean ,負責將 ServletConfig 設定到當前 Servlet 物件中,它的 Java doc:

    /**
     * Simple extension of {@link javax.servlet.http.HttpServlet} which treats
     * its config parameters ({@code init-param} entries within the
     * {@code servlet} tag in {@code web.xml}) as bean properties.
     */
    
  • FrameworkServlet ,負責初始化 Spring Servlet WebApplicationContext 容器,同時該類覆寫了 doGet、doPost 等方法,並將所有型別的請求委託給 doService 方法去處理,doService 是一個抽象方法,需要子類實現,它的 Java doc:

    /**
     * Base servlet for Spring's web framework. Provides integration with
     * a Spring application context, in a JavaBean-based overall solution.
     */
    
  • DispatcherServlet ,負責初始化 Spring MVC 的各個元件,以及處理客戶端的請求,協調各個元件工作,它的 Java doc:

    /**
     * Central dispatcher for HTTP request handlers/controllers, e.g. for web UI controllers
     * or HTTP-based remote service exporters. Dispatches to registered handlers for processing
     * a web request, providing convenient mapping and exception handling facilities.
     */
    

每一層的 Servlet 實現類,負責執行相應的邏輯,條理清晰,我們逐個來看

HttpServletBean

org.springframework.web.servlet.HttpServletBean 抽象類,實現 EnvironmentCapable、EnvironmentAware 介面,繼承 HttpServlet 抽象類,負責將 ServletConfig 整合到 Spring 中

構造方法
public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {

	@Nullable
	private ConfigurableEnvironment environment;

	/**
	 * 必須配置的屬性的集合,在 {@link ServletConfigPropertyValues} 中,會校驗是否有對應的屬性
	 * 預設為空
	 */
	private final Set<String> requiredProperties = new HashSet<>(4);

	protected final void addRequiredProperty(String property) {
		this.requiredProperties.add(property);
	}

    /**
     * 實現了 EnvironmentAware 介面,自動注入 Environment 物件
     */
	@Override
	public void setEnvironment(Environment environment) {
		Assert.isInstanceOf(ConfigurableEnvironment.class, environment, "ConfigurableEnvironment required");
		this.environment = (ConfigurableEnvironment) environment;
	}

    /**
     * 實現了 EnvironmentAware 介面,返回 Environment 物件
     */
	@Override
	public ConfigurableEnvironment getEnvironment() {
		if (this.environment == null) {
            // 如果 Environment 為空,則建立 StandardServletEnvironment 物件
			this.environment = createEnvironment();
		}
		return this.environment;
	}

	/**
	 * Create and return a new {@link StandardServletEnvironment}.
	 */
	protected ConfigurableEnvironment createEnvironment() {
		return new StandardServletEnvironment();
	}
}

關於 xxxAware介面,在 Spring 初始化該 Bean 的時候會呼叫其setXxx方法來注入一個物件,本文暫不分析

init方法

init()方法,重寫 GenericServlet 中的方法,負責將 ServletConfig 設定到當前 Servlet 物件中,方法如下:

@Override
public final void init() throws ServletException {
    // Set bean properties from init parameters.
    // <1> 解析 <init-param /> 標籤,封裝到 PropertyValues pvs 中
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            // <2.1> 將當前的這個 Servlet 物件,轉化成一個 BeanWrapper 物件。從而能夠以 Spring 的方式來將 pvs 注入到該 BeanWrapper 物件中
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
            // <2.2> 註冊自定義屬性編輯器,一旦碰到 Resource 型別的屬性,將會使用 ResourceEditor 進行解析
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
            // <2.3> 空實現,留給子類覆蓋,目前沒有子類實現
            initBeanWrapper(bw);
            // <2.4> 以 Spring 的方式來將 pvs 注入到該 BeanWrapper 物件中
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            if (logger.isErrorEnabled()) {
                logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            }
            throw ex;
        }
    }
    // Let subclasses do whatever initialization they like.
    // 交由子類去實現,檢視 FrameworkServlet#initServletBean() 方法
    initServletBean();
}
  1. 解析 Servlet 配置的 <init-param /> 標籤,封裝成 PropertyValues pvs 物件。其中,ServletConfigPropertyValues 是 HttpServletBean 的私有靜態類,繼承 MutablePropertyValues 類,ServletConfig 的 封裝實現類,該類的程式碼如下:

    private static class ServletConfigPropertyValues extends MutablePropertyValues {
        public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties) throws ServletException {
            // 獲得缺失的屬性的集合
            Set<String> missingProps = (!CollectionUtils.isEmpty(requiredProperties) ? new HashSet<>(requiredProperties) : null);
    
            // <1> 遍歷 ServletConfig 的初始化引數集合,新增到 ServletConfigPropertyValues 中,並從 missingProps 移除
            Enumeration<String> paramNames = config.getInitParameterNames();
            while (paramNames.hasMoreElements()) {
                String property = paramNames.nextElement();
                Object value = config.getInitParameter(property);
                // 新增到 ServletConfigPropertyValues 中
                addPropertyValue(new PropertyValue(property, value));
                // 從 missingProps 中移除
                if (missingProps != null) {
                    missingProps.remove(property);
                }
            }
            // Fail if we are still missing properties.
            if (!CollectionUtils.isEmpty(missingProps)) {
                throw new ServletException("...");
            }
        }
    }
    

    在它的構造方法中可以看到,將<init-param />標籤定義的一些配置項解析成 PropertyValue 物件,例如在前面概述web.xml中的配置,如下:

    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    
  2. 如果存在<init-param />初始化引數

    1. 將當前的這個 Servlet 物件,轉化成一個 BeanWrapper 物件。從而能夠以 Spring 的方式來將 pvs 注入到該 BeanWrapper 物件中。簡單來說,BeanWrapper 是 Spring 提供的一個用來操作 Java Bean 屬性的工具,使用它可以直接修改一個物件的屬性
    2. 註冊自定義屬性編輯器,一旦碰到 Resource 型別的屬性,將會使用 ResourceEditor 進行解析
    3. 呼叫initBeanWrapper(BeanWrapper bw)方法,可初始化當前這個 Servlet 物件,空實現,留給子類覆蓋,目前好像還沒有子類實現
    4. 遍歷 pvs 中的屬性值,注入到該 BeanWrapper 物件中,也就是設定到當前 Servlet 物件中,例如 FrameworkServlet 中的 contextConfigLocation 屬性則會設定為上面的 classpath:spring-mvc.xml 值了
  3. 【關鍵】呼叫initServletBean()方法,空實現,交由子類去實現,完成自定義初始化邏輯,檢視 FrameworkServlet#initServletBean() 方法

FrameworkServlet

org.springframework.web.servlet.FrameworkServlet 抽象類,實現 ApplicationContextAware 介面,繼承 HttpServletBean 抽象類,負責初始化 Spring Servlet WebApplicationContext 容器

構造方法
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
    // ... 省略部分屬性
    
    /** Default context class for FrameworkServlet. */
	public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;

	/** WebApplicationContext implementation class to create. */
	private Class<?> contextClass = DEFAULT_CONTEXT_CLASS;

	/** Explicit context config location. 配置檔案的地址 */
	@Nullable
	private String contextConfigLocation;

	/** Should we publish the context as a ServletContext attribute?. */
	private boolean publishContext = true;

	/** Should we publish a ServletRequestHandledEvent at the end of each request?. */
	private boolean publishEvents = true;

	/** WebApplicationContext for this servlet. */
	@Nullable
	private WebApplicationContext webApplicationContext;

	/** 標記是否是通過 {@link #setApplicationContext} 注入的 WebApplicationContext */
	private boolean webApplicationContextInjected = false;

	/** 標記已經是否接收到 ContextRefreshedEvent 事件,即 {@link #onApplicationEvent(ContextRefreshedEvent)} */
	private volatile boolean refreshEventReceived = false;

	/** Monitor for synchronized onRefresh execution. */
	private final Object onRefreshMonitor = new Object();

	public FrameworkServlet() {
	}

	public FrameworkServlet(WebApplicationContext webApplicationContext) {
		this.webApplicationContext = webApplicationContext;
	}
    
    @Override
	public void setApplicationContext(ApplicationContext applicationContext) {
		if (this.webApplicationContext == null && applicationContext instanceof WebApplicationContext) {
			this.webApplicationContext = (WebApplicationContext) applicationContext;
			this.webApplicationContextInjected = true;
		}
	}
}
  • contextClass 屬性:建立的 WebApplicationContext 型別,預設為 XmlWebApplicationContext.class,在 Root WebApplicationContext 容器的建立過程中也是它

  • contextConfigLocation 屬性:配置檔案的地址,例如:classpath:spring-mvc.xml

  • webApplicationContext 屬性:WebApplicationContext 物件,即本文的關鍵,Servlet WebApplicationContext 容器,有四種建立方式

    1. 通過上面的構造方法
    2. 實現了 ApplicationContextAware 介面,通過 Spring 注入,也就是 setApplicationContext(ApplicationContext applicationContext) 方法
    3. 通過 findWebApplicationContext() 方法,下文見
    4. 通過 createWebApplicationContext(WebApplicationContext parent) 方法,下文見
initServletBean

initServletBean() 方法,重寫父類的方法,在 HttpServletBean 的 init() 方法的最後一步會呼叫,進一步初始化當前 Servlet 物件,當前主要是初始化Servlet WebApplicationContext 容器,程式碼如下:

@Override
protected final void initServletBean() throws ServletException {
    getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
    if (logger.isInfoEnabled()) {
        logger.info("Initializing Servlet '" + getServletName() + "'");
    }
    long startTime = System.currentTimeMillis();

    try {
        // <1> 初始化 WebApplicationContext 物件
        this.webApplicationContext = initWebApplicationContext();
        // <2> 空實現,留給子類覆蓋,目前沒有子類實現
        initFrameworkServlet();
    }
    catch (ServletException | RuntimeException ex) {
        logger.error("Context initialization failed", ex);
        throw ex;
    }

    if (logger.isDebugEnabled()) {
        String value = this.enableLoggingRequestDetails ?
                "shown which may lead to unsafe logging of potentially sensitive data" :
                "masked to prevent unsafe logging of potentially sensitive data";
        logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
                "': request parameters and headers will be " + value);
    }

    if (logger.isInfoEnabled()) {
        logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
    }
}
  1. 呼叫 initWebApplicationContext() 方法,初始化 Servlet WebApplicationContext 物件
  2. 呼叫 initFrameworkServlet() 方法,可對當前 Servlet 物件進行自定義操作,空實現,留給子類覆蓋,目前好像還沒有子類實現
initWebApplicationContext

initWebApplicationContext() 方法【核心】,初始化 Servlet WebApplicationContext 物件,方法如下:

protected WebApplicationContext initWebApplicationContext() {
    // <1> 獲得根 WebApplicationContext 物件
    WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    // <2> 獲得 WebApplicationContext wac 物件
    WebApplicationContext wac = null;

    // 第一種情況,如果構造方法已經傳入 webApplicationContext 屬性,則直接使用
    if (this.webApplicationContext != null) {
        // A context instance was injected at construction time -> use it
        wac = this.webApplicationContext;
        // 如果是 ConfigurableWebApplicationContext 型別,並且未啟用,則進行初始化
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) { // 未啟用
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }
                // 配置和初始化 wac
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    // 第二種情況,從 ServletContext 獲取對應的 WebApplicationContext 物件
    if (wac == null) {
        // No context instance was injected at construction time -> see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    // 第三種,建立一個 WebApplicationContext 物件
    if (wac == null) {
        // No context instance is defined for this servlet -> create a local one
        wac = createWebApplicationContext(rootContext);
    }

    // <3> 如果未觸發重新整理事件,則主動觸發重新整理事件
    if (!this.refreshEventReceived) {
        // Either the context is not a ConfigurableApplicationContext with refresh
        // support or the context injected at construction time had already been
        // refreshed -> trigger initial onRefresh manually here.
        synchronized (this.onRefreshMonitor) {
            onRefresh(wac);
        }
    }

    // <4> 將 context 設定到 ServletContext 中
    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}
  1. 呼叫 WebApplicationContextUtils#getWebApplicationContext((ServletContext sc) 方法,從 ServletContext 中獲得 Root WebApplicationContext 物件,可以回到ContextLoader#initWebApplicationContext方法中的第 5 步,你會覺得很熟悉

  2. 獲得 WebApplicationContext wac 物件,有三種情況

    1. 如果構造方法已經傳入 webApplicationContext 屬性,則直接引用給 wac,也就是上面構造方法中提到的第 1、2 種建立方式

      如果 wac 是 ConfigurableWebApplicationContext 型別,並且未重新整理(未啟用),則呼叫 configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) 方法,進行配置和重新整理,下文見

      如果父容器為空,則設定為上面第 1 步獲取到的 Root WebApplicationContext 物件

    2. 呼叫 findWebApplicationContext()方法,從 ServletContext 獲取對應的 WebApplicationContext 物件,也就是上面構造方法中提到的第 3 種建立方式

      @Nullable
      protected WebApplicationContext findWebApplicationContext() {
          String attrName = getContextAttribute();
          // 需要配置了 contextAttribute 屬性下,才會去查詢,一般我們不會去配置
          if (attrName == null) {
              return null;
          }
          // 從 ServletContext 中,獲得屬性名對應的 WebApplicationContext 物件
          WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
          // 如果不存在,則丟擲 IllegalStateException 異常
          if (wac == null) {
              throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
          }
          return wac;
      }
      

      一般不會這樣做

    3. 呼叫createWebApplicationContext(@Nullable WebApplicationContext parent)方法,建立一個 WebApplicationContext 物件

      protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
          // <a> 獲得 context 的類,XmlWebApplicationContext.class
          Class<?> contextClass = getContextClass();
          // 如果非 ConfigurableWebApplicationContext 型別,丟擲 ApplicationContextException 異常
          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");
          }
          // <b> 建立 context 類的物件
          ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
      
          // <c> 設定 environment、parent、configLocation 屬性
          wac.setEnvironment(getEnvironment());
          wac.setParent(parent);
          String configLocation = getContextConfigLocation();
          if (configLocation != null) {
              wac.setConfigLocation(configLocation);
          }
          // <d> 配置和初始化 wac
          configureAndRefreshWebApplicationContext(wac);
      
          return wac;
      }
      

      <a> 獲得 context 的 Class 物件,預設為 XmlWebApplicationContext.class,如果非 ConfigurableWebApplicationContext 型別,則丟擲異常

      <b> 建立 context 的例項物件

      <c> 設定 environmentparentconfigLocation 屬性。其中,configLocation 是個重要屬性

      <d> 呼叫 configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) 方法,進行配置和重新整理,下文見

  3. 如果未觸發重新整理事件,則呼叫 onRefresh(ApplicationContext context) 方法,主動觸發重新整理事件,該方法為空實現,交由子類 DispatcherServlet 去實現

  4. context 設定到 ServletContext 中

configureAndRefreshWebApplicationContext

configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) 方法,配置和初始化 wac 物件,方法如下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    // <1> 如果 wac 使用了預設編號,則重新設定 id 屬性
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // The application context id is still set to its original default value
        // -> assign a more useful id based on available information
        // 情況一,使用 contextId 屬性
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        // 情況二,自動生成
        else {
            // Generate default id...
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
        }
    }

    // <2> 設定 wac 的 servletContext、servletConfig、namespace 屬性
    wac.setServletContext(getServletContext());
    wac.setServletConfig(getServletConfig());
    wac.setNamespace(getNamespace());
    // <3> 新增監聽器 SourceFilteringListener 到 wac 中
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

    // The wac environment's #initPropertySources will be called in any case when the context
    // is refreshed; do it eagerly here to ensure servlet property sources are in place for
    // use in any post-processing or initialization that occurs below prior to #refresh
    // <4>
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }

    // <5> 執行處理完 WebApplicationContext 後的邏輯。目前是個空方法,暫無任何實現
    postProcessWebApplicationContext(wac);
    // <6> 執行自定義初始化 context
    applyInitializers(wac);
    // <7> 重新整理 wac ,從而初始化 wac
    wac.refresh();
}

實際上,處理邏輯和ContextLoader#configureAndRefreshWebApplicationContext方法差不多

  1. 如果 wac 使用了預設編號,則重新設定 id 屬性
  2. 設定 wac 的 servletContext、servletConfig、namespace 屬性
  3. 新增監聽器 SourceFilteringListener 到 wac
  4. 配置 Environment 物件,暫時忽略
  5. 執行處理完 WebApplicationContext 後的邏輯,空方法,暫無任何實現
  6. wac 進行定製化處理,暫時忽略
  7. 【關鍵】觸發 wac 的重新整理事件,執行初始化。此處,就會進行一些的 Spring 容器的初始化工作,涉及到 Spring IOC 相關內容
onRefresh

onRefresh(ApplicationContext context) 方法,當 Servlet WebApplicationContext 重新整理完成後,觸發 Spring MVC 元件的初始化,方法如下:

/**
 * Template method which can be overridden to add servlet-specific refresh work.
 * Called after successful context refresh.
 * <p>This implementation is empty.
 * @param context the current WebApplicationContext
 * @see #refresh()
 */
protected void onRefresh(ApplicationContext context) {
    // For subclasses: do nothing by default.
}

這是一個空方法,具體的實現,在子類 DispatcherServlet 中,程式碼如下:

// DispatcherServlet.java
@Override
protected void onRefresh(ApplicationContext context) {
    initStrategies(context);
}

/**
 * Initialize the strategy objects that this servlet uses.
 * <p>May be overridden in subclasses in order to initialize further strategy objects.
 */
protected void initStrategies(ApplicationContext context) {
    // 初始化 MultipartResolver
    initMultipartResolver(context);
    // 初始化 LocaleResolver
    initLocaleResolver(context);
    // 初始化 ThemeResolver
    initThemeResolver(context);
    // 初始化 HandlerMappings
    initHandlerMappings(context);
    // 初始化 HandlerAdapters
    initHandlerAdapters(context);
    // 初始化 HandlerExceptionResolvers 
    initHandlerExceptionResolvers(context);
    // 初始化 RequestToViewNameTranslator
    initRequestToViewNameTranslator(context);
    // 初始化 ViewResolvers
    initViewResolvers(context);
    // 初始化 FlashMapManager
    initFlashMapManager(context);
}

初始化九個元件,這裡只是先提一下,在後續的文件中會進行分析

onRefresh方法的觸發有兩種方式:

  • 方式一:如果refreshEventReceivedfalse,也就是未接收到重新整理事件(防止重複初始化相關元件),則在 initWebApplicationContext 方法中直接呼叫
  • 方式二:通過在 configureAndRefreshWebApplicationContext 方法中,觸發 wac 的重新整理事件

為什麼上面的方式二可以觸發這個方法的呼叫呢?

先看到 configureAndRefreshWebApplicationContext 方法的第 3 步,新增了一個 SourceFilteringListener 監聽器,如下:

// <3> 新增監聽器 SourceFilteringListener 到 wac 中
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

監聽到相關事件後,會委派給 ContextRefreshListener 進行處理,它是 FrameworkServlet 的私有內部類,來看看它又是怎麼處理的,程式碼如下:

private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        FrameworkServlet.this.onApplicationEvent(event);
    }
}

直接將該事件委派給了 FrameworkServlet 的 onApplicationEvent 方法,如下:

public void onApplicationEvent(ContextRefreshedEvent event) {
    // 標記 refreshEventReceived 為 true
    this.refreshEventReceived = true;
    synchronized (this.onRefreshMonitor) {
        // 處理事件中的 ApplicationContext 物件,空實現,子類 DispatcherServlet 會實現
        onRefresh(event.getApplicationContext());
    }
}

先設定 refreshEventReceivedtrue,表示已接收到重新整理時間,然後再呼叫 onRefresh 方法,回到上面的方式一方式二,是不是連通起來了,所以說該方法是一定會被觸發的

總結

本分對 Spring MVC 兩種容器的建立過程進行分析,分別為 Root WebApplicationContextServlet WebApplicationContext 容器,它們是父子關係,建立過程並不是很複雜。前置是在 Tomcat 或者 Jetty 等 Servlet 容器啟動後,由 ContextLoaderListener 監聽到相應事件而建立的,後者是在 DispatcherServlet 初始化的過程中建立的,因為它是一個 HttpServlet 物件,會呼叫其 init 方法,完成初始化相關工作

DispatcherServlet 是 Spring MVC 的核心類,相當於一個排程者,請求的處理過程都是通過它排程各個元件來完成的,在後續的文章中進行分析

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

相關文章