Spring Web MVC 5源碼分析(1)——DispatcherServlet的加載過程

本文中使用的Spring框架的版本爲5.1前端

前置知識

從設計上說,Spring Web MVC 使用前端控制器模式圍繞一箇中心Servlet進行設計,這個中心Servlet就是DispatcherServlet,在DispatcherServlet中提供了用於處理請求的通用邏輯,而具體工做委託給可配置的組件執行,經過這種模式使得Spring Web MVC框架變得很是靈活。java

與其它的Servlet同樣,DispatcherServlet須要根據Servlet規範使用Java代碼或者在web.xml文件中進行配置。web

Spring Web MVC在啓動時,最早加載DispatcherServlet,而後DispatcherServlet再根據配置加載請求映射、視圖解析、異常處理所需的組件。spring

下面是在web.xml文件中對DispatcherServlet進行配置的示例:app

<web-app>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/app-context.xml</param-value>
    </context-param>

    <servlet>
        <servlet-name>app</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>app</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>

</web-app>
複製代碼

一些完成具體工做的Bean

在對請求進行處理時,DispatcherServlet會把委託下面這些Bean對請求進行處理,以及給出適當的響應結果。框架

HandlerMappingide

將請求與handler進行映射,HandlerMapping有兩個主要的實現:函數

  • RequestMappingHandlerMapping
  • SimpleUrlHandlerMapping

前者用於對@RequestMapping註解提供支持,後者支持控制器的顯示註冊。源碼分析

HandlerAdapter佈局

幫助DispatcherServlet調用與請求路徑匹配的handler去處理請求。

經過使用適配器的方式使DispatcherServlet不用關心handler的具體實現細節,好比:調用@Controller註解的控制器須要對該註解進行處理。

HandlerExceptionResolver

使用不一樣的策略對異常進行處理,好比:返回的HTTP狀態碼是5xx仍是4xx。

ViewResolver

對視圖進行解析,好比對JSP與FreeMarker的模版文件進行解析。

LocaleResolver, LocaleContextResolver

提供本地化支持,好比:多國語言、時區等。

ThemeResolver

提供主題支持,用於個性化佈局。

MultipartResolver

用於解析multi-part請求。

FlashMapManager

用於管理存放Falsh AttributeFlashMapFalsh Attribute用於跨請求傳遞數據。

源碼分析

從源碼中能夠找到DispatcherServlet類的定義以下:

public class DispatcherServlet extends FrameworkServlet 複製代碼

能夠看出,它繼承自類FrameworkServlet,下面咱們再來看一下類的總體繼承關係:

從類的繼承關係來看,最後會經過實現Servlet接口來處理HTTP請求,其中與Servlet有關係的類按繼承順序從上至下分別是:

  • GenericServlet,一個抽象類,實現了Servlet接口。
  • HttpServlet,一個抽象類,繼承自GenericServlet
  • HttpSerlvetBean,一個抽象類,繼承自HttpServlet
  • FrameworkServlet,一個抽象類,繼承自HttpServlet
  • DispatcherServlet,一個具體類,繼承自FrameworkServlet

其中GenericServletHttpServlet類只是簡單對Servlet接口作了一些封裝與擴展,所以能夠把分析的重點放在HttpSerlvetBeanFrameworkServletDispatcherServlet這三個類上面。

DispatcherServlet初始化時,這三個類之間調用順序以下圖所示:

根據Servlet規範,在Servlet接口中會存在一個init()方法,在一個Servlet實被例化以後容器將會調用一次該方法。Spring Web MVC經過在HttpSerlvetBean類中覆寫init()方法從而實現整個框架的加載。

HttpServletBean類

HttpSerlvetBean類的init()方法中主要作了兩件事,一是從web.xml文件中讀取初始化參數,好比:contextConfigLocation參數。二是調用由子類實現的initServletBean()方法完成具體的初始化工做。

init()方法的實現以下:

@Override
public final void init() throws ServletException {

    // 從web.xml文件中讀取初始化參數
    PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
            initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        }
        catch (BeansException ex) {
            if (logger.isErrorEnabled()) {
            	logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
            }
            throw ex;
        }
    }

    // 調用由子類實現的`initServletBean()`方法。
    initServletBean();
}
複製代碼

在上面這段代碼中有點意思的是從web.xml文件中讀取初始化參數的方式,在這裏咱們拿contextConfigLocation參數來舉例子。

對於參數contextConfigLocation而言,在HttpServletBean的子類FrameworkServlet中存在一個具備如下定義的私有變量:

@Nullable
private String contextConfigLocation;
複製代碼

以及對應的Get與Set方法:

public void setContextConfigLocation(@Nullable String contextConfigLocation) {
	this.contextConfigLocation = contextConfigLocation;
}

@Nullable
public String getContextConfigLocation() {
	return this.contextConfigLocation;
}
複製代碼

可是Spring並不會直接調用setContextConfigLocation()方法來給contextConfigLocation變量賦值,而是由BeanWrapper搭配ResourceEditor來給變量賦值。

init()方法在讀取初始化參數以後,便會調用initServletBean()方法來作初始化工做,該方法的在HttpServletBean中是一個被protected修飾的空方法,其定義以下:

protected void initServletBean() throws ServletException 複製代碼

而具體的初始化工做則在HttpServletBean的子類FrameworkServlet中經過覆寫initServletBean()方法來完成。

FrameworkServlet類

initServletBean()方法的實現以下:

protected final void initServletBean() throws ServletException {
    try {
        // 初始化WebApplicationContext
    	this.webApplicationContext = initWebApplicationContext();
    	
    	// 一個Hook,讓子類有機會在上下文初始化後作一些相關的工做
    	initFrameworkServlet();
    }
    catch (ServletException | RuntimeException ex) {
    	logger.error("Context initialization failed", ex);
    	throw ex;
    }
}
複製代碼

在該方法中主要作了下面兩件事:

  • 調用initWebApplicationContext()方法初始化webApplicationContext
  • 調用預留的initFrameworkServlet()方法讓子類在初始化以後有機會作一些額外的工做。

事實上,目前initFrameworkServlet()是一個沒有使用的空函數,而且子類也沒有對它進行覆寫,因此咱們只須要關注initWebApplicationContext()方法便可。

initWebApplicationContext()方法的實現以下:

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
            WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    if (this.webApplicationContext != null) {
        // 實例化時若是提供了webApplicationContext參數,則使用它。
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // 若是該上下文尚未進行過refresh則爲它設置一個ID並進行refresh
                if (cwac.getParent() == null) {
                    // 若是該的上下文沒有父上下文則爲它設置一個。
                    cwac.setParent(rootContext);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // 若是實例化時沒有提供上下文,則查找ServletContext中有沒有提供。
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // 若是上面沒找到一個已經存在的上下文,則本身建立一個
        wac = createWebApplicationContext(rootContext);
    }

    // 若是onRefresh尚未被調用,則手動調用一次
    if (!this.refreshEventReceived) {
        synchronized (this.onRefreshMonitor) {
            // 調用子類實現的onRefresh初始化策略對象
            onRefresh(wac);
        }
    }

    if (this.publishContext) {
        // 將WebApplicationContext放入ServletContext之中
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}
複製代碼

這個方法中主要作了下面這些事:

第一,檢查是否是經過構造函數FrameworkServlet(WebApplicationContext webApplicationContext)傳入了webApplicationContext參數。若是是,則會對傳入的webApplicationContext進行一些配置,好比:設置父上下文與上下文ID。

第二,檢查ServletContext中有沒有提供webApplicationContext。若是有,拿過來直接使用。

第三,若是在第一步與第二步中都沒有發現可用的webApplicationContext,那就調用createWebApplicationContext()方法本身建立一個。

第四,作一次兜底,經過refreshEventReceived變量的值判斷是否調用過onRefresh()方法,若是從未調用過,則觸發一次調用。

onRefresh方法除了在initWebApplicationContext()方法中調用了以外,在FrameworkServletonApplicationEvent()方法中也調用了onRefresh方法。

onApplicationEvent()方法的實現以下:

public void onApplicationEvent(ContextRefreshedEvent event) {
    this.refreshEventReceived = true;
    synchronized (this.onRefreshMonitor) {
        onRefresh(event.getApplicationContext());
    }
}
複製代碼

在上面的代碼中除了調用onRefresh()方法以外,還給變量refreshEventReceived賦值爲真,確保onRefresh()方法只會被調用一次。

那麼問題來了,又是誰在調用onApplicationEvent()方法?

對於這個問題咱們先來看一下用於建立webApplicationContextcreateWebApplicationContext()方法的實現:

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    Class<?> contextClass = getContextClass();
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException(
                "Fatal initialization error in servlet with name '" + getServletName() +
                "': custom WebApplicationContext class [" + contextClass.getName() +
                "] is not of type ConfigurableWebApplicationContext");
    }
    ConfigurableWebApplicationContext wac =
            (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    wac.setEnvironment(getEnvironment());
    wac.setParent(parent);
    // 使用configLocation所指定的配置文件
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    // 對webApplicationContext進行配置
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}
複製代碼

在這個方法中,主要作了兩件事:一是使用configLocation所指定的配置文件來加載Bean,二是調用configureAndRefreshWebApplicationContext()方法對webApplicationContext進行配置。

若是你還有印象的話,你可能記得下面這段位於initWebApplicationContext()方法中的代碼也調用過 configureAndRefreshWebApplicationContext()方法:

if (this.webApplicationContext != null) {
    // 實例化時若是提供了webApplicationContext參數,則使用它。
    wac = this.webApplicationContext;
    if (wac instanceof ConfigurableWebApplicationContext) {
        ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
        if (!cwac.isActive()) {
            // 若是該上下文尚未進行過refresh則爲它設置一個ID並進行refresh
            if (cwac.getParent() == null) {
                // 若是該的上下文沒有父上下文則爲它設置一個。
                cwac.setParent(rootContext);
            }
            // 對webApplicationContext進行配置
            configureAndRefreshWebApplicationContext(cwac);
        }
    }
}
複製代碼

因而乎,咱們要研究一下在configureAndRefreshWebApplicationContext()方法中作了哪些鮮爲人知的事情。

configureAndRefreshWebApplicationContext()方法的實現以下:

protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
    if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
        // 若是上下文的ID原裝的,則爲其設置一個更具可讀性的ID
        // 使用配置文件指定的ID
        if (this.contextId != null) {
            wac.setId(this.contextId);
        }
        else {
            // 生成一個默認的ID
            wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
                    ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
        }
    }

    wac.setServletContext(getServletContext());
    wac.setServletConfig(getServletConfig());
    wac.setNamespace(getNamespace());
    // 添加一個Listener用於監聽webApplicationContext的refresh事件
    wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));

    // 初始化PropertySource
    ConfigurableEnvironment env = wac.getEnvironment();
    if (env instanceof ConfigurableWebEnvironment) {
        ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
    }

    postProcessWebApplicationContext(wac);
    
    // 調用ApplicationContextInitializer
    applyInitializers(wac);
    
    // 調用webApplicationContext的refresh方法
    wac.refresh();
}
複製代碼

在上面的代碼中作了這麼幾件事:

  • 第一步,爲webApplicationContext設置一個ID。
  • 第二步,添加一個ContextRefreshListener用於監聽webApplicationContext的refresh事件。
  • 第三步,初始化PropertySource。
  • 第四步,調用webApplicationContext的refresh方法。

在上面第二步中,ContextRefreshListener類的實現以下:

private class ContextRefreshListener implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        FrameworkServlet.this.onApplicationEvent(event);
    }
}
複製代碼

經過這個類的實現能夠看出,這個類做爲FrameworkServlet的內部類在收到上下文已刷新的事件後會調用onApplicationEvent()方法。

如今咱們已經能夠前面的問題了:是誰在調用onApplicationEvent()方法?。

經過上述的分析咱們能夠知道在configureAndRefreshWebApplicationContext()方法中對webApplicationContext設置了一個監聽器,這個監聽器在監聽到上下文refresh以後會調用onApplicationEvent()方法。

既然onApplicationEvent()方法與initWebApplicationContext()都會調用onRefresh()方法,那麼在onRefresh()方法中又作了哪些事情?

onRefresh()方法在FrameworkServlet中的定義以下:

protected void onRefresh(ApplicationContext context) {
}
複製代碼

可見,它在FrameworkServlet類中是一個空方法。具體邏輯由FrameworkServlet的子類DispatcherServlet覆寫這個方法來實現,下文將對它的具體實現進行分析。

DispatcherServlet類

DispatcherServlet類中onRefresh()方法的實現以下:

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

protected void initStrategies(ApplicationContext context) {
    // 初始化解析multi-part請求須要的組件
    initMultipartResolver(context);
    // 初始化用於提供本地化支持的主鍵
    initLocaleResolver(context);
    // 初始化用於提供主題支持的組件
    initThemeResolver(context);
    // 初始化用於請求映射的組件
    initHandlerMappings(context);
    // 初始化用於對handler進行適配的組件
    initHandlerAdapters(context);
    // 初始化用於異常處理的組件
    initHandlerExceptionResolvers(context);
    // 初始化用於視圖解析的組件
    initRequestToViewNameTranslator(context);
    initViewResolvers(context);
    // 初始化用於管理Flash Attribute的組件
    initFlashMapManager(context);
}
複製代碼

經過上面的代碼中能夠看出,onRefresh()方法將具體的工做委託給了initStrategies()方法。

initStrategies()方法中,初始化了一系列的策略對象用於對不一樣的功能提供支持,好比:請求映射、視圖解析、異常處理等。

至此,DispatcherServlet初始化完畢。

未完,待續。

相關文章
相關標籤/搜索