目录
1.2 设置和刷新WebApplicationContext
3.5.1 @ControllerAdvice 与 initControllerAdviceCache
3.5.2 参数解析器 HandlerMethodArgumentResolver
3.5.4 返回值解析器 HandlerMethodReturnValueHandler
3.6 处理器异常解析器HandlerExceptionResolver
3.7 视图名翻译器RequestToViewNameTranslator
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。