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

實戰java 發佈 2024-05-07T05:52:57.127686+00:00

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



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


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


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


圖1 tomcat web應用啟動過程


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

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


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

圖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的類聲明如圖:


圖3 ContextLoaderListener類源碼


ContextLoaderListener類繼承自ContextLoader類,並實現了ServletContextListener接口。


圖4 ServletContextListener源碼


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


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


圖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();


圖6 Servlet源碼


在spring mvc中,實現了一個非常重要的servlet,即DispatcherServlet。它是整個spring mvc應用的核心,用於獲取分發用戶請求並返迴響應。

圖7 DispatcherServlet類圖


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


圖8 DispatcherServlet類初始化過程


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


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


圖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()方法去創建一個。


圖10 createWebApplicationContext源碼


該方法用於創建一個子IOC容器並將根IOC容器做為其父容器,接著執行配置和刷新操作構建相關的Bean。


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


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

圖11 spring mvc父子容器


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


圖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上,兩種方式啟動的源碼有所不同,還需要分別分析。

關鍵字: