天天看點

SpringMVC源碼剖析(三)- DispatcherServlet的初始化流程

在我們第一次學Servlet程式設計,學java web的時候,還沒有那麼多架構。我們開發一個簡單的功能要做的事情很簡單,就是繼承HttpServlet,根據需要重寫一下doGet,doPost方法,跳轉到我們定義好的jsp頁面。Servlet類編寫完之後在web.xml裡注冊這個Servlet類。

除此之外,沒有其他了。我們啟動web伺服器,在浏覽器中輸入位址,就可以看到浏覽器上輸出我們寫好的頁面。為了更好的了解上面這個過程,你需要學習關于Servlet生命周期的三個階段,就是所謂的“init-service-destroy”。

以上的知識,我覺得對于你了解SpringMVC的設計思想,已經足夠了。SpringMVC當然可以稱得上是一個複雜的架構,但是同時它又遵循Servlet世界裡最簡單的法則,那就是“init-service-destroy”。我們要分析SpringMVC的初始化流程,其實就是分析DispatcherServlet類的init()方法,讓我們帶着這種單純的觀點,打開DispatcherServlet的源碼一窺究竟吧。

1.配置元素讀取

用Eclipse IDE打開DispatcherServlet類的源碼,ctrl+T看一下。

SpringMVC源碼剖析(三)- DispatcherServlet的初始化流程

DispatcherServlet類的初始化入口方法init()定義在HttpServletBean這個父類中,HttpServletBean類作為一個直接繼承于HttpServlet類的類,覆寫了HttpServlet類的init()方法,實作了自己的初始化行為。

@Override
public final void init() throws ServletException {
  if (logger.isDebugEnabled()) {
    logger.debug("Initializing servlet '" + getServletName() + "'");
  }

  // Set bean properties from init parameters.
  try {
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
    ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
    bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.environment));
    initBeanWrapper(bw);
    bw.setPropertyValues(pvs, true);
  } catch (BeansException ex) {
    logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
    throw ex;
  }

  // Let subclasses do whatever initialization they like.
  initServletBean();

  if (logger.isDebugEnabled()) {
    logger.debug("Servlet '" + getServletName() + "' configured successfully");
  }
}      

這裡的initServletBean()方法在HttpServletBean類中是一個沒有任何實作的空方法,它的目的就是留待子類實作自己的初始化邏輯,也就是我們常說的模闆方法設計模式。SpringMVC在此生動的運用了這個模式,init()方法就是模版方法模式中的模闆方法,SpringMVC真正的初始化過程,由子類FrameworkServlet中覆寫的initServletBean()方法觸發。

再看一下init()方法内被try,catch塊包裹的代碼,裡面涉及到BeanWrapper,PropertyValues,ResourceEditor這些Spring内部非常底層的類。要深究具體代碼實作上面的細節,需要對Spring架構源碼具有相當深入的了解。我們這裡先避繁就簡,從代碼效果和設計思想上面來分析這段try,catch塊内的代碼所做的事情:

  • 注冊一個字元串到資源檔案的編輯器,讓Servlet下面的<init-param>配置元素可以使用形如“classpath:”這種方式指定SpringMVC架構bean配置檔案的來源。
  • 将web.xml中在DispatcherServlet這個Servlet下面的<init-param>配置元素利用JavaBean的方式(即通過setter方法)讀取到DispatcherServlet中來。

這兩點,我想通過下面一個例子來說明一下。

我在web.xml中注冊的DispatcherServlet配置如下:

<!-- springMVC配置開始 -->
<servlet>
  <servlet-name>appServlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:spring/spring-servlet.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
  <servlet-name>appServlet</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>
<!-- springMVC配置結束 -->      

可以看到,我注冊了一個名為contextConfigLocation的<init-param>元素,其值為“classpath:spring/spring-servlet.xml”,這也是大家常常用來指定SpringMVC配置檔案路徑的方法。上面那段try,catch塊包裹的代碼發揮的作用,一個是将“classpath:spring/spring-servlet.xml”這段字元串轉換成classpath路徑下的一個資源檔案,供架構初始化讀取配置元素。在我的工程中是在spring檔案夾下面的配置檔案spring-servlet.xml。

SpringMVC源碼剖析(三)- DispatcherServlet的初始化流程

另外一個作用,就是将contextConfigLocation的值讀取出來,然後通過setContextConfigLocation()方法設定到DispatcherServlet中,這個setContextConfigLocation()方法是在FrameworkServlet類中定義的,也就是上面繼承類圖中DispatcherServlet的直接父類。

我們在setContextConfigLocation()方法上面打上一個斷點,啟動web工程,可以看到下面的調試結果。

SpringMVC源碼剖析(三)- DispatcherServlet的初始化流程

HttpServletBean類的作者是大名鼎鼎的Spring之父Rod Johnson。作為POJO程式設計哲學的大師,他在HttpServletBean這個類的設計中,運用了依賴注入思想完成了<init-param>配置元素的讀取。他抽離出HttpServletBean這個類的目的也在于此,就是“以依賴注入的方式來讀取Servlet類的<init-param>配置資訊”,而且這裡很明顯是一種setter注入。

明白了HttpServletBean類的設計思想,我們也就知道可以如何從中獲益。具體來說,我們繼承HttpServletBean類(就像DispatcherServlet做的那樣),在類中定義一個屬性,為這個屬性加上setter方法後,我們就可以在<init-param>元素中為其定義值。在類被初始化後,值就會被注入進來,我們可以直接使用它,避免了樣闆式的getInitParameter()方法的使用,而且還免費享有Spring中資源編輯器的功能,可以在web.xml中,通過“classpath:”直接指定類路徑下的資源檔案。

注意,雖然SpringMVC本身為了後面初始化上下文的友善,使用了字元串來聲明和設定contextConfigLocation參數,但是将其聲明為Resource類型,同樣能夠成功擷取。鼓勵讀者們自己繼承HttpServletBean寫一個測試用的Servlet類,并設定一個參數來調試一下,這樣能夠幫助你更好的了解擷取配置參數的過程。

2.容器上下文的建立

上一篇文章中提到過,SpringMVC使用了Spring容器來容納自己的配置元素,擁有自己的bean容器上下文。在SpringMVC初始化的過程中,非常關鍵的一步就是要建立起這個容器上下文,而這個建立上下文的過程,發生在FrameworkServlet類中,由上面init()方法中的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 {
    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");
  }
}      

initFrameworkServlet()方法是一個沒有任何實作的空方法,除去一些樣闆式的代碼,那麼這個initServletBean()方法所做的事情已經非常明白:

this.webApplicationContext = initWebApplicationContext();      

這一句簡單直白的代碼,道破了FrameworkServlet這個類,在SpringMVC類體系中的設計目的,它是 

用來抽離出建立

WebApplicationContext

上下文這個過程的

initWebApplicationContext()方法,封裝了建立Spring容器上下文的整個過程,方法内的邏輯如下:

  1. 擷取由ContextLoaderListener初始化并注冊在ServletContext中的根上下文,記為rootContext
  2. 如果webApplicationContext已經不為空,表示這個Servlet類是通過程式設計式注冊到容器中的(Servlet 3.0+中的ServletContext.addServlet() ),上下文也由程式設計式傳入。若這個傳入的上下文還沒被初始化,将rootContext上下文設定為它的父上下文,然後将其初始化,否則直接使用。
  3. 通過wac變量的引用是否為null,判斷第2步中是否已經完成上下文的設定(即上下文是否已經用程式設計式方式傳入),如果wac==null成立,說明該Servlet不是由程式設計式注冊到容器中的。此時以contextAttribute屬性的值為鍵,在ServletContext中查找上下文,查找得到,說明上下文已經以别的方式初始化并注冊在contextAttribute下,直接使用。
  4. 檢查wac變量的引用是否為null,如果wac==null成立,說明2、3兩步中的上下文初始化政策都沒成功,此時調用createWebApplicationContext(rootContext),建立一個全新的以rootContext為父上下文的上下文,作為SpringMVC配置元素的容器上下文。大多數情況下我們所使用的上下文,就是這個建立的上下文。
  5. 以上三種初始化上下文的政策,都會回調onRefresh(ApplicationContext context)方法(回調的方式根據不同政策有不同),onRefresh方法在DispatcherServlet類中被覆寫,以上面得到的上下文為依托,完成SpringMVC中預設實作類的初始化。
  6. 最後,将這個上下文釋出到ServletContext中,也就是将上下文以一個和Servlet類在web.xml中注冊名字有關的值為鍵,設定為ServletContext的一個屬性。你可以通過改變publishContext的值來決定是否釋出到ServletContext中,預設為true。

以上面6點跟蹤FrameworkServlet類中的代碼,可以比較清晰的了解到整個容器上下文的建立過程,也就能夠領會到FrameworkServlet類的設計目的,它是用來建立一個和Servlet關聯的Spring容器上下文,并将其注冊到ServletContext中的。跳脫開SpringMVC體系,我們也能通過繼承FrameworkServlet類,得到與Spring容器整合的好處,FrameworkServlet和HttpServletBean一樣,是一個可以獨立使用的類。整個SpringMVC設計中,處處展現開閉原則,這裡顯然也是其中一點。

3.初始化SpringMVC預設實作類

初始化流程在FrameworkServlet類中流轉,建立了上下文後,通過onRefresh(ApplicationContext context)方法的回調,進入到DispatcherServlet類中。

@Override
protected void onRefresh(ApplicationContext context) {
  initStrategies(context);
}      

DispatcherServlet類覆寫了父類FrameworkServlet中的onRefresh(ApplicationContext context)方法,提供了SpringMVC各種程式設計元素的初始化。當然這些程式設計元素,都是作為容器上下文中一個個bean而存在的。具體的初始化政策,在initStrategies()方法中封裝。

protected void initStrategies(ApplicationContext context) {
    initMultipartResolver(context);
    initLocaleResolver(context);
    initThemeResolver(context);
    initHandlerMappings(context);
    initHandlerAdapters(context);
    initHandlerExceptionResolvers(context);
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    initFlashMapManager(context);
  }      

我們以其中initHandlerMappings(context)方法為例,分析一下這些SpringMVC程式設計元素的初始化政策,其他的方法,都是以類似的政策初始化的。

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;

    if (this.detectAllHandlerMappings) {
      // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
      Map<String, HandlerMapping> matchingBeans =
          BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
      if (!matchingBeans.isEmpty()) {
        this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
        // We keep HandlerMappings in sorted order.
        OrderComparator.sort(this.handlerMappings);
      }
    }
    else {
      try {
        HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
        this.handlerMappings = Collections.singletonList(hm);
      }
      catch (NoSuchBeanDefinitionException ex) {
        // Ignore, we'll add a default HandlerMapping later.
      }
    }

    // Ensure we have at least one HandlerMapping, by registering
    // a default HandlerMapping if no other mappings are found.
    if (this.handlerMappings == null) {
      this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
      if (logger.isDebugEnabled()) {
        logger.debug("No HandlerMappings found in servlet '" + getServletName() + "': using default");
      }
    }
  }      

detectAllHandlerMappings變量預設為true,是以在初始化HandlerMapping接口預設實作類的時候,會把上下文中所有HandlerMapping類型的Bean都注冊在handlerMappings這個List變量中。如果你手工将其設定為false,那麼将嘗試擷取名為handlerMapping的Bean,建立一個隻有一個元素的List,将其賦給handlerMappings。如果經過上面的過程,handlerMappings變量仍為空,那麼說明你沒有在上下文中提供自己HandlerMapping類型的Bean定義。此時,SpringMVC将采用預設初始化政策來初始化handlerMappings。

點進去getDefaultStrategies看一下。

@SuppressWarnings("unchecked")
  protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
    String key = strategyInterface.getName();
    String value = defaultStrategies.getProperty(key);
    if (value != null) {
      String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
      List<T> strategies = new ArrayList<T>(classNames.length);
      for (String className : classNames) {
        try {
          Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
          Object strategy = createDefaultStrategy(context, clazz);
          strategies.add((T) strategy);
        }
        catch (ClassNotFoundException ex) {
          throw new BeanInitializationException(
              "Could not find DispatcherServlet's default strategy class [" + className +
                  "] for interface [" + key + "]", ex);
        }
        catch (LinkageError err) {
          throw new BeanInitializationException(
              "Error loading DispatcherServlet's default strategy class [" + className +
                  "] for interface [" + key + "]: problem with class file or dependent class", err);
        }
      }
      return strategies;
    }
    else {
      return new LinkedList<T>();
    }
  }      

它是一個範型的方法,承擔所有SpringMVC程式設計元素的預設初始化政策。方法的内容比較直白,就是以傳遞類的名稱為鍵,從defaultStrategies這個Properties變量中擷取實作類,然後反射初始化。

需要說明一下的是defaultStrategies變量的初始化,它是在DispatcherServlet的靜态初始化代碼塊中加載的。

private static final Properties defaultStrategies;

  static {
    // Load default strategy implementations from properties file.
    // This is currently strictly internal and not meant to be customized
    // by application developers.
    try {
      ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
      defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
    }
    catch (IOException ex) {
      throw new IllegalStateException("Could not load 'DispatcherServlet.properties': " + ex.getMessage());
    }
  }      
private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties";      

這個DispatcherServlet.properties裡面,以鍵值對的方式,記錄了SpringMVC預設實作類,它在spring-webmvc-3.1.3.RELEASE.jar這個jar包内,在org.springframework.web.servlet包裡面。

# Default implementation classes for DispatcherServlet's strategy interfaces.
# Used as fallback when no matching beans are found in the DispatcherServlet context.
# Not meant to be customized by application developers.

org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver

org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver

org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
  org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping

org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
  org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
  org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\
  org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\
  org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager      

至此,我們分析完了initHandlerMappings(context)方法的執行過程,其他的初始化過程與這個方法非常類似。所有初始化方法執行完後,SpringMVC正式完成初始化,靜靜等待Web請求的到來。

4.總結

回顧整個SpringMVC的初始化流程,我們看到,通過HttpServletBean、FrameworkServlet、DispatcherServlet三個不同的類層次,SpringMVC的設計者将三種不同的職責分别抽象,運用模版方法設計模式分别固定在三個類層次中。其中HttpServletBean完成的是<init-param>配置元素的依賴注入,FrameworkServlet完成的是容器上下文的建立,DispatcherServlet完成的是SpringMVC具體程式設計元素的初始化政策。