Spring源碼學習(六):Spring MVC的初始化過程

目錄

1.ContextLoaderListener

1.1 創建WebApplicationContext

1.2 設置和刷新WebApplicationContext

2.DispatcherServlet

2.1 init方法

2.2 initServletBean方法

2.3 OnRefresh方法

3.九大組件的註冊

3.1 文件上傳解析器MultipartResolver

3.2 本地化解析器LocaleResolver

3.3 主題解析器ThemeResolver

3.4 處理器映射器HandlerMapping

3.5 處理器適配器HandlerAdapter

3.5.1 @ControllerAdvice 與 initControllerAdviceCache

3.5.2 參數解析器 HandlerMethodArgumentResolver

3.5.3 @InitBinder的初始化

3.5.4 返回值解析器 HandlerMethodReturnValueHandler

3.6 處理器異常解析器HandlerExceptionResolver

3.7 視圖名翻譯器RequestToViewNameTranslator

3.8 視圖解析器ViewResolver

3.9 FlashMap管理器 FlashMapManager


Spring最常用的場景就是Web後臺開發,這就要使用到Spring MVC相關包:spring-web、spring-webmvc等。一個簡單的Spring MVC項目如下:

首先是web.xml,它配置了首頁、servlet、servlet-mapping、filter、listener等,Spring MVC通過加載該文件,獲取配置的Servlet,來攔截URL。下面的配置中,指定了Spring配置文件的位置,設置了DispatcherServlet及啓動級別,它將會在啓動後嘗試從WEB-INF下面加載 servletName-servlet.xml(斜粗體部分爲servlet-name配置的內容),listener部分配置了上下文載入器,用來載入其它上下文配置文件,然後配置了servlet映射,“/”表示它攔截所有類型的URL:

<web-app version="2.5" 
    xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
    http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>hello</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>hello</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

Spring也支持編程式配置DispatcherServlet,只要實現WebApplicationInitializer的onStartup方法,在裏面創建DispatcherServlet實例並註冊到ServletContext即可(例子來源於官方文檔):

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext servletCxt) {

        // Load Spring web application configuration
        AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
        ac.register(AppConfig.class);
        ac.refresh();

        // Create and register the DispatcherServlet
        DispatcherServlet servlet = new DispatcherServlet(ac);
        ServletRegistration.Dynamic registration = servletCxt.addServlet("app", servlet);
        registration.setLoadOnStartup(1);
        registration.addMapping("/app/*");
    }

然後是applicationContext.xml,它就是一個普通的Spring配置文件,一般會在這裏配置ViewResolver,下面是一個JSP配置:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
          p:viewClass="org.springframework.web.servlet.view.JstlView"
          p:prefix="/WEB-INF/jsp/"
          p:suffix=".jsp"/>

hello-context.xml用來配置URL處理器的映射規則,也可以配置如下:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-4.0.xsd">
    <context:component-scan base-package="com.test.controller"/>
</beans>

上述配置表示自動掃描com.test.controller包下,由stereotype類型註解標記的類,有四種:@Controller、@Component、@Repository、@Service。

然後我們就可以編寫jsp文件和Controller,啓動程序後就可以輸入URL看到結果。

在上述配置中,有兩個關鍵類:ContextLoaderListener和DispatcherServlet。

1.ContextLoaderListener

它自身的代碼很簡單,實現了來自ServletContextLoader接口的contextInitialized、contextDestroyed兩個方法,不過主要實現在父類ContextLoader中。

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
    ContextLoaderListener() {
	}

	public ContextLoaderListener(WebApplicationContext context) {
		super(context);
	}

	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}

	@Override
	public void contextDestroyed(ServletContextEvent event) {
		closeWebApplicationContext(event.getServletContext());
		ContextCleanupListener.cleanupAttributes(event.getServletContext());
	}
}

實際上,Spring正是依靠ServletContextListener,才能被Tomcat容器加載的:

public boolean listenerStart() {
    ...
    for (int i = 0; i < instances.length; i++) {
       if (!(instances[i] instanceof ServletContextListener))
           continue;
       ServletContextListener listener =
           (ServletContextListener) instances[i];
       try {
           fireContainerEvent("beforeContextInitialized", listener);
           if (noPluggabilityListeners.contains(listener)) {
               listener.contextInitialized(tldEvent);
           } else {
               listener.contextInitialized(event);
           }
           fireContainerEvent("afterContextInitialized", listener);
       } catch (Throwable t) {
           ...
       }
   }
   return ok;
}

可見Tomcat啓動Spring容器就是靠contextInitialized調用initWebApplicationContext方法來實現的,從名字不難看出,WebApplicationContext就是在ApplicationContext的基礎上增加了一些Web操作及屬性。下面來看看這個方法的源碼。

首先判斷了一次web.xml中是否重複定義了ContextLoader,從下面的代碼可以看出,每當創建WebApplicationContext實例時,就會記錄在ServletContext中以便全局調用,key就是ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,所以可以getAttribute來檢查是否已經創建過WebApplicationContext:

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!");
}

1.1 創建WebApplicationContext

接下來,假如當前ContextLoader還沒有管理任何WebApplicationContext實例,就創建一個,創建方法爲createWebApplicationContext。最後的instantiateClass在閱讀Spring源碼時已經見過很多次了,作用是將Class對象實例化,因此,該方法的核心是determineContextClass方法:

protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
	Class<?> contextClass = determineContextClass(sc);
	if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
		throw new ApplicationContextException("Custom context class [" + contextClass.getName() + "] is not of type [" + ConfigurableWebApplicationContext.class.getName() + "]");
	}
	return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
}

determineContextClass基本邏輯如下(去除了try-catch),ClassUtil.forName很顯然就是反射創建類

protected Class<?> determineContextClass(ServletContext servletContext) {
    String contextClassName = servletContext.getInitParameter("contextClass");
    if (contextClassName != null) {
        return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader());
    }
    else {
        contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName());
        return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader());
    }
}

如果是配合Tomcat使用,一般傳入的是ApplicationContext(這個ApplicationContext是ServletContext的子類,而不是Spring容器),它的getInitParameter實現如下:

public String getInitParameter(final String name) {
    if ("org.apache.jasper.XML_VALIDATE_TLD".equals(name) &&
        context.getTldValidation()) {
            return "true";
    }
    if ("org.apache.jasper.XML_BLOCK_EXTERNAL".equals(name)) {
        if (!context.getXmlBlockExternal()) {
            return "false";
        }
    }
    return parameters.get(name);
}

這裏將常量替換爲對應的字面值,可以看到,最終是從一個Map中獲取值。如果我們配置了自定義的WebApplicationContext實現,則加載自定義的,否則通過WebApplicationContext的全限定名查找需要加載的類名,並進行加載。在ContextLoader的靜態塊中,可以看到如下語句:

ClassPathResource resource = new ClassPathResource("ContextLoader.properties", ContextLoader.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);

也就是說,SpringMVC默認從classpath:org.springframework/web/context/ContextLoader.properties文件加載容器的類名,查詢一下,果然如此:

# Default WebApplicationContext implementation class for ContextLoader.
# Used as fallback when no explicit context implementation has been specified as context-param.
# Not meant to be customized by application developers.

org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext

可見Spring Web容器的實現類爲XmlWebApplicationContext。

1.2 設置和刷新WebApplicationContext

容器創建完畢後,根據經驗來看,還需要一些設置和刷新,源碼中通過configureAndRefreshWebApplicationContext方法實現。

該源碼可分爲設置和刷新兩部分。首先看設置,代碼檢查了是否配置了contextId和contextConfigLocation,是則賦給新創建的容器,並且通過setServletContext將Web容器和Servlet容器關聯起來。然後獲取Environment進行PropertySource的初始化,這一步中如果沒有設置環境,會創建一個StandardServletEnvironment實例,獲取servletContextInitParams和servletConfigInitParams,然後進行屬性替換。

if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
	String idParam = sc.getInitParameter("contextId");
	if (idParam != null) {
		wac.setId(idParam);
	}
	else {
		wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
				ObjectUtils.getDisplayString(sc.getContextPath()));
	}
}
wac.setServletContext(sc);
String configLocationParam = sc.getInitParameter("contextConfigLocation");
if (configLocationParam != null) {
	wac.setConfigLocation(configLocationParam);
}
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
	((ConfigurableWebEnvironment) env).initPropertySources(sc, null);
}

接下來調用customizeContext方法對Web容器進行初始化,它會尋找配置的contextInitializerClasses或globalInitializerClasses,使用它們對Web容器進行初始化。

protected void customizeContext(ServletContext sc, ConfigurableWebApplicationContext wac) {
    List<Class<ApplicationContextInitializer<ConfigurableApplicationContext>>> initializerClasses = determineContextInitializerClasses(sc);
	for (Class<ApplicationContextInitializer<ConfigurableApplicationContext>> initializerClass : initializerClasses) {
		Class<?> initializerContextClass =
			GenericTypeResolver.resolveTypeArgument(initializerClass, ApplicationContextInitializer.class);
		if (initializerContextClass != null && !initializerContextClass.isInstance(wac)) {
			throw new ApplicationContextException(String.format(
				"Could not apply context initializer [%s] since its generic parameter [%s] " + "is not assignable from the type of application context used by this " + "context loader: [%s]", initializerClass.getName(), initializerContextClass.getName(),wac.getClass().getName()));
		}
		this.contextInitializers.add(BeanUtils.instantiateClass(initializerClass));
	}
	AnnotationAwareOrderComparator.sort(this.contextInitializers);
	for (ApplicationContextInitializer<ConfigurableApplicationContext> initializer : this.contextInitializers) {
		initializer.initialize(wac);
	}
}

接着調用refresh方法對容器進行刷新。使用過Spring一定不會對它陌生,該方法位於AbstractApplicationContext,絕大部分基本邏輯和Spring是一致的,但是在XmlWebApplicationContext中,對loadBeanDefinitions和postProcessBeanFactory進行了實現,因此又有些區別,首先是loadBeanDefinitions:

protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws IOException {
    String[] configLocations = getConfigLocations();
    if (configLocations != null) {
        for (String configLocation : configLocations) {
            reader.loadBeanDefinitions(configLocation);
        }
    }
}

protected String[] getDefaultConfigLocations() {
   return this.getNamespace() != null ? new String[]{"/WEB-INF/" + this.getNamespace() + ".xml"} : new String[]{"/WEB-INF/applicationContext.xml"};
}

這裏讀取了WEB-INF下的配置文件,要麼由Namespace決定,要麼默認讀取applicationContext.xml。提到Namespace就不難想到Spring Schema,即通過META-INF下的spring.handlers文件配置命名空間解析器。

Spring MVC的默認命名空間解析器爲MvcNamespaceHandler,它註冊了一系列解析器,這些解析器方法又在parse方法中註冊了一系列組件,例如常用的<mvc:annotation-driven/>配置,就會註冊RequestMappingHandlerMapping、RequestMappingHandlerAdapter等:

context.registerComponent(new BeanComponentDefinition(handlerMappingDef, HANDLER_MAPPING_BEAN_NAME));
context.registerComponent(new BeanComponentDefinition(handlerAdapterDef, HANDLER_ADAPTER_BEAN_NAME));
context.registerComponent(new BeanComponentDefinition(uriContributorDef, uriContributorName));
context.registerComponent(new BeanComponentDefinition(mappedInterceptorDef, mappedInterceptorName));
context.registerComponent(new BeanComponentDefinition(methodExceptionResolver, methodExResolverName));
context.registerComponent(new BeanComponentDefinition(statusExceptionResolver, statusExResolverName));
context.registerComponent(new BeanComponentDefinition(defaultExceptionResolver, defaultExResolverName));

postProcessBeanFactory實際上是在父類AbstractRefreshableWebApplicationContext實現的,源碼如下:

protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    beanFactory.addBeanPostProcessor(new ServletContextAwareProcessor(this.servletContext, this.servletConfig));
    beanFactory.ignoreDependencyInterface(ServletContextAware.class);
    beanFactory.ignoreDependencyInterface(ServletConfigAware.class);
    WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, this.servletContext);
    WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, this.servletContext, this.servletConfig);
}

在ClassPathXmlApplicationContext的實現中,該方法爲空,所以這裏全都是新的邏輯。這裏註冊了ServletContextAwareProcessor,然後忽略了ServletContextAware和ServletConfigAware類型Bean的依賴,接下來,通過registerWebApplicationScopes擴展了scope屬性。在Spring中,有singleton和prototype兩種,現在額外增加了request、session、globalSession、application四種。registerEnvironmentBeans將servletContext、servletConfig以及contextParameters註冊到Web容器中。

initWebApplicationContext方法剩下的代碼就是將創建出的Web容器記錄在Servlet容器中。到此爲止,Web容器已經創建完畢,剩下的工作就是等待請求到達服務器。

2.DispatcherServlet

當請求到達服務器後,如果URL符合web.xml中url-pattern的配置,就會被DispatcherServlet攔截。它是Spring MVC的核心,其繼承關係如下:

在Tomcat解析完web.xml後,會將其中配置的Servlet對象封裝爲StandardWrapper,添加到Context中:

private void configureContext(WebXml webxml) {
    ...
    for (ServletDef servlet : webxml.getServlets().values()) {
        Wrapper wrapper = context.createWrapper();
        ...
        context.addChild(wrapper);
    }
    ...
}

當Bootstrap調用start方法後,就會按照Catalina、Server、Service、(Engine、Executor、Connector)、Host、Context、Wrapper的順序逐級啓動子元素(括號括起來的三個屬於同一級),上面說到,Wrapper其實就對應着一個Servlet,Context調用了Wrapper的load方法,實質上就是調用Servlet的init方法,於是就將Tomcat的啓動過程和DispatcherServlet的初始化過程串聯起來了。

2.1 init方法

DispatcherServlet直接繼承了HttpServletBean的init方法,這裏先將DispatcherServlet包裝成Bean,並賦予init-param配置的初始化參數,然後調用了FrameWorkServlet的initServletBean方法:

public final void init() throws ServletException {
    PropertyValues pvs = new HttpServletBean.ServletConfigPropertyValues(this.getServletConfig(), this.requiredProperties);
    if (!pvs.isEmpty()) {
        try {
            BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
            ResourceLoader resourceLoader = new ServletContextResourceLoader(this.getServletContext());
            bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, this.getEnvironment()));
            this.initBeanWrapper(bw);
            bw.setPropertyValues(pvs, true);
        } catch (BeansException var4) {
            throw var4;
        }
    }
    this.initServletBean();
}

ServletConfigPropertyValues除了封裝屬性,還進行了一些驗證,假如遍歷了所有init-param,仍有需要的屬性沒有設置,則拋出異常:

public ServletConfigPropertyValues(ServletConfig config, Set<String> requiredProperties) throws ServletException {
    Set<String> missingProps = !CollectionUtils.isEmpty(requiredProperties) ? new HashSet(requiredProperties) : null;
    Enumeration paramNames = config.getInitParameterNames();
    while(paramNames.hasMoreElements()) {
        String property = (String)paramNames.nextElement();
        Object value = config.getInitParameter(property);
        this.addPropertyValue(new PropertyValue(property, value));
        if (missingProps != null) {
            missingProps.remove(property);
        }
    }
    if (!CollectionUtils.isEmpty(missingProps)) {
        throw new ServletException("Initialization from ServletConfig for servlet '" + config.getServletName() + "' failed; the following required properties were missing: " + StringUtils.collectionToDelimitedString(missingProps, ", "));
    }
}

2.2 initServletBean方法

initServletBean邏輯也很清晰,分別調用了initWebApplicationContext和initFrameworkServlet兩個方法,其中initFrameworkServlet爲一個擴展點,沒有默認實現:

protected final void initServletBean() throws ServletException {
    this.getServletContext().log("Initializing Spring FrameworkServlet '" + this.getServletName() + "'");
    long startTime = System.currentTimeMillis();
    try {
        this.webApplicationContext = this.initWebApplicationContext();
        this.initFrameworkServlet();
    } catch (ServletException var5) {
        throw var5;
    } catch (RuntimeException var6) {
        throw var6;
    }
}

因此下面僅介紹initWebApplicationContext,該方法同樣位於FrameworkServlet。該方法實際就做了兩件事:獲取可用的Web容器、對Web容器進行刷新。

首先是獲取Web容器,如果Servlet中已經有一個Web容器,即DispatcherServlet已經作爲Bean初始化,且Web容器已經注入進來,則將其設爲根Web容器的子容器,並執行刷新。根容器在Servlet容器中存儲的key爲WebApplicationContext.class.getName() + ".ROOT":

WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
    wac = this.webApplicationContext;
    if (wac instanceof ConfigurableWebApplicationContext) {
        ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
        if (!cwac.isActive()) {
            if (cwac.getParent() == null) {
                 cwac.setParent(rootContext);
            }
            configureAndRefreshWebApplicationContext(cwac);
        }
    }
}
if (wac == null) {
    wac = findWebApplicationContext();
}
if (wac == null) {
    wac = createWebApplicationContext(rootContext);
}

假如Servlet中目前還沒有Web容器實例,則通過findWebApplicationContext尋找一個:

protected WebApplicationContext findWebApplicationContext() {
    String attrName = getContextAttribute();
    if (attrName == null) {
        return null;
    }
    WebApplicationContext wac = WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
    if (wac == null) {
        throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
    }
    return wac;
}

實際上還是調用了getWebApplicationContext,只不過這次通過web.xml中配置的servlet參數contextAttribute來查找。

假如通過參數查找也無效,那就只能重新創建一個Web容器實例。此處的createWebApplicationContext方法和ContextLoader的不太一樣:

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

首先獲取了contextClass,它可以作爲init-param配置在web.xml中,允許我們修改容器類型,下面的contextConfigLocation屬性也是如此。

然後判斷了解析出來的contextClass類型是否爲ConfigurableWebApplicationContext類型,XmlWebApplicationContext就實現了這個接口。

然後就是組裝一個Web容器,並執行刷新。FrameworkServlet的configureAndRefreshWebApplicationContext和ContextLoader的同名方法不同之處在於,用以下語句代替了customizeContext:

this.postProcessWebApplicationContext(wac);
this.applyInitializers(wac);

前者又是一個擴展點,略過。applyInitializer和customizeContext邏輯基本一致,不過僅僅解析了globalInitializerClasses的值。

2.3 OnRefresh方法

接下來就可以通過OnRefresh方法對Web容器進行刷新了。OnRefresh方法位於DispatcherServlet中,實際調用了initStrategies方法,在這裏,我們可以看到Spring MVC的九大組件:

protected void initStrategies(ApplicationContext context) {
    this.initMultipartResolver(context); //文件上傳解析器
    this.initLocaleResolver(context); //本地化解析器
    this.initThemeResolver(context); //主題解析器
    this.initHandlerMappings(context); //處理器映射器
    this.initHandlerAdapters(context); //處理器適配器
    this.initHandlerExceptionResolvers(context); //處理器異常解析器
    this.initRequestToViewNameTranslator(context); //視圖名翻譯器
    this.initViewResolvers(context); //視圖解析器
    this.initFlashMapManager(context); //FlashMap管理器
}

接下來就介紹這九大組件的註冊過程。

3.九大組件的註冊

3.1 文件上傳解析器MultipartResolver

MultipartResolver用於處理文件上傳,當開發者配置了該組件,Spring就可以處理請求中包含的multipart。

private void initMultipartResolver(ApplicationContext context) {
    try {
        this.multipartResolver = (MultipartResolver)context.getBean("multipartResolver", MultipartResolver.class);
    } catch (NoSuchBeanDefinitionException var3) {
        this.multipartResolver = null;
    }
}

可以看到,自定義的文件上傳解析器Bean的id屬性必須是"multipartResolver",否則將不會提供默認實現,即Spring默認不支持文件上傳。

3.2 本地化解析器LocaleResolver

它的初始化方法和文件上傳解析器很相似,不同的是,這裏Spring提供了一個默認實現:

private void initLocaleResolver(ApplicationContext context) {
    try {
        this.localeResolver = (LocaleResolver)context.getBean("localeResolver", LocaleResolver.class);
    } catch (NoSuchBeanDefinitionException var3) {
        this.localeResolver = (LocaleResolver)this.getDefaultStrategy(context, LocaleResolver.class);
    }
}

看到getDefaultStrategy就不難猜到,這裏和ContextLoader採用了相同的方式:在META-INF下配置一個同名properties文件,在裏面進行配置,查看一下,果不其然:

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.method.annotation.RequestMappingHandlerMapping

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

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,\
	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

可以發現,除了文件上傳解析器,其它八大組件都提供了默認實現,Handler相關的三個組件甚至提供了多種選擇。本地化解析器的實現是AccpetHeaderLocaleResolver,它的功能就是根據請求頭中的accept-language屬性來本地化。

除此之外,還有兩種本地化配置方式,它們對應着不同的本地化解析器:

  • 基於Session的配置:SessionLocaleResolver會讀取Session中 SessionLocaleResolver.class.getName() + ".LOCALE" 對應的值。
  • 基於Cookie的配置:CookieLocaleResolver會解析Cookie中Locale的配置。

3.3 主題解析器ThemeResolver

主題指的是網頁風格,即CSS、圖片等靜態資源。initThemeResolver代碼和本地化解析器基本一致,允許開發者定義名爲“themeResolver”的Bean,或者使用默認實現。

從上面的配置中,可以看到,主題解析器的默認實現是FixedThemeResolver,其源碼如下:

public class FixedThemeResolver extends AbstractThemeResolver {
    @Override
    public String resolveThemeName(HttpServletRequest request) {
        return getDefaultThemeName();
    }

    @Override
    public void setThemeName(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable String themeName) {
        throw new UnsupportedOperationException("Cannot change theme - use a different theme resolution strategy");
    }
}

它可以通過defaultThemeName屬性配置主題,但是無法動態配置。

除此之外,Spring還提供了CookieThemeResolver和SessionThemeResolver,不難理解它們分別可以解析存放在Cookie和Session中的主題信息,實現動態配置的。

如果要實現自定義的ThemeResolver,可以繼承AbstractThemeResolver。

實際上,Spring通過攔截器機制,還提供了一種根據URL動態配置主題的方法:可以創建一個ThemeChangeInteceptor類型的Bean,並註冊到HandlerMapping的interceptors屬性中,這樣就可以通過"?theme="來指定主題。

3.4 處理器映射器HandlerMapping

當服務器接收到來自客戶端的Request後,Dispatcher就會將請求提交給RequestMapping,然後根據Web容器的配置,將請求分派到對應的Controller處進行處理。從源碼中可以看到,與上面介紹的三個組件不同,HandlerMapping可以配置多個,還可以通過實現Ordered接口設置優先級,它們按照優先級形成了一個鏈條,前一個HandlerMapping如果無法處理,將由後一個繼續。當然也可以僅配置一個或者不配置。

private void initHandlerMappings(ApplicationContext context) {
    this.handlerMappings = null;
    if (this.detectAllHandlerMappings) {
        Map<String, HandlerMapping> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
        if (!matchingBeans.isEmpty()) {
            this.handlerMappings = new ArrayList(matchingBeans.values());
            AnnotationAwareOrderComparator.sort(this.handlerMappings);
        }
    } else {
        try {
            HandlerMapping hm = (HandlerMapping)context.getBean("handlerMapping", HandlerMapping.class);
            this.handlerMappings = Collections.singletonList(hm);
        } catch (NoSuchBeanDefinitionException var3) {
        }
    }
    if (this.handlerMappings == null) {
        this.handlerMappings = this.getDefaultStrategies(context, HandlerMapping.class);
    }
}

detectAllHandlerMappings通過web.xml的init-param配置,默認爲true。如果沒有配置HandlerMapping,則會加載以下兩個默認實現:

  • BeanNameUrlHandlerMapping:從名字就能看出,它是通過BeanName來匹配Url和Handler的,支持完全匹配和“*”匹配
  • RequestMappingHandlerMapping:RequestMapping註解對於Spring使用者來說一定不陌生,該註解可以用於Controller及成員方法上,用來標記它們可以處理的URL

Spring源碼學習(五):Bean的創建和獲取中,我們曾經看到,每個InitializingBean實現類的afterPropertiesSet方法都會被調用,而在瀏覽RequestMappingHandlerMapping源碼的過程中,我們也發現了這個方法的身影,查看繼承關係,果然不出所料:

因此,在DispatcherServlet嘗試獲取RequestMappingHandlerMapping實例時,會觸發afterPropertiesSet,由此進行它的初始化:

public void afterPropertiesSet() {
    this.config = new BuilderConfiguration();
    this.config.setUrlPathHelper(this.getUrlPathHelper());
    this.config.setPathMatcher(this.getPathMatcher());
    this.config.setSuffixPatternMatch(this.useSuffixPatternMatch);
    this.config.setTrailingSlashMatch(this.useTrailingSlashMatch);
    this.config.setRegisteredSuffixPatternMatch(this.useRegisteredSuffixPatternMatch);
    this.config.setContentNegotiationManager(this.getContentNegotiationManager());
    super.afterPropertiesSet();
}

其核心是通過父類同名方法調用的initHandlerMethod方法:

protected void initHandlerMethods() {
    for (String beanName : getCandidateBeanNames()) {
        if (!beanName.startsWith("scopedTarget.")) {
            processCandidateBean(beanName);
        }
    }
    handlerMethodsInitialized(getHandlerMethods());
}

getCadidateBeanNames方法獲取了所有Bean的BeanName,對於名稱不以"scopedTarget."開頭的Bean進行處理,對於其中Handler類型(由@Controller、@RequestMapping註解)的Bean,探測並註冊其包含的處理器方法:

protected void processCandidateBean(String beanName) {
    Class<?> beanType = null;
    try {
        beanType = obtainApplicationContext().getType(beanName);
    }
    catch (Throwable ex) {
    }
    if (beanType != null && isHandler(beanType)) {
        detectHandlerMethods(beanName);
    }
}

detectHandlerMethods反射遍歷類中包含的公共方法,通過getMappingForMethod將探測到的處理器方法包裝爲RequestMappingInfo,並將方法註解的路徑和類註解的基礎路徑合併,作爲方法對應的完整URI:

protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
    RequestMappingInfo info = createRequestMappingInfo(method);
    if (info != null) {
        RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
        if (typeInfo != null) {
            info = typeInfo.combine(info);
        }
        String prefix = getPathPrefix(handlerType);
        if (prefix != null) {
            info = RequestMappingInfo.paths(prefix).build().combine(info);
        }
    }
    return info;
}

 createRequestMappingInfo基於傳入的被註解對象,創建了RequestMapping和RequestCondition對象,把這兩個新對象作爲參數傳入重載方法中,讀取ReqeustMapping中各屬性的值,構造一個生成器,然後生成RequestMappingInfo實例:

protected RequestMappingInfo createRequestMappingInfo(RequestMapping requestMapping, @Nullable RequestCondition<?> customCondition) {
    RequestMappingInfo.Builder builder = RequestMappingInfo
		.paths(resolveEmbeddedValuesInPatterns(requestMapping.path()))
		.methods(requestMapping.method())
		.params(requestMapping.params())
		.headers(requestMapping.headers())
		.consumes(requestMapping.consumes())
		.produces(requestMapping.produces())
		.mappingName(requestMapping.name());
	if (customCondition != null) {
		builder.customCondition(customCondition);
	}
	return builder.options(this.config).build();
}

RequestMappingInfo生成後,會註冊到MappingRegistry中,其register方法會調用createHandlerMethod方法產生HandlerMethod對象,然後存入相關Map中。createHandlerMethod調用HandlerMethod的構造方法來創建實例。在構造方法中完成了對方法參數和@ResponseStatus註解的解析:

public void register(T mapping, Object handler, Method method) {
	this.readWriteLock.writeLock().lock();
	try {
		HandlerMethod handlerMethod = createHandlerMethod(handler, method);
		assertUniqueMethodMapping(handlerMethod, mapping);
		this.mappingLookup.put(mapping, handlerMethod);
		List<String> directUrls = getDirectUrls(mapping);
		for (String url : directUrls) {
			this.urlLookup.add(url, mapping);
		}
		String name = null;
		if (getNamingStrategy() != null) {
			name = getNamingStrategy().getName(handlerMethod, mapping);
			addMappingName(name, handlerMethod);
		}
		CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
		if (corsConfig != null) {
			this.corsLookup.put(handlerMethod, corsConfig);
		}
    	this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
	    }
		finally {
			this.readWriteLock.writeLock().unlock();
		}
	}

getNamingStrategy方法得到了一個HandlerMethodMappingNamingStrategy接口的實例,可以根據HandlerMethod得到一個處理器名稱。默認實現爲RequestMappingInfoHandlerMethodMappingNamingStrategy,getName源碼如下:

public String getName(HandlerMethod handlerMethod, RequestMappingInfo mapping) {
    if (mapping.getName() != null) {
		return mapping.getName();
	}
	StringBuilder sb = new StringBuilder();
	String simpleTypeName = handlerMethod.getBeanType().getSimpleName();
	for (int i = 0; i < simpleTypeName.length(); i++) {
		if (Character.isUpperCase(simpleTypeName.charAt(i))) {
			sb.append(simpleTypeName.charAt(i));
		}
	}
	sb.append("#").append(handlerMethod.getMethod().getName());
	return sb.toString();
}

舉一個例子,假設處理器方法registerUser位於UserController類中,經過處理生成的處理器名就是“UC#registerUser”。

3.5 處理器適配器HandlerAdapter

當DispatcherServlet通過HandlerMapping,解析到目標Handler後,還不能直接調用,因爲請求處理器有很多種類型,還需要靠HandlerAdapter進行適配。HandlerAdapter的初始化方法和HandlerMapping完全一致,它的三個默認實現爲:

  • HttpRequestHandlerAdapter:僅支持適配HttpRequestHandler,其handle方法只是做了request和response參數的轉發,並且直接返回null,也就是不需要返回值。
  • SimpleControllerHandlerAdapter:支持適配Controller,其handle方法只是做了request和response參數的轉發,並返回轉發得到的返回值。
  • RequestMappingHandlerAdapter:其handle方法間接調用了invokeHandlerMethod,在該方法中解析了被註解方法的參數,然後通過反射調用,返回ModelAndView對象。

與RequestMappingHandlerMapping一樣,RequestMappingHandlerAdapter也實現了afterProperties方法,在這裏完成了被@ControllerAdvice註解的類的解析,以及參數解析器、@InitBinder和返回值解析器的創建:

public void afterPropertiesSet() {
	initControllerAdviceCache();
	if (this.argumentResolvers == null) {
	    List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
		this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
	}
	if (this.initBinderArgumentResolvers == null) {
		List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
		this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
	}
	if (this.returnValueHandlers == null) {
		List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
		this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
	}
}

3.5.1 @ControllerAdvice 與 initControllerAdviceCache

@ControllerAdvice是Spring 3.2加入到註解,從名字上就能看出來,它用來對Controller進行增強。它的源碼如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
	@AliasFor("basePackages")
	String[] value() default {};

	@AliasFor("value")
	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<?>[] assignableTypes() default {};

	Class<? extends Annotation>[] annotations() default {};
}

它被@Component註解了,所以可以被<context:component-scan>自動掃描到。它的作用是把@ControllerAdvice註解類內部使用@ExceptionHandler(用於異常處理)、@InitBinder(用於數據綁定)、@ModelAttribute(用於自動向ModelMap中添加屬性)註解的成員方法,應用到被@RequestMapping註解的處理器上。在不提供參數的情況下,默認是應用到所有處理器,也可以限定範圍:

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}

initControllerAdviceCache方法就很簡單了,就是解析被@ControllerAdvice註解的Bean,然後添加到緩存(一些List)中。

3.5.2 參數解析器 HandlerMethodArgumentResolver

這個很容易理解,它就是用來從request中解析出處理器方法需要的參數,getDefaultArgumentResolvers方法默認註冊了一大堆,也支持自定義實現:

// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());

// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

// Custom arguments
if (getCustomArgumentResolvers() != null) {
	resolvers.addAll(getCustomArgumentResolvers());
}

// Catch-all
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));

這裏以PathVariableMethodArgumentResolver爲例。HandlerMethodArgumentResolver的核心方法是resolveArgument,在AbstractNamedValueMethodArgumentResolver中提供了實現,一共接受四個參數:MethodParameter方法參數對象、ModelAndViewContainer邏輯視圖容器對象、NativeWebRequest請求對象和WebDataBinderFactory數據綁定工廠對象。

首先,將傳入的參數轉換成NamedValueInfo,轉換方法很簡單,找到被@PathVariable註解的參數,封裝成NamedValueInfo即可:

NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);

private NamedValueInfo getNamedValueInfo(MethodParameter parameter) {
    NamedValueInfo namedValueInfo = this.namedValueInfoCache.get(parameter);
	if (namedValueInfo == null) {
		namedValueInfo = createNamedValueInfo(parameter);
		namedValueInfo = updateNamedValueInfo(parameter, namedValueInfo);
		this.namedValueInfoCache.put(parameter, namedValueInfo);
	}
	return namedValueInfo;
}

protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
	PathVariable ann = parameter.getParameterAnnotation(PathVariable.class);
	Assert.state(ann != null, "No PathVariable annotation");
	return new PathVariableNamedValueInfo(ann);
}

 然後對@PathVariable的value屬性進行解析,主要是處理佔位符、通配符等。

Object resolvedName = resolveStringValue(namedValueInfo.name);

 然後從request中按照解析出的名稱獲取屬性:

protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {
	Map<String, String> uriTemplateVars = (Map<String, String>) request.getAttribute(HandlerMapping.class.getName() + ".uriTemplateVariables", 0);
	return (uriTemplateVars != null ? uriTemplateVars.get(name) : null);
}

 假如沒能解析到值,或者解析到空值,則進行處理:

if (arg == null) {
	if (namedValueInfo.defaultValue != null) {
		arg = resolveStringValue(namedValueInfo.defaultValue);
	}
	else if (namedValueInfo.required && !nestedParameter.isOptional()) {
		handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
	}
	arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
	arg = resolveStringValue(namedValueInfo.defaultValue);
}

假如傳入了不爲空的數據綁定工廠對象,則會嘗試進行數據轉換:

if (binderFactory != null) {
	WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
	try {
		arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
	}
	catch (ConversionNotSupportedException ex) {
		throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
				namedValueInfo.name, parameter, ex.getCause());
	}
	catch (TypeMismatchException ex) {
		throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
				namedValueInfo.name, parameter, ex.getCause());
	}
}

最後,把解析到的參數寫回request:

protected void handleResolvedValue(@Nullable Object arg, String name, MethodParameter parameter,@Nullable ModelAndViewContainer mavContainer, NativeWebRequest request) {
	String key = View.class.getName() + ".pathVariables";
	int scope = 0;
	Map<String, Object> pathVars = (Map<String, Object>) request.getAttribute(key, scope);
	if (pathVars == null) {
		pathVars = new HashMap<>();
		request.setAttribute(key, pathVars, scope);
	}
	pathVars.put(name, arg);
}

3.5.3 @InitBinder的初始化

首先來看一下@InitBinder的作用,它可以修改WebDataBinder對象,綁定請求參數到模型對象,進行參數值轉換或格式化。

官方提供的一個例子如下:

@Controller
public class FormController {

    @InitBinder 
    public void initBinder(WebDataBinder binder) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
    }

    // ...
}

從而對請求傳入的日期類值進行自動格式轉換。不過這個例子只能對當前Controller生效,如果要對所有Controller生效,可以藉助上面的@ControllerAdvice。

getDefaultInitBinderArgumentResolvers方法和上一個一樣,也是註冊了一堆ArgumentResolver。在上面介紹PathVariableMethodArgumentResolver時,最後提到,如果有WebDataBinderFactory,則會嘗試產生實例WebDataBinder並進行值轉換。

WebDataBinderFactory的一個實現類就是InitBinderDataBinderFactory,它的createBinder方法會構造一個WebRequestDataBinder對象,並調用非空初始化器WebBindingInitializer和自身的initBinder方法對它進行初始化。

WebBindingInitializer的唯一實現類是ConfigurableWebBindingInitializer,它的initBinder方法如下:

public void initBinder(WebDataBinder binder) {
	binder.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
	if (this.directFieldAccess) {
		binder.initDirectFieldAccess();
	}
	if (this.messageCodesResolver != null) {
		binder.setMessageCodesResolver(this.messageCodesResolver);
	}
	if (this.bindingErrorProcessor != null) {
		binder.setBindingErrorProcessor(this.bindingErrorProcessor);
	}
	if (this.validator != null && binder.getTarget() != null &&
			this.validator.supports(binder.getTarget().getClass())) {
		binder.setValidator(this.validator);
	}
	if (this.conversionService != null) {
		binder.setConversionService(this.conversionService);
	}
	if (this.propertyEditorRegistrars != null) {
		for (PropertyEditorRegistrar propertyEditorRegistrar : this.propertyEditorRegistrars) {
			propertyEditorRegistrar.registerCustomEditors(binder);
		}
	}
}

實際就是向WebDataBinder註冊一些解析器、驗證器、轉換器,然後把整個WebDataBinder作爲一個大號的PropertyEditor註冊到容器中。

initBinder方法遍歷了所有可調用的處理器方法,反射調用其中被@InitBinder註解的方法:

public void initBinder(WebDataBinder dataBinder, NativeWebRequest request) throws Exception {
	for (InvocableHandlerMethod binderMethod : this.binderMethods) {
		if (isBinderMethodApplicable(binderMethod, dataBinder)) {
			Object returnValue = binderMethod.invokeForRequest(request, null, dataBinder);
			if (returnValue != null) {
				throw new IllegalStateException("@InitBinder methods must not return a value (should be void): " + binderMethod);
			}
		}
	}
}

3.5.4 返回值解析器 HandlerMethodReturnValueHandler

HandlerMethodReturnValueHandler的主要作用就是對返回值進行一些後處理,它的核心方法是handleReturnValue。

getDefaultReturnValueHandlers形式上跟上面兩個方法差不多。這裏以ModelAndViewMethodReturnValueResolver來看一下其作用:

public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
	if (returnValue == null) {
		mavContainer.setRequestHandled(true);
		return;
	}
	ModelAndView mav = (ModelAndView) returnValue;
	if (mav.isReference()) {
		String viewName = mav.getViewName();
		mavContainer.setViewName(viewName);
		if (viewName != null && isRedirectViewName(viewName)) {
			mavContainer.setRedirectModelScenario(true);
		}
	}
	else {
		View view = mav.getView();
		mavContainer.setView(view);
		if (view instanceof SmartView && ((SmartView) view).isRedirectView()) {
			mavContainer.setRedirectModelScenario(true);
		}
	}
	mavContainer.setStatus(mav.getStatus());
	mavContainer.addAllAttributes(mav.getModel());
}

如果返回值爲空,則不必處理,直接返回。否則判斷一下返回的ModelAndView是否使用了視圖引用,所謂視圖引用,即其內部的View對象是經過翻譯的視圖名,而非真實視圖對象。然後獲取視圖名或者視圖對象,以及Http狀態和ModelMap,一併設置到ModelAndView容器中。

3.6 處理器異常解析器HandlerExceptionResolver

Spring 異常處理的幾種方式介紹過四種ExceptionResolver,該接口的核心方法爲resolveException,返回一個ModelAndView,當Handler出現異常時,由ExceptionResolver進行捕獲並處理,如果某ExceptionResolver能夠處理髮生的異常,就可以返回ModelAndView對象,否則返回null,由下一個ExceptionResolver繼續處理。

HandlerExceptionResolver在Spring MVC中的默認實現有三:

  • ExceptionHandlerExceptionResolver
  • ResponseStatusExceptionResolver
  • DefaultHandlerExceptionResolver

3.7 視圖名翻譯器RequestToViewNameTranslator

Handler方法有可能沒有返回View對象或者邏輯視圖名稱,也沒有修改response,此時就需要Spring按照約定生成一個邏輯視圖名稱。生成邏輯視圖名稱需要藉助RequestToViewNameTranslator的getViewName方法。

Spring提供的默認實現也很簡單粗暴,名字就叫DefaultRequestToViewNameTranslator。它的getViewName方法源碼如下:

public String getViewName(HttpServletRequest request) {
    String lookupPath = this.urlPathHelper.getLookupPathForRequest(request);
    return (this.prefix + transformPath(lookupPath) + this.suffix);
}

protected String transformPath(String lookupPath) {
    String path = lookupPath;
    if (this.stripLeadingSlash && path.startsWith(SLASH)) {
        path = path.substring(1);
    }
    if (this.stripTrailingSlash && path.endsWith(SLASH)) {
        path = path.substring(0, path.length() - 1);
    }
    if (this.stripExtension) {
        path = StringUtils.stripFilenameExtension(path);
    }
    if (!SLASH.equals(this.separator)) {
        path = StringUtils.replace(path, SLASH, this.separator);
    }
    return path;
}

prefix和suffix默認都是空字符串,SLASH代表"/",seperator默認就是SLASH

stripLeadingSlash、stripTrailingSlash、stripExtension默認都是true,代表是否去除位於首字符/尾字符位置的SLASH,以及是否去除擴展名。假如現在傳入的URI是 /hello/index.html,經過上述代碼的處理,就會變成:hello/index。

3.8 視圖解析器ViewResolver

Handler處理完請求後,會把結果存入ModelAndView,它雖然叫做View,但並不是真正的頁面,只是個邏輯上的視圖,還需要ViewResolver將它渲染成真實頁面,渲染的方法爲resolveViewName。Spring提供的默認視圖解析器爲InternalResourceViewResolver。在本文開頭提供的示例中,我們配置的也是這個類:

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"
          p:viewClass="org.springframework.web.servlet.view.JstlView"
          p:prefix="/WEB-INF/jsp/"
          p:suffix=".jsp"/>

還配置了viewClass、prefix、suffix三個屬性。

resolveViewName(位於AbstractCachingViewResolver)分爲兩種情形:假如沒有啓用緩存機制,或者緩存裏沒有要渲染的視圖,則通過createView(位於UrlBasedViewResolver)創建一個;否則直接從緩存中取出。

createView首先判斷了能否處理,不能則直接返回null,否則根據邏輯視圖類型進行渲染:

protected View createView(String viewName, Locale locale) throws Exception {
    if (!this.canHandle(viewName, locale)) {
        return null;
    } else {
        String forwardUrl;
        if (viewName.startsWith("redirect:")) {
            forwardUrl = viewName.substring("redirect:".length());
            RedirectView view = new RedirectView(forwardUrl, this.isRedirectContextRelative(), this.isRedirectHttp10Compatible());
            view.setHosts(this.getRedirectHosts());
            return this.applyLifecycleMethods(viewName, view);
        } else if (viewName.startsWith("forward:")) {
            forwardUrl = viewName.substring("forward:".length());
            return new InternalResourceView(forwardUrl);
        } else {
            return super.createView(viewName, locale);
        }
    }
}

對於重定向視圖,則創建一個RedirectView;對於轉發視圖,創建一個InternalResourceViewResolver實例,默認渲染爲JSP;對於其它類型(包括一般的視圖),則調用父類的createView方法(實際調用了loadView方法,loadView方法內部又調用了buildView方法)。

buildView(位於InternalResourceViewResolver)源碼如下:

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
    InternalResourceView view = (InternalResourceView)super.buildView(viewName);
    if (this.alwaysInclude != null) {
        view.setAlwaysInclude(this.alwaysInclude);
    }
    view.setPreventDispatchLoop(true);
    return view;
}

父類的buildView用到了上面配置的三個屬性,創建了一個視圖對象,併爲其拼接產生了URL,然後就是些屬性設置:

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
    AbstractUrlBasedView view = (AbstractUrlBasedView)BeanUtils.instantiateClass(this.getViewClass());
    view.setUrl(this.getPrefix() + viewName + this.getSuffix());
    String contentType = this.getContentType();
    if (contentType != null) {
        view.setContentType(contentType);
    }
    view.setRequestContextAttribute(this.getRequestContextAttribute());
    view.setAttributesMap(this.getAttributesMap());
    Boolean exposePathVariables = this.getExposePathVariables();
    if (exposePathVariables != null) {
        view.setExposePathVariables(exposePathVariables);
    }
    Boolean exposeContextBeansAsAttributes = this.getExposeContextBeansAsAttributes();
    if (exposeContextBeansAsAttributes != null) {
        view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
    }
    String[] exposedContextBeanNames = this.getExposedContextBeanNames();
    if (exposedContextBeanNames != null) {
        view.setExposedContextBeanNames(exposedContextBeanNames);
    }
    return view;
}

3.9 FlashMap管理器 FlashMapManager

FlashMap的作用就是在進行重定向時,暫存數據,以便在重定向之後還能繼續使用,FlashMapManager就是用來存儲、傳遞、管理FlashMap的,它們可以通過RequestContextUtils在任意位置訪問。Spring提供的默認實現爲SessionFlashMapManager。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章