SpringMVC初始化流程

web應用部署初始化流程

當一個Web應用部署到容器內時(eg.tomcat),在Web應用開始響應執行用戶請求前,如下步驟會被依次執行:css

  • 部署描述文件中(eg.tomcat的web.xml)由<listener>元素標記的事件監聽器會被建立和初始化
  • 對於全部事件監聽器,若是實現了ServletContextListener接口,將會執行其實現的contextInitialized()方法
  • 部署描述文件中由<filter>元素標記的過濾器會被建立和初始化,並調用其init()方法
  • 部署描述文件中由<servlet>元素標記的servlet會根據<load-on-startup>的權值按順序建立和初始化,並調用其init()方法

web初始化流程圖以下:前端

clipboard.png

SpringMVC初始化流程

接下來以一個常見的簡單web.xml配置進行Spring MVC啓動過程的分析,web.xml配置內容以下:web

<web-app>

  <display-name>Web Application</display-name>

  <!--全局變量配置-->
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext-*.xml</param-value>
  </context-param>

  <!--監聽器-->
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  
  <!--解決亂碼問題的filter-->
  <filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>utf-8</param-value>
    </init-param>
  </filter>

  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

  <!--Restful前端控制器-->
  <servlet>
    <servlet-name>springMVC_rest</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--配置servlet初始化優先級,本質是調用init()-->
    <load-on-startup>1<load-on-startup/>
  </servlet>

  <!--靜態資源使用DefaultServletHttpRequestHandler處理器處理-->
  <servlet-mapping>
    <servlet-name>default</servlet-name>
    <url-pattern>*.css</url-pattern>
  </servlet-mapping>

</web-app>

在web初始化的時候會去加載web.xml文件,會按照配置的內容進行初始化spring

Listener監聽器初始化

首先定義了<context-param>標籤,用於配置一個全局變量,<context-param>標籤的內容讀取後會被放進application中,作爲Web應用的全局變量使用,加載bean的時候做爲application的configLocation參數,所以,Web應用在容器中部署後,進行初始化時會先讀取這個全局變量,以後再進行上述講解的初始化啓動過程。
接着定義了一個ContextLoaderListener類的listener。查看ContextLoaderListener的類聲明源碼以下圖:數據庫

clipboard.png

ContextLoaderListener類繼承了ContextLoader類並實現了ServletContextListener接口,首先看一下前面講述的ServletContextListener接口源碼:tomcat

clipboard.png

該接口只有兩個方法contextInitializedcontextDestroyed,這裏採用的是觀察者模式,也稱爲爲訂閱-發佈模式,實現了該接口的listener會向發佈者進行訂閱,當Web應用初始化或銷燬時會分別調用上述兩個方法。
繼續看ContextLoaderListener,該listener實現了ServletContextListener接口,所以在Web應用初始化時會調用該方法,該方法的具體實現以下:安全

* Initialize the root web application context.*/
    @Override
    public void contextInitialized(ServletContextEvent event) {
        initWebApplicationContext(event.getServletContext());
    }

ContextLoaderListener的contextInitialized()方法直接調用了initWebApplicationContext()方法,這個方法是繼承自ContextLoader類,經過函數名能夠知道,該方法是用於初始化Web應用上下文,即IoC容器,這裏使用的是代理模式,繼續查看ContextLoader類的initWebApplicationContext()方法的源碼以下:app

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    /*
    首先經過WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
    這個String類型的靜態變量獲取一個根IoC容器,根IoC容器做爲全局變量
    存儲在application對象中,若是存在則有且只能有一個
    若是在初始化根WebApplicationContext即根IoC容器時發現已經存在
    則直接拋出異常,所以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!");
        }

        Log logger = LogFactory.getLog(ContextLoader.class);
        servletContext.log("Initializing Spring root WebApplicationContext");
        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.
            // 若是當前成員變量中不存在WebApplicationContext則建立一個根WebApplicationContext
            if (this.context == null) {
                this.context = createWebApplicationContext(servletContext);
            }
            if (this.context instanceof ConfigurableWebApplicationContext) {
                ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
                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 ->
                        // determine parent for root web application context, if any.
                        //爲根WebApplicationContext設置一個父容器
                        ApplicationContext parent = loadParentContext(servletContext);
                        cwac.setParent(parent);
                    }
                    //配置並刷新整個根IoC容器,在這裏會進行Bean的建立和初始化
                    configureAndRefreshWebApplicationContext(cwac, servletContext);
                }
            }
            /*
            將建立好的IoC容器放入到application對象中,並設置key爲WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
            所以,在SpringMVC開發中能夠在jsp中經過該key在application對象中獲取到根IoC容器,進而獲取到相應的Ben
            */
            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()) {
                logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" +
                        WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]");
            }
            if (logger.isInfoEnabled()) {
                long elapsedTime = System.currentTimeMillis() - startTime;
                logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms");
            }

            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;
        }
    }

initWebApplicationContext()方法如上註解講述,主要目的就是建立root WebApplicationContext對象即根IoC容器,其中比較重要的就是,整個Web應用若是存在根IoC容器則有且只能有一個,根IoC容器做爲全局變量存儲在ServletContext即application對象中。將根IoC容器放入到application對象以前進行了IoC容器的配置和刷新操做,調用了configureAndRefreshWebApplicationContext()方法,該方法源碼以下:jsp

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
        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
            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()));
            }
        }

        wac.setServletContext(sc);
        /*
        CONFIG_LOCATION_PARAM = "contextConfigLocation"
        獲取web.xml中<context-param>標籤配置的全局變量,其中key爲CONFIG_LOCATION_PARAM
        也就是咱們配置的相應Bean的xml文件名,並將其放入到WebApplicationContext中
        */
        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);
        }

        customizeContext(sc, wac);
        wac.refresh();
    }

比較重要的就是獲取到了web.xml中的<context-param>標籤配置的全局變量contextConfigLocation,並最後一行調用了refresh()方法,ConfigurableWebApplicationContext是一個接口,經過對經常使用實現類ClassPathXmlApplicationContext逐層查找後能夠找到一個抽象類AbstractApplicationContext實現了refresh()方法,其源碼以下:ide

@Override
    public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
            prepareRefresh();

            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);

            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);

                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);

                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();

                // Instantiate all remaining (non-lazy-init) singletons.
                finishBeanFactoryInitialization(beanFactory);

                // Last step: publish corresponding event.
                finishRefresh();
            }

            catch (BeansException ex) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
                }

                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();

                // Reset 'active' flag.
                cancelRefresh(ex);

                // Propagate exception to caller.
                throw ex;
            }

            finally {
                // Reset common introspection caches in Spring's core, since we
                // might not ever need metadata for singleton beans anymore...
                resetCommonCaches();
            }
        }
    }

該方法主要用於建立並初始化contextConfigLocation類配置的xml文件中的Bean,所以,若是咱們在配置Bean時出錯,在Web應用啓動時就會拋出異常,而不是等到運行時才拋出異常。
整個ContextLoaderListener類的啓動過程到此就結束了,能夠發現,建立ContextLoaderListener是比較核心的一個步驟,主要工做就是爲了建立根IoC容器並使用特定的key將其放入到application對象中,供整個Web應用使用,因爲在ContextLoaderListener類中構造的根IoC容器配置的Bean是全局共享的,所以,在<context-param>標識的contextConfigLocation的xml配置文件通常包括:數據庫DataSource、DAO層、Service層、事務等相關Bean。

Filter的初始化

在監聽器listener初始化完成後,按照文章開始的講解,接下來會進行filter的初始化操做,filter的建立和初始化中沒有涉及IoC容器的相關操做,所以不是本文講解的重點,本文舉例的filter是一個用於編碼用戶請求和響應的過濾器,採用utf-8編碼用於適配中文。

Servlet的初始化

Servlet的初始化過程能夠經過一張圖來總結,以下所示:

clipboard.png

經過類圖和相關初始化函數調用的邏輯來看,DispatcherServlet類的初始化過程將模板方法使用的淋漓盡致,其父類完成不一樣的統一的工做,並預留出相關方法用於子類覆蓋去完成不一樣的可變工做。

DispatcherServelt類的本質是Servlet,經過文章開始的講解可知,在Web應用部署到容器後進行Servlet初始化時會調用相關的init(ServletConfig)方法,所以,DispatchServlet類的初始化過程也由該方法開始。上述調用邏輯中比較重要的就是FrameworkServlet抽象類中的initServletBean()方法、initWebApplicationContext()方法以及DispatcherServlet類中的onRefresh()方法,接下來會逐一進行講解。

首先來看initServletBean()方法:

@Override
    protected final void initServletBean() throws ServletException {
        getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
        if (this.logger.isInfoEnabled()) {
            this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
        }
        long startTime = System.currentTimeMillis();

        try {
            //這裏是重點,用於初始化子ApplicationContext對象,主要是用來加載<servlet/>對應的servletName-servlet.xml文件如:springMVC_rest-servlet.xml
            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()) {
            long elapsedTime = System.currentTimeMillis() - startTime;
            this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
                    elapsedTime + " ms");
        }
    }

接下來來着重分析initWebApplicationContext()方法,

protected WebApplicationContext initWebApplicationContext() {
        /*
        獲取由ContextLoaderListener建立的根IoC容器
        獲取根IoC容器有兩種方法,還可經過key直接獲取
        */
        WebApplicationContext rootContext =
                WebApplicationContextUtils.getWebApplicationContext(getServletContext());
        WebApplicationContext wac = null;

        if (this.webApplicationContext != null) {
            // A context instance was injected at construction time -> use it
            wac = this.webApplicationContext;
            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
                        /*
                        若是當前Servelt存在一個WebApplicationContext即子IoC容器
                        而且上文獲取的根IoC容器存在,則將根IoC容器做爲子IoC容器的父容器
                        */
                        cwac.setParent(rootContext);
                    }
                    //配置並刷新當前的子IoC容器,功能與前文講解根IoC容器時的配置刷新一致,用於構建相關Bean
                    configureAndRefreshWebApplicationContext(cwac);
                }
            }
        }
        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
            //若是當前Servlet不存在一個子IoC容器則去查找一下
            wac = findWebApplicationContext();
        }
        if (wac == null) {
            // No context instance is defined for this servlet -> create a local one
            //若是仍舊沒有查找到子IoC容器則建立一個子IoC容器
            wac = createWebApplicationContext(rootContext);
        }

        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.
            //調用子類覆蓋的onRefresh方法完成「可變」的初始化過程
            onRefresh(wac);
        }

        if (this.publishContext) {
            // Publish the context as a servlet context attribute.
            String attrName = getServletContextAttributeName();
            getServletContext().setAttribute(attrName, wac);
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
                        "' as ServletContext attribute with name [" + attrName + "]");
            }
        }

        return wac;
    }

經過函數名不難發現,該方法的主要做用一樣是建立一個WebApplicationContext對象,即Ioc容器,不過前文講過每一個Web應用最多隻能存在一個根IoC容器,這裏建立的則是特定Servlet擁有的子IoC容器,可能有些讀者會有疑問,爲何須要多個Ioc容器,首先介紹一個父子IoC容器的訪問特性,有興趣的讀者能夠自行實驗。

父子IoC容器的訪問特性

在學習Spring時,咱們都是從讀取xml配置文件來構造IoC容器,經常使用的類有ClassPathXmlApplicationContext類,該類存在一個初始化方法用於傳入xml文件路徑以及一個父容器,咱們能夠建立兩個不一樣的xml配置文件並實現以下代碼:

//applicationContext1.xml文件中配置一個id爲baseBean的Bean
ApplicationContext baseContext = new ClassPathXmlApplicationContext("applicationContext1.xml");

Object obj1 = baseContext.getBean("baseBean");

System.out.println("baseContext Get Bean " + obj1);

//applicationContext2.xml文件中配置一個id未subBean的Bean
ApplicationContext subContext = new ClassPathXmlApplicationContext(new String[]{"applicationContext2.xml"}, baseContext);

Object obj2 = subContext.getBean("baseBean");

System.out.println("subContext get baseContext Bean " + obj2);

Object obj3 = subContext.getBean("subBean");

System.out.println("subContext get subContext Bean " + obj3);

//拋出NoSuchBeanDefinitionException異常
Object obj4 = baseContext.getBean("subBean");

System.out.println("baseContext get subContext Bean " + obj4);

首先建立baseContext沒有爲其設置父容器,接着能夠成功獲取id爲baseBean的Bean,接着建立subContext並將baseContext設置爲其父容器,subContext能夠成功獲取baseBean以及subBean,最後試圖使用baseContext去獲取subContext中定義的subBean,此時會拋出異常NoSuchBeanDefinitionException,因而可知,父子容器相似於類的繼承關係,子類能夠訪問父類中的成員變量,而父類不可訪問子類的成員變量,一樣的,子容器能夠訪問父容器中定義的Bean,但父容器沒法訪問子容器定義的Bean。
經過上述實驗咱們能夠理解爲什麼須要建立多個Ioc容器,根IoC容器作爲全局共享的IoC容器放入Web應用須要共享的Bean,而子IoC容器根據需求的不一樣,放入不一樣的Bean,這樣可以作到隔離,保證系統的安全性。


接下來繼續講解DispatcherServlet類的子IoC容器建立過程,若是當前Servlet存在一個IoC容器則爲其設置根IoC容器做爲其父類,並配置刷新該容器,用於構造其定義的Bean,這裏的方法與前文講述的根IoC容器相似,一樣會讀取用戶在web.xml中配置的<servlet>中的<init-param>值,用於查找相關的xml配置文件用於構造定義的Bean,這裏再也不贅述了。若是當前Servlet不存在一個子IoC容器就去查找一個,若是仍然沒有查找到則調用
createWebApplicationContext()方法去建立一個,查看該方法的源碼以下圖所示:

clipboard.png

該方法用於建立一個子IoC容器並將根IoC容器作爲其父容器,接着進行配置和刷新操做用於構造相關的Bean。至此,根IoC容器以及相關Servlet的子IoC容器已經配置完成,子容器中管理的Bean通常只被該Servlet使用,所以,其中管理的Bean通常是「局部」的,如SpringMVC中須要的各類重要組件,包括Controller、Interceptor、Converter、ExceptionResolver等。相關關係以下圖所示:

clipboard.png

當IoC子容器構造完成後調用了onRefresh()方法,該方法的調用與initServletBean()方法的調用相同,由父類調用但具體實現由子類覆蓋,調用onRefresh()方法時將前文建立的IoC子容器做爲參數傳入,查看DispatcherServletBean類的onRefresh()方法源碼以下:

@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) {
        initMultipartResolver(context);
        initLocaleResolver(context);
        initThemeResolver(context);
        initHandlerMappings(context);
        initHandlerAdapters(context);
        initHandlerExceptionResolvers(context);
        initRequestToViewNameTranslator(context);
        initViewResolvers(context);
        initFlashMapManager(context);
    }

onRefresh()方法直接調用了initStrategies()方法,源碼如上,經過函數名能夠判斷,該方法用於初始化建立multipartResovle來支持圖片等文件的上傳、本地化解析器、主題解析器、HandlerMapping處理器映射器、HandlerAdapter處理器適配器、異常解析器、視圖解析器、flashMap管理器等,這些組件都是SpringMVC開發中的重要組件,相關組件的初始化建立過程均在此完成。因爲篇幅問題本文再也不進行更深刻的探討,有興趣的讀者能夠閱讀本系列文章的其餘博客內容。至此,DispatcherServlet類的建立和初始化過程也就結束了,整個Web應用部署到容器後的初始化啓動過程的重要部分所有分析清楚了,經過前文的分析咱們能夠認識到層次化設計的優勢,以及IoC容器的繼承關係所表現的隔離性。分析源碼能讓咱們更清楚的理解和認識到相關初始化邏輯以及配置文件的配置原理。

相關文章
相關標籤/搜索