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 層的業務邏輯類。下面舉個例子進行說明:

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

  • 設置容器 id
  • 獲取 contextConfigLocation 配置,並設置到容器中
  • 刷新容器

到此,關於業務容器的創建過程就分析完了,下面我們繼續分析 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 容器的源碼,下面總結一下該容器創建的過程。如下:

  • 從 ServletContext 中獲取 ContextLoaderListener 創建的容器
  • 若 this.webApplicationContext != null 條件成立,僅設置父容器和刷新容器即可
  • 嘗試從 ServletContext 中獲取容器,若容器不為空,則無需執行步驟4
  • 創建容器,並將 rootContext 作為父容器
  • 設置容器到 ServletContext 中

到這里,關於 Web 容器的創建過程就講完了。總的來說,Web 容器的創建過程和業務容器的創建過程大致相同,但是差異也是有的,不能忽略。

3.總結

本篇文章對 Spring MVC 兩種容器的創建過程進行了較為詳細的分析,總的來說兩種容器的創建過程並不是很復雜。大家在分析這兩種容器的創建過程時,看的不明白的地方,可以進行調試,這對於理解代碼邏輯還是很有幫助的。當然閱讀 Spring MVC 部分的源碼最好有 Servlet 和 Spring IOC 容器方面的知識,這些是基礎,Spring MVC 就是在這些基礎上構建的。

限於個人能力,文章敘述有誤,還望大家指明。也請多多指教,在這里說聲謝謝。好了,本篇文章就到這里了。感謝大家的閱讀。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM