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

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

Spring 版本:5.2.4.RELEASEjava

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

概述

在開始看具體的源碼實現以前,咱們先一塊兒來看看如今「陌生」的 web.xml 文件,能夠查看個人另外一篇 MyBatis 使用手冊 文檔中集成 Spring小節涉及到的 web.xml 的文件,部份內容以下:github

<?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 容器,監聽到銷燬事件,則會銷燬該容器web

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

  • 爲何有了 【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 容器spring-mvc

而 ContextLoaderListener 的類圖以下:mybatis

ContextLoader

ContextLoaderListener

org.springframework.web.context.ContextLoaderListener 類,實現 javax.servlet.ServletContextListener 接口,繼承 ContextLoader 類,實現 Servlet 容器啓動和關閉時,分別初始化和銷燬 WebApplicationContext 容器,代碼以下:mvc

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 容器的邏輯的類app

靜態代碼塊
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,也就是上面構造方法中提到的第 一、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 源碼分析》

相關文章
相關標籤/搜索