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。

 

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