SpringMVC源碼分析3:DispatcherServlet的初始化與請求轉發

在咱們第一次學Servlet編程,學java web的時候,尚未那麼多框架。咱們開發一個簡單的功能要作的事情很簡單,就是繼承HttpServlet,根據須要重寫一下doGet,doPost方法,跳轉到咱們定義好的jsp頁面。Servlet類編寫完以後在web.xml裏註冊這個Servlet類。java

除此以外,沒有其餘了。咱們啓動web服務器,在瀏覽器中輸入地址,就能夠看到瀏覽器上輸出咱們寫好的頁面。爲了更好的理解上面這個過程,你須要學習關於Servlet生命週期的三個階段,就是所謂的「init-service-destroy」。web

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

配置元素讀取

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

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下面的 配置元素可使用形如「classpath:」這種方式指定SpringMVC框架bean配置文件的來源。
  • 將web.xml中在DispatcherServlet這個Servlet下面的 配置元素利用JavaBean的方式(即經過setter方法)讀取到DispatcherServlet中來。

這兩點,我想經過下面一個例子來講明一下。mvc

我在web.xml中註冊的DispatcherServlet配置以下:app

<!-- 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的 元素,其值爲「classpath:spring/spring-servlet.xml」,這也是你們經常用來指定SpringMVC配置文件路徑的方法。上面那段try,catch塊包裹的代碼發揮的做用,一個是將「classpath:spring/spring-servlet.xml」這段字符串轉換成classpath路徑下的一個資源文件,供框架初始化讀取配置元素。在個人工程中是在spring文件夾下面的配置文件spring-servlet.xml。 框架

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

咱們在setContextConfigLocation()方法上面打上一個斷點,啓動web工程,能夠看到下面的調試結果。

HttpServletBean類的做者是大名鼎鼎的Spring之父Rod Johnson。做爲POJO編程哲學的大師,他在HttpServletBean這個類的設計中,運用了依賴注入思想完成了 配置元素的讀取。他抽離出HttpServletBean這個類的目的也在於此,就是「以依賴注入的方式來讀取Servlet類的 配置信息」,並且這裏很明顯是一種setter注入。

明白了HttpServletBean類的設計思想,咱們也就知道能夠如何從中獲益。具體來講,咱們繼承HttpServletBean類(就像DispatcherServlet作的那樣),在類中定義一個屬性,爲這個屬性加上setter方法後,咱們就能夠在 元素中爲其定義值。在類被初始化後,值就會被注入進來,咱們能夠直接使用它,避免了樣板式的getInitParameter()方法的使用,並且還免費享有Spring中資源編輯器的功能,能夠在web.xml中,經過「classpath:」直接指定類路徑下的資源文件。

注意,雖然SpringMVC自己爲了後面初始化上下文的方便,使用了字符串來聲明和設置contextConfigLocation參數,可是將其聲明爲Resource類型,一樣可以成功獲取。鼓勵讀者們本身繼承HttpServletBean寫一個測試用的Servlet類,並設置一個參數來調試一下,這樣可以幫助你更好的理解獲取配置參數的過程。

容器上下文的創建

上一篇文章中提到過,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成立,說明二、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設計中,到處體現開閉原則,這裏顯然也是其中一點。

初始化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請求的到來。

總結

回顧整個SpringMVC的初始化流程,咱們看到,經過HttpServletBean、FrameworkServlet、DispatcherServlet三個不一樣的類層次,SpringMVC的設計者將三種不一樣的職責分別抽象,運用模版方法設計模式分別固定在三個類層次中。其中HttpServletBean完成的是 配置元素的依賴注入,FrameworkServlet完成的是容器上下文的創建,DispatcherServlet完成的是SpringMVC具體編程元素的初始化策略。

圖片描述

相關文章
相關標籤/搜索