天天看點

深入了解spring mvc啟動過程與原理

作者:實戰Java
深入了解spring mvc啟動過程與原理

spring mvc的啟動,是跟随着tomcat啟動的,是以要深入了解spring mvc的啟動過程與原理,需要先了解下tomcat啟動的一些關鍵過程。

1、tomcat web應用啟動及初始化過程

參考官方文檔,tomcat web應用啟動過程是這樣的:

深入了解spring mvc啟動過程與原理

圖1 tomcat web應用啟動過程

大概意思就是,當一個Web應用部署到容器内時,在web應用開始執行使用者請求前,會依次執行以下步驟:

  • 部署描述檔案web.xml中<listener>元素标記的事件監聽器會被建立和初始化;
  • 對于所有事件監聽器,如果實作了ServletContextListener接口,将會執行其實作的contextInitialized()方法;
  • 部署描述檔案中由<filter>元素标記的過濾器會被建立和初始化,并調用其init()方法;
  • 部署描述檔案中由<servlet>元素标記的servlet會根據<load-on-startup>的權值按順序建立和初始化,并調用其init()方法;

通過上述文檔的描述,可知tomcat web應用啟動初始化流程是這樣的:

深入了解spring mvc啟動過程與原理

圖2 tomcat web應用初始化過程

可以看出,在tomcat web應用的初始化流程是,先初始化listener,接着初始化filter,最後初始化servlet。

2、spring mvc應用的啟動初始化

做過spring mvc項目開發的夥伴,都會配置一個web.xml配置檔案,内容一般是這樣的:

<?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_2_5.xsd" version="2.5">
    <!--全局變量配置-->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:spring/spring-main.xml
    </param-value>
    </context-param>
    <!--監聽器-->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <listener>
        <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>
    <!--解決亂碼問題的filter-->
    <filter>
        <filter-name>encodingFilter</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>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- MVC Servlet -->
    <servlet>
        <servlet-name>springServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath*:spring/spring-mvc*.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>springServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>           

web.xml配置檔案中也主要是配置了Listener,Filter,Servlet。

是以spring mvc應用啟動的時候,主要是在這三大元件初始化的過程中完成對spring及spring mvc的初始化。

3、Listener與spring的初始化過程

web.xml配置檔案中首先定義了<context-param>标簽,用于配置一個全局變量,<context-param>标簽的内容讀取後會做為Web應用的全局變量使用。當Listener建立的時候,會使用到這個全局變量,是以,Web應用在容器中部署後,進行初始化時會先讀取這個全局變量,之後再進行初始化過程。

接着定義了一個ContextLoaderListener類的Listener。檢視ContextLoaderListener的類聲明如圖:

深入了解spring mvc啟動過程與原理

圖3 ContextLoaderListener類源碼

ContextLoaderListener類繼承自ContextLoader類,并實作了ServletContextListener接口。

深入了解spring mvc啟動過程與原理

圖4 ServletContextListener源碼

ServletContextListener隻有兩個方法,contextInitialized和contextDestroyed,當Web應用初始化或銷毀時會分别調用這兩個方法。

ContextLoaderListener實作了ServletContextListener接口,是以在Web應用初始化時會調用contextInitialized方法,該方法的具體實作如下:

深入了解spring mvc啟動過程與原理

圖5 contextInitialized方法

ContextLoaderListener的contextInitialized()方法直接調用了initWebApplicationContext()方法,這個方法是繼承自ContextLoader類,通過函數名可以知道,該方法是用于初始化web應用上下文,即IOC容器。

這裡是spring mvc應用啟動的第一個重點,就是在ContextLoaderListener初始化的時候,初始化了spring IOC容器。

我們繼續看ContextLoader類的initWebApplicationContext()方法。

//servletContext,servlet上下文
  public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
  //  首先通過WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
  //  這個String類型的靜态變量擷取一個根IoC容器,根IoC容器作為全局變量
  //  存儲在servletContext對象中,如果存在則有且隻能有一個
  //  如果在初始化根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!");
    }


    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.
     // 如果目前成員變量中不存在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容器放入到servletContext對象中,并設定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.isInfoEnabled()) {
        long elapsedTime = System.currentTimeMillis() - startTime;
        logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
      }


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

在jsp中,可以通過這兩種方法擷取到IOC容器:

WebApplicationContext applicationContext = (WebApplicationContext) servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);


WebApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());           

總的來說,initWebApplicationContext方法的主要目的是建立root WebApplicationContext對象,即根IOC容器。

如果整個Web應用存在根IOC容器則有且隻能有一個,根IOC容器會作為全局變量存儲在ServletContext對象中。然後将根IOC容器放入到ServletContext對象之前進行了IOC容器的配置和重新整理操作,即調用configureAndRefreshWebApplicationContext()方法,該方法源碼如下:

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);
  // 擷取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();
}           

configureAndRefreshWebApplicationContext方法裡擷取了<context-param>标簽配置的全局變量,并且在方法最後調用了refresh()方法。

對spring容器初始化有一定了解的同學都知道,這是初始化spring容器的入口方法,其最終調用的是AbstractApplicationContext類中的refresh方法。

refresh()方法主要是建立并初始化contextConfigLocation類配置的xml檔案中的Bean。

具體代碼不貼了,有興趣的同學,可以自行查閱。

到此為止,整個ContextLoaderListener類的啟動過程就結束了,可以發現,建立ContextLoaderListener是比較重要的一個步驟,主要做的事情就是建立根IOC容器,并使用特定的key将其放入到servletContext對象中,供整個Web應用使用。

由于在ContextLoaderListener類中構造的根IOC容器配置的Bean是全局共享的,是以,在<context-param>辨別的contextConfigLocation的xml配置檔案一般包括:資料庫DataSource、DAO層、Service層、事務等相關Bean。

4、Filter與spring的初始化

在監聽器Listener初始化完成後,接下來會進行Filter的初始化操作,Filter的建立和初始化沒有涉及IOC容器的相關操作,是以不是本文講解的重點。

5、Servlet與spring的初始化

web應用啟動的最後一個步驟就是建立和初始化相關servlet,servlet最重要的方法就是:init(),service(),destroy();

深入了解spring mvc啟動過程與原理

圖6 Servlet源碼

在spring mvc中,實作了一個非常重要的servlet,即DispatcherServlet。它是整個spring mvc應用的核心,用于擷取分發使用者請求并傳回響應。

深入了解spring mvc啟動過程與原理

圖7 DispatcherServlet類圖

DispatcherServlet本質上也是一個Servlet,其源碼實作充分利用了模闆模式,将不變的部分統一實作,将變化的部分留給子類實作,上層父類不同程度的實作了相關接口的部分方法,留出了相關方法由子類覆寫。

深入了解spring mvc啟動過程與原理

圖8 DispatcherServlet類初始化過程

web應用部署到容器啟動後,進行Servlet的初始化時會調用相關的init(ServletConfig)方法,是以,DispatchServlet類的初始化過程也由該方法開始。

其中比較重要的是,FrameworkServlet類中的initServletBean()方法、initWebApplicationContext()方法以及DispatcherServlet類中的onRefresh()方法。

深入了解spring mvc啟動過程與原理

圖9 FrameworkServlet類中的initServletBean()方法

FrameworkServlet的父類HttpServletBean中的initServletBean()方法,HttpServletBean抽象類在執行init()方法時會調用initServletBean()方法。

該方法中比較重要的是initWebApplicationContext()方法,該方法仍由FrameworkServlet抽象類實作,繼續檢視其源碼如下所示:

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
      // 如果仍舊沒有查找到子IoC容器則建立一個子IoC容器
      wac = findWebApplicationContext();
    }
    if (wac == null) {
      // No context instance is defined for this servlet -> create a local one
      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.
      synchronized (this.onRefreshMonitor) {
        // 調用子類覆寫的onRefresh方法完成“可變”的初始化過程
        onRefresh(wac);
      }
    }


    if (this.publishContext) {
      // Publish the context as a servlet context attribute.
      String attrName = getServletContextAttributeName();
      getServletContext().setAttribute(attrName, wac);
    }


    return wac;
  }           

通過函數名不難發現,該方法的主要作用同樣是建立一個WebApplicationContext對象,即IOC容器,不過這裡建立的是spring mvc容器,是前面ContextLoadListener建立的IOC容器的子容器。

為什麼需要多個IOC容器?還是父子容器?這就要說明下父子IOC容器的通路特性了。

父子容器類似于類的繼承關系,子類可以通路父類中的成員變量,而父類不可通路子類的成員變量,同樣的,子容器可以通路父容器中定義的Bean,但父容器無法通路子容器定義的Bean。

建立多個容器的目的是,父IOC容器做為全局共享的IOC容器,存放Web應用共享的Bean,比如Service,DAO。而子IOC容器根據需求的不同,放入不同的Bean,比如Conroller,這樣能夠做到隔離,保證系統的安全性。

如果你看過sprin cloud netflix系列源碼,比如spring-cloud-netflix-ribbon,spring-cloud-openfeign,就會發現它裡邊建立了很多父子容器,作用與這裡是一樣的,容器與容器互相隔離,保證系統的安全性。

我們繼續講解DispatcherServlet類的子IOC容器建立過程,如果目前Servlet存在一個IOC容器則為其設定根IOC容器作為其父類,并重新整理該容器,用于初始化定義的Bean。

這裡的方法與前文講述的根IOC容器類似,同樣會讀取使用者在web.xml中配置的<servlet>中的<init-param>值,用于查找相關的xml配置檔案來建立定義的Bean。如果目前Servlet不存在一個子IoC容器就去查找一個,如果沒有查找到,則調用createWebApplicationContext()方法去建立一個。

深入了解spring mvc啟動過程與原理

圖10 createWebApplicationContext源碼

該方法用于建立一個子IOC容器并将根IOC容器做為其父容器,接着執行配置和重新整理操作建構相關的Bean。

至此,根IOC容器以及相關Servlet的子IOC容器已經初始化完成了,子容器中管理的Bean一般隻被該Servlet使用,比如SpringMVC中需要的各種重要元件,包括Controller、Interceptor、Converter、ExceptionResolver等。

spring mvc父子容器之間的關系,可以用下圖來描述:

深入了解spring mvc啟動過程與原理

圖11 spring mvc父子容器

當IOC子容器構造完成後調用了onRefresh()方法,其實作是在子類DispatcherServlet中,檢視DispatcherServletBean類的onRefresh()方法源碼如下:

深入了解spring mvc啟動過程與原理

圖12 onRefresh方法

onRefresh()方法調用了initStrategies()方法,通過函數名可以判斷,該方法主要初始化建立multipartResovle來支援圖檔等檔案的上傳、本地化解析器、主題解析器、HandlerMapping處理器映射器、HandlerAdapter處理器擴充卡、異常解析器、視圖解析器、flashMap管理器等。這些元件都是SpringMVC開發中的重要元件,相關元件的初始化建立過程均在此完成,是以在這裡最終完成了spring mvc元件的初始化。

至此,整個spring mvc應用啟動的原理以及過程就講完了,總的來說,spring以及spring mvc的初始化過程是跟随在tomcat的Listener、Filter、Servlet的初始化過程完成的。

6、總結

spring以及spring mvc應用初始化過程還是比較清晰明了的,本文做了完整的分析記錄。spring mvc也支援無web.xml配置檔案的方式開發web應用,其原理是類似的,隻不過是用代碼的方式來建立了web.xml中需要的元件。

後續會分析spring boot應用的啟動以及初始化過程分析,spring boot是在spring及spring mvc的基礎上做了更深的封裝,是以看起來也更複雜。spring boot應用既可以以war包的方式運作在tomcat上,也可以以jar的方式運作再内嵌的tomcat上,兩種方式啟動的源碼有所不同,還需要分别分析。