天天看點

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

1.簡介

在上一篇

文章

中,我向大家介紹了 Spring MVC 是如何處理 HTTP 請求的。Spring MVC 可對外提供服務時,說明其已經處于了就緒狀态。再次之前,Spring MVC 需要進行一系列的初始化操作。正所謂兵馬未動,糧草先行。這些操作包括建立容器,加載 DispatcherServlet 中用到的各種元件等。本篇文章就來和大家讨論一下這些初始化操作中的容器建立操作,容器的建立是其他一些初始化過程的基礎。那其他的就不多說了,我們直入主題吧。

2.容器的建立過程

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

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

如上,我們将 dao 層的類配置在 application-dao.xml 檔案中,将 service 層的類配置在 application-service.xml 檔案中。然後我們将這兩個配置檔案通過 标簽導入到 application.xml 檔案中。此時,我們可以讓業務容器去加載 application.xml 配置檔案即可。另一方面,我們将 Web 相關的配置放在 application-web.xml 檔案中,并将該檔案交給 Web 容器去加載。

這裡我們把配置檔案進行分層,結構上看起來清晰了很多,也便于維護。這個其實和代碼分層是一個道理,如果我們把所有的代碼都放在同一個包下,那看起來會多難受啊。同理,我們用業務容器和 Web 容器去加載不同的類也是一種分層的展現吧。當然,如果應用比較簡單,僅用 Web 容器去加載所有的類也不是不可以。

2.1 業務容器的建立過程

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

<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 中

。這三步流程中,最後一步就不進行分析,接下來分析一下第一步和第二步流程對應的源碼。如下:

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 方法的源碼。如下:

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 方法主要做了事情,如下:

  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

Ⅱ. AOP

Ⅲ. MVC

本文在知識共享許可協定 4.0 下釋出,轉載需在明顯位置處注明出處

作者:coolblog.xyz

本文同步釋出在我的個人部落格:

http://www.coolblog.xyz
Spring MVC 原理探秘 - 容器的建立過程

本作品采用

知識共享署名-非商業性使用-禁止演繹 4.0 國際許可協定

進行許可。