Spring MVC 原理探祕 - 容器的建立過程

1.簡介

在上一篇文章中,我向你們介紹了 Spring MVC 是如何處理 HTTP 請求的。Spring MVC 可對外提供服務時,說明其已經處於了就緒狀態。再次以前,Spring MVC 須要進行一系列的初始化操做。正所謂兵馬未動,糧草先行。這些操做包括建立容器,加載 DispatcherServlet 中用到的各類組件等。本篇文章就來和你們討論一下這些初始化操做中的容器建立操做,容器的建立是其餘一些初始化過程的基礎。那其餘的就很少說了,咱們直入主題吧。java

2.容器的建立過程

通常狀況下,咱們會在一個 Web 應用中配置兩個容器。一個容器用於加載 Web 層的類,好比咱們的接口 Controller、HandlerMapping、ViewResolver 等。在本文中,咱們把這個容器叫作 web 容器。另外一個容器用於加載業務邏輯相關的類,好比 service、dao 層的一些類。在本文中,咱們把這個容器叫作業務容器。在容器初始化的過程當中,業務容器會先於 web 容器進行初始化。web 容器初始化時,會將業務容器做爲父容器。這樣作的緣由是,web 容器中的一些 bean 會依賴於業務容器中的 bean。好比咱們的 controller 層接口一般會依賴 service 層的業務邏輯類。下面舉個例子進行說明:web

如上,咱們將 dao 層的類配置在 application-dao.xml 文件中,將 service 層的類配置在 application-service.xml 文件中。而後咱們將這兩個配置文件經過 標籤導入到 application.xml 文件中。此時,咱們可讓業務容器去加載 application.xml 配置文件便可。另外一方面,咱們將 Web 相關的配置放在 application-web.xml 文件中,並將該文件交給 Web 容器去加載。spring

這裏咱們把配置文件進行分層,結構上看起來清晰了不少,也便於維護。這個其實和代碼分層是一個道理,若是咱們把全部的代碼都放在同一個包下,那看起來會多難受啊。同理,咱們用業務容器和 Web 容器去加載不一樣的類也是一種分層的體現吧。固然,若是應用比較簡單,僅用 Web 容器去加載全部的類也不是不能夠。mvc

2.1 業務容器的建立過程

前面說了一些背景知識做爲鋪墊,那下面咱們開始分析容器的建立過程吧。按照建立順序,咱們先來分析業務容器的建立過程。業務容器的建立入口是 ContextLoaderListener 的 contextInitialized 方法。顧名思義,ContextLoaderListener 是用來監聽 ServletContext 加載事件的。當 ServletContext 被加載後,監聽器的 contextInitialized 方法就會被 Servlet 容器調用。ContextLoaderListener Spring 框架提供的,它的配置方法以下:app

<web-app>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:application.xml</param-value>
    </context-param>
    
    <!-- 省略其餘配置 -->
</web-app>

如上,ContextLoaderListener 可經過 ServletContext 獲取到 contextConfigLocation 配置。這樣,業務容器就能夠加載 application.xml 配置文件了。那下面咱們來分析一下 ContextLoaderListener 的源碼吧。框架

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

    // 省略部分代碼

    @Override
    public void contextInitialized(ServletContextEvent event) {
        // 初始化 WebApplicationContext
        initWebApplicationContext(event.getServletContext());
    }
}

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    /*
     * 若是 ServletContext 中 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 屬性值
     * 不爲空時,代表有其餘監聽器設置了這個屬性。Spring 認爲不能替換掉別的監聽器設置
     * 的屬性值,因此這裏拋出異常。
     */
    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!");
    }

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

    try {
        if (this.context == null) {
            // 建立 WebApplicationContext
            this.context = createWebApplicationContext(servletContext);
        }
        if (this.context instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    /*
                     * 加載父 ApplicationContext,通常狀況下,業務容器不會有父容器,
                     * 除非進行配置
                     */ 
                    ApplicationContext parent = loadParentContext(servletContext);
                    cwac.setParent(parent);
                }
                // 配置並刷新 WebApplicationContext
                configureAndRefreshWebApplicationContext(cwac, servletContext);
            }
        }

        // 設置 ApplicationContext 到 servletContext 中
        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.isDebugEnabled()) {...}
        if (logger.isInfoEnabled()) {...}

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

如上,咱們看一下上面的建立過程。首先 Spring 會檢測 ServletContext 中 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 屬性有沒有被設置,若被設置過,則拋出異常。若未設置,則調用 createWebApplicationContext 方法建立容器。建立好後,再調用 configureAndRefreshWebApplicationContext 方法配置並刷新容器。最後,調用 setAttribute 方法將容器設置到 ServletContext 中。通過以上幾步,整個建立流程就結束了。流程並不複雜,可簡單總結爲建立容器 → 配置並刷新容器 → 設置容器到 ServletContext 中。這三步流程中,最後一步就不進行分析,接下來分析一下第一步和第二步流程對應的源碼。以下:ide

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
    // 判斷建立什麼類型的容器,默認類型爲 XmlWebApplicationContext
    Class<?> contextClass = determineContextClass(sc);
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
                "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
    }
    // 經過反射建立容器
    return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

protected Class<?> determineContextClass(ServletContext servletContext) {
    /*
     * 讀取用戶自定義配置,好比:
     * <context-param>
     *     <param-name>contextClass</param-name>
     *     <param-value>XXXConfigWebApplicationContext</param-value>
     * </context-param>
     */
    String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
    if (contextClassName != null) {
        try {
            return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
        }
        catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load custom context class [" + contextClassName + "]", ex);
        }
    }
    else {
        /*
         * 若無自定義配置,則獲取默認的容器類型,默認類型爲 XmlWebApplicationContext。
         * defaultStrategies 讀取的配置文件爲 ContextLoader.properties,
         * 該配置文件內容以下:
         * org.springframework.web.context.WebApplicationContext =
         *     org.springframework.web.context.support.XmlWebApplicationContext
         */
        contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
        try {
            return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
        }
        catch (ClassNotFoundException ex) {
            throw new ApplicationContextException(
                    "Failed to load default context class [" + contextClassName + "]", ex);
        }
    }
}

簡單說一下 createWebApplicationContext 方法的流程,該方法首先會調用 determineContextClass 判斷建立什麼類型的容器,默認爲 XmlWebApplicationContext。而後調用 instantiateClass 方法經過反射的方式建立容器實例。instantiateClass 方法就不跟進去分析了,你們能夠本身去看看,比較簡單。源碼分析

繼續往下分析,接下來分析一下 configureAndRefreshWebApplicationContext 方法的源碼。以下:post

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // 從 ServletContext 中獲取用戶配置的 contextId 屬性
        String idParam = sc.getInitParameter(CONTEXT_ID_PARAM);
        if (idParam != null) {
            // 設置容器 id
            wac.setId(idParam);
        }
        else {
            // 用戶未配置 contextId,則設置一個默認的容器 id
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(sc.getContextPath()));
        }
    }

    wac.setServletContext(sc);
    // 獲取 contextConfigLocation 配置
    String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
    if (configLocationParam != null) {
        wac.setConfigLocation(configLocationParam);
    }
    
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
    }

    customizeContext(sc, wac);

    // 刷新容器
    wac.refresh();
}

上面的源碼不是很長,邏輯不是很複雜。下面簡單總結 configureAndRefreshWebApplicationContext 方法主要作了事情,以下:ui

  1. 設置容器 id
  2. 獲取 contextConfigLocation 配置,並設置到容器中
  3. 刷新容器

到此,關於業務容器的建立過程就分析完了,下面咱們繼續分析 Web 容器的建立過程。

2.2 Web 容器的建立過程

前面說了業務容器的建立過程,業務容器是經過 ContextLoaderListener。那 Web 容器是經過什麼建立的呢?答案是經過 DispatcherServlet。我在上一篇文章介紹 HttpServletBean 抽象類時,說過該類覆寫了父類 HttpServlet 中的 init 方法。這個方法就是建立 Web 容器的入口,那下面咱們就從這個方法入手。以下:

// -☆- org.springframework.web.servlet.HttpServletBean
public final void init() throws ServletException {
    if (logger.isDebugEnabled()) {...}

    // 獲取 ServletConfig 中的配置信息
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            /*
             * 爲當前對象(好比 DispatcherServlet 對象)建立一個 BeanWrapper,
             * 方便讀/寫對象屬性。
             */ 
            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) {
            if (logger.isErrorEnabled()) {...}
            throw ex;
        }
    }

    // 進行後續的初始化
    initServletBean();

    if (logger.isDebugEnabled()) {...}
}

protected void initServletBean() throws ServletException {
}

上面的源碼主要作的事情是將 ServletConfig 中的配置信息設置到 HttpServletBean 的子類對象中(好比 DispatcherServlet),咱們並未從上面的源碼中發現建立容器的痕跡。不過若是你們注意看源碼的話,會發現 initServletBean 這個方法稍顯奇怪,是個空方法。這個方法的訪問級別爲 protected,子類可進行覆蓋。HttpServletBean 子類 FrameworkServlet 覆寫了這個方法,下面咱們到 FrameworkServlet 中探索一番。

// -☆- org.springframework.web.servlet.FrameworkServlet
protected final void initServletBean() throws ServletException {
    getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
    if (this.logger.isInfoEnabled()) {...}
    long startTime = System.currentTimeMillis();

    try {
        // 初始化容器
        this.webApplicationContext = initWebApplicationContext();
        initFrameworkServlet();
    }
    catch (ServletException ex) {
        this.logger.error("Context initialization failed", ex);
        throw ex;
    }
    catch (RuntimeException ex) {
        this.logger.error("Context initialization failed", ex);
        throw ex;
    }

    if (this.logger.isInfoEnabled()) {...}
}

protected WebApplicationContext initWebApplicationContext() {
    // 從 ServletContext 中獲取容器,也就是 ContextLoaderListener 建立的容器
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    /*
     * 若下面的條件成立,則須要從外部設置 webApplicationContext。有兩個途徑能夠設置 
     * webApplicationContext,以 DispatcherServlet 爲例:
     *    1. 經過 DispatcherServlet 有參構造方法傳入 WebApplicationContext 對象
     *    2. 將 DispatcherServlet 配置到其餘容器中,由其餘容器經過 
     *       setApplicationContext 方法進行設置
     *       
     * 途徑1 可參考 AbstractDispatcherServletInitializer 中的 
     * registerDispatcherServlet 方法源碼。通常狀況下,代碼執行到此處,
     * this.webApplicationContext 爲 null,你們可自行調試進行驗證。
     */
    if (this.webApplicationContext != null) {
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                if (cwac.getParent() == null) {
                    // 設置 rootContext 爲父容器
                    cwac.setParent(rootContext);
                }
                // 配置並刷新容器
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // 嘗試從 ServletContext 中獲取容器
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // 建立容器,並將 rootContext 做爲父容器
        wac = createWebApplicationContext(rootContext);
    }

    if (!this.refreshEventReceived) {
        onRefresh(wac);
    }

    if (this.publishContext) {
        String attrName = getServletContextAttributeName();
        // 將建立好的容器設置到 ServletContext 中
        getServletContext().setAttribute(attrName, wac);
        if (this.logger.isDebugEnabled()) {...}
    }

    return wac;
}

protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
    // 獲取容器類型,默認爲 XmlWebApplicationContext.class
    Class<?> contextClass = getContextClass();
    if (this.logger.isDebugEnabled()) {...}
    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);
    wac.setConfigLocation(getContextConfigLocation());

    // 配置並刷新容器
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // 設置容器 id
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        else {
            // 生成默認 id
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
        }
    }

    wac.setServletContext(getServletContext());
    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());
    }

    // 後置處理,子類能夠覆蓋進行一些自定義操做。在 Spring MVC 未使用到,是個空方法。
    postProcessWebApplicationContext(wac);
    applyInitializers(wac);
    // 刷新容器
    wac.refresh();
}

以上就是建立 Web 容器的源碼,下面總結一下該容器建立的過程。以下:

  1. 從 ServletContext 中獲取 ContextLoaderListener 建立的容器
  2. 若 this.webApplicationContext != null 條件成立,僅設置父容器和刷新容器便可
  3. 嘗試從 ServletContext 中獲取容器,若容器不爲空,則無需執行步驟4
  4. 建立容器,並將 rootContext 做爲父容器
  5. 設置容器到 ServletContext 中

到這裏,關於 Web 容器的建立過程就講完了。總的來講,Web 容器的建立過程和業務容器的建立過程大體相同,可是差別也是有的,不能忽略。

3.總結

本篇文章對 Spring MVC 兩種容器的建立過程進行了較爲詳細的分析,總的來講兩種容器的建立過程並非很複雜。你們在分析這兩種容器的建立過程時,看的不明白的地方,能夠進行調試,這對於理解代碼邏輯仍是頗有幫助的。固然閱讀 Spring MVC 部分的源碼最好有 Servlet 和 Spring IOC 容器方面的知識,這些是基礎,Spring MVC 就是在這些基礎上構建的。

限於我的能力,文章敘述有誤,還望你們指明。也請多多指教,在這裏說聲謝謝。好了,本篇文章就到這裏了。感謝你們的閱讀。

參考

附錄:Spring 源碼分析文章列表

Ⅰ. IOC

更新時間 標題
2018-05-30 Spring IOC 容器源碼分析系列文章導讀
2018-06-01 Spring IOC 容器源碼分析 - 獲取單例 bean
2018-06-04 Spring IOC 容器源碼分析 - 建立單例 bean 的過程
2018-06-06 Spring IOC 容器源碼分析 - 建立原始 bean 對象
2018-06-08 Spring IOC 容器源碼分析 - 循環依賴的解決辦法
2018-06-11 Spring IOC 容器源碼分析 - 填充屬性到 bean 原始對象
2018-06-11 Spring IOC 容器源碼分析 - 餘下的初始化工做

Ⅱ. AOP

更新時間 標題
2018-06-17 Spring AOP 源碼分析系列文章導讀
2018-06-20 Spring AOP 源碼分析 - 篩選合適的通知器
2018-06-20 Spring AOP 源碼分析 - 建立代理對象
2018-06-22 Spring AOP 源碼分析 - 攔截器鏈的執行過程

Ⅲ. MVC

更新時間 標題
2018-06-29 Spring MVC 原理探祕 - 一個請求的旅行過程
2018-06-30 Spring MVC 原理探祕 - 容器的建立過程

本文在知識共享許可協議 4.0 下發布,轉載需在明顯位置處註明出處
做者:田小波
本文同步發佈在個人我的博客:http://www.tianxiaobo.com

cc
本做品採用知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協議進行許可。

相關文章
相關標籤/搜索