本文我们尝试总结分析SpringMVC体系中的视图解析器-ViewResolver。其根据name解析视图View,通常鼓励实现类考虑国际化策略实现。

前置流程

DispatcherServlet#doDispatch--
DispatcherServlet#processDispatchResult--
DispatcherServlet#render--
DispatcherServlet#resolveViewName--
--view.render(mv.getModelInternal(), request, response);

DispatcherServlet中根据视图名称获取view代码如下所示,遍历循环viewResolvers尝试获取一个不为null的view。

@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {

if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}

ViewResolver 获取具体的View,SpringMVC调用View的Render方法来渲染视图-将model数据与视图结合在一起写入到response中。

ViewResolver 接口只有一个抽象方法如下所示:

@Nullable
View resolveViewName(String viewName, Locale locale) throws Exception;

locale表示鼓励你支持国际化策略。如果当前实现类不能解析视图,那么请返回null以使得视图解析器链可以流转下去。

【1】ViewResolver

首先我们看下其家族树图示:
SpringMVC常见组件之ViewResolver分析_spring mvc
ViewResolver实现主要分为四大类:

  • ① ContentNegotiatingViewResolver
  • ② AbstractCachingViewResolver
  • ③ ViewResolverComposite
  • ④ BeanNameViewResolver

子类视图解析器如下

  • ViewResolverComposite
  • AbstractCachingViewResolver
  • ResourceBundleViewResolver
  • XmlViewResolver
  • UrlBasedViewResolver
  • TilesViewResolver
  • ScriptTemplateViewResolver
  • InternalResourceViewResolver
  • XsltViewResolver
  • AbstractTemplateViewResolver
  • GroovyMarkupViewResolver
  • FreeMarkerViewResolver

  • ThymeleafViewResolver
  • AjaxThymeleafViewResolver

  • ContentNegotiatingViewResolver
  • BeanNameViewResolver

下面我们按照类别来分析其子类。

【1】ViewResolverComposite

为什么先说这个呢?ViewResolverComposite本身不是一个视图解析器,其使用了组合模式内部维护了一个​​private final List<ViewResolver> viewResolvers = new ArrayList<>();​​​,将具体resolveViewName动作委派了具体的ViewResolver实例(委派模式)。是不是和​​HandlerMethodArgumentResolverComposite​​​、​​HandlerMethodReturnValueHandlerComposite​​类型?

如下图所示,其实现了​​ViewResolver​​​, ​​Ordered​​​, ​​InitializingBean​​​,​​ApplicationContextAware​​​, ​​ServletContextAware​​接口。这意味着其在实例化过程中会进行诸多操作:

  • Ordered,表示其可以排序;
  • InitializingBean,实例化时调用其afterPropertiesSet方法;
  • ApplicationContextAware,实例化时会调用setApplicationContext方法;
  • ServletContextAware,实例化时会调用setServletContext方法。

SpringMVC常见组件之ViewResolver分析_ide_02
我们从其源码也可以知晓,​​​ViewResolverComposite​​​在实例化过程中对内部维护的ViewResolver 做了​​增强工作​​。

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
for (ViewResolver viewResolver : this.viewResolvers) {
if (viewResolver instanceof ApplicationContextAware) {
((ApplicationContextAware)viewResolver).setApplicationContext(applicationContext);
}
}
}

@Override
public void setServletContext(ServletContext servletContext) {
for (ViewResolver viewResolver : this.viewResolvers) {
if (viewResolver instanceof ServletContextAware) {
((ServletContextAware)viewResolver).setServletContext(servletContext);
}
}
}

@Override
public void afterPropertiesSet() throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
if (viewResolver instanceof InitializingBean) {
((InitializingBean) viewResolver).afterPropertiesSet();
}
}
}

如下代码所示,其解析视图的方法很简单:遍历viewResolvers,然后使用具体的viewResolver 去解析,返回view或者null。

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}

【2】BeanNameViewResolver

​BeanNameViewResolver将视图名字解析为bean name​​,找到上下文中该bean实例返回。bean需要实现View接口。

① 解析视图

其解析视图的方法如下所示,尝试根据viewName从ApplicationContext 找到对应的bean。如果ApplicationContext 中没有该bean直接返回null;如果该bean不是View类型也返回null;最后返回bean的实例。

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = obtainApplicationContext();
if (!context.containsBean(viewName)) {
// Allow for ViewResolver chaining...
return null;
}
if (!context.isTypeMatch(viewName, View.class)) {
if (logger.isDebugEnabled()) {
logger.debug("Found bean named '" + viewName + "' but it does not implement View");
}
// Since we're looking into the general ApplicationContext here,
// let's accept this as a non-match and allow for chaining as well...
return null;
}
return context.getBean(viewName, View.class);
}

② 实践

① 配置解析器

如下所示,配置两个视图解析器,SpringMVC.xml配置:

<!-- 配置视图解析器: 如何把 handler 方法返回值解析为实际的物理视图 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

<!-- ****** 配置视图 BeanNameViewResolver 解析器: 使用视图的名字来解析视图 ************ -->
<!-- 通过 order 属性来定义视图解析器的优先级, order 值越小优先级越高 -->
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver">
<property name="order" value="10"></property>
</bean>

InternalResourceViewResolver的order属性默认为Integer的最大值。

② 自定义View实现类

实例代码如下:

@Component
public class HelloView implements View{
/*自定义视图,需配置BeanNameViewResolver*/
public String getContentType() {
return "text/html";
}
//视图渲染,转发前不可避免的重要一步!
public void render(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {

/*打印响应信息到浏览器页面*/
response.getWriter().print("hello view, time: " + new Date());
}
}

测试方法:

@RequestMapping("/testView")
public String testView(){
System.out.println("testView");
return "helloView";
//返回bean名字
}

result as follows :

SpringMVC常见组件之ViewResolver分析_ide_03

同理,可以通过继承AbstractExcelView 并实现其buildExcelDocument方法,来渲染Excel。如下所示:

public class ExcelView extends AbstractExcelView {
@Override
protected void buildExcelDocument(Map<String, Object> model,
HSSFWorkbook workbook, HttpServletRequest request,
HttpServletResponse response) throws Exception {
// TODO Auto-generated method stub
}
}

【3】ContentNegotiatingViewResolver

内容协商视图解析器。简单来说就是根据viewName和Media Type(依据请求的文件名称或者Accept头)找到候选的View再根据MediaType和View的ContentType确定最优View。

如下所示,​​ContentNegotiatingViewResolver​​维护了集合defaultViews和viewResolvers表示其并不会进行“解析View”的具体动作。解析任务将会被委派给具体的ViewResolver执行。defaultViews集合提供了入口,用户可以通过该属性注入自定义视图解析器。

@Nullable
private List<View> defaultViews;

@Nullable
private List<ViewResolver> viewResolvers;

① 继承类分析

如下图所示,​​ContentNegotiatingViewResolver​​​继承了​​WebApplicationObjectSupport​​​并实现了​​ViewResolver, Ordered, InitializingBean​​接口。那么这里我们要有个清晰认知,这些类/接口意味着什么。

SpringMVC常见组件之ViewResolver分析_ide_04
实现了Order接口意味着其拥有order属性,内部viewResolver可以按照顺序执行。

实现了InitializingBean接口
如下图所示,如果bean实现了​​​InitializingBean​​​接口,那么在初始化过程中一定会调用其​​afterPropertiesSet​​方法。

SpringMVC常见组件之ViewResolver分析_spring_05
我们看下其afterPropertiesSet方法。如下所示根据cnmFactoryBean的build方法获取一个​​​contentNegotiationManager​​ 实例。

@Override
public void afterPropertiesSet() {
if (this.contentNegotiationManager == null) {
this.contentNegotiationManager = this.cnmFactoryBean.build();
}
if (this.viewResolvers == null || this.viewResolvers.isEmpty()) {
logger.warn("No ViewResolvers configured");
}
}

cnmFactoryBean是什么呢?其是一个工厂bean-​​ContentNegotiationManagerFactoryBean​​​。主要是生产一个​​contentNegotiationManager​​​并使用​​ContentNegotiationStrategy​​​配置​​contentNegotiationManager​​​。如下所示其实现了​​FactoryBean​​​、​​ServletContextAware​​​以及​​InitializingBean​​ 接口。

public class ContentNegotiationManagerFactoryBean
implements FactoryBean<ContentNegotiationManager>, ServletContextAware, InitializingBean {
//...
}
  • ​ServletContextAware​​​接口提供了方法​​void setServletContext(ServletContext servletContext);​
  • ​InitializingBean​​​接口提供了方法​​void afterPropertiesSet() throws Exception;​​​在bean属性被设置,并调用了一系列诸如​​BeanFactoryAware​​​、​​ApplicationContextAware​​​等提供的方法后,再调用​​afterPropertiesSet​​方法。
  • FactoryBean接口提供了方法​​T getObject() throws Exception; && Class<?> getObjectType(); &&default boolean isSingleton() {return true;}​​,主要用来生产一个object。

继承自WebApplicationObjectSupport

​WebApplicationObjectSupport​​​继承自​​ApplicationObjectSupport​​​并实现了​​ServletContextAware​​接口。

​ServletContextAware​​​接口提供了​​void setServletContext(ServletContext servletContext);​​​方法用来设置ServletContext。该方法执行时机在在bean属性设置后、​​ApplicationContextAware's setApplicationContext​​​方法后但是在init方法前如​​InitializingBean's afterPropertiesSet​​及其他自定义初始化方法前。

​ApplicationObjectSupport​​​实现了​​ApplicationContextAware​​​接口。​​ApplicationContextAware​​​接口提供了方法​​void setApplicationContext(ApplicationContext applicationContext) throws BeansException;​​​用来设置​​ApplicationContext​​​ 。该方法执行时机在bean属性设置后、init方法调用前如​​InitializingBean#afterPropertiesSet()​​​方法及其他自定义初始化方法。注意,其同样在​​ResourceLoaderAware#setResourceLoader​​​、​​ApplicationEventPublisherAware#setApplicationEventPublisher​​​以及​​MessageSourceAware​​后,如果实现这些Aware接口。

​ApplicationObjectSupport​​​提供了方法​​protected void initApplicationContext() throws BeansException{ }​​​让子类实现,​​WebApplicationObjectSupport​​​实现了该方法。如下所示其首先调用父类的​​initApplicationContext​​方法,然后获取servletContext 交给子类的initServletContext方法。

@Override
protected void initApplicationContext(ApplicationContext context) {
super.initApplicationContext(context);
if (this.servletContext == null && context instanceof WebApplicationContext) {
this.servletContext = ((WebApplicationContext) context).getServletContext();
if (this.servletContext != null) {
initServletContext(this.servletContext);
}
}
}

​ContentNegotiatingViewResolver​​​实现了​​initServletContext​​方法。

@Override
protected void initServletContext(ServletContext servletContext) {
Collection<ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList<>(matchingBeans.size());
for (ViewResolver viewResolver : matchingBeans) {
if (this != viewResolver) {
this.viewResolvers.add(viewResolver);
}
}
}
else {
for (int i = 0; i < this.viewResolvers.size(); i++) {
ViewResolver vr = this.viewResolvers.get(i);
if (matchingBeans.contains(vr)) {
continue;
}
String name = vr.getClass().getName() + i;
//进行bean的初始化过程
obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
}

}
// 根据order进行排序
AnnotationAwareOrderComparator.sort(this.viewResolvers);
// 设置上下文
this.cnmFactoryBean.setServletContext(servletContext);
}

方法如上所示,从ApplicationContext拿到ViewResolver放到集合​​Collection<ViewResolver> matchingBeans​​​中。如果自身成员属性​​viewResolvers​​​为null,那么把​​matchingBeans​​​中的​​ViewResolver​​​放到自身成员属性​​viewResolvers​​​中(除了当前对象哦)。如果自身成员属性viewResolvers不为null,那么遍历调用除了自身之外的​​ViewResolver​​​的​​initializeBean​​方法,也就是bean的初始化过程。

protected Object initializeBean(String beanName, Object bean, @Nullable RootBeanDefinition mbd) {
if (System.getSecurityManager() != null) {
AccessController.doPrivileged((PrivilegedAction<Object>) () -> {
invokeAwareMethods(beanName, bean);
return null;
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}

这里主要核心步骤如下:

  • ① invokeAwareMethods(beanName, bean);如BeanFactoryAware
  • ② applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);Bean后置处理器的前置方法
  • ③ invokeInitMethods(beanName, wrappedBean, mbd);反射调用bean的init方法以及自定义初始化方法。这里如果判断是InitializingBean类型,则会先调用其afterPropertiesSet方法。
  • ④ applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);Bean后置处理器的后置方法

② 视图解析

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 获取请求属性对象,实现类诸如 ServletRequestAttributes
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");

// 获取请求接受的,并且响应能够生产的MediaType
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());

// 获取候选View,然后得到一个BestView
if (requestedMediaTypes != null) {
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
// 下面是异常逻辑处理
String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
" given " + requestedMediaTypes.toString() : "";

if (this.useNotAcceptableStatusCode) {
if (logger.isDebugEnabled()) {
logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
}
return NOT_ACCEPTABLE_VIEW;
}
else {
logger.debug("View remains unresolved" + mediaTypeInfo);
return null;
}
}

这里我们主要关心两点:​​getCandidateViews​​​与​​getBestView​​。

获取候选View的逻辑如下所示,拿到能直接解析viewName的viewResolver解析后的View放入​​List<View> candidateViews​​​中。然后遍历requestedMediaTypes,根据一个个​​viewName + '.' + extension​​​尝试找到viewResolver解析后的View放入​​candidateViews​​ 中。

private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
throws Exception {

List<View> candidateViews = new ArrayList<>();
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
for (ViewResolver viewResolver : this.viewResolvers) {
//如果能直接拿到View则放入candidateViews
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
// 根据requestedMediaType 的extensions -尝试找到viewResolver进行解析
for (MediaType requestedMediaType : requestedMediaTypes) {
List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
for (String extension : extensions) {
String viewNameWithExtension = viewName + '.' + extension;
view = viewResolver.resolveViewName(viewNameWithExtension, locale);
if (view != null) {
candidateViews.add(view);
}
}
}
}
}
// 如果defaultViews不为空,放入candidateViews
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}

getBestView方法如下所示,先判断是否重定向View,如果是重定向View直接返回。然后对requestedMediaTypes和candidateViews进行遍历查找,找到一个ContentType合适的candidateView 返回,否则返回null。

@Nullable
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
// 判断重定向View
for (View candidateView : candidateViews) {
if (candidateView instanceof SmartView) {
SmartView smartView = (SmartView) candidateView;
if (smartView.isRedirectView()) {
return candidateView;
}
}
}
// 根据mediaType 和 ContentType进行比较筛选
for (MediaType mediaType : requestedMediaTypes) {
for (View candidateView : candidateViews) {
if (StringUtils.hasText(candidateView.getContentType())) {
MediaType candidateContentType = MediaType.parseMediaType(candidateView.getContentType());
if (mediaType.isCompatibleWith(candidateContentType)) {
if (logger.isDebugEnabled()) {
logger.debug("Selected '" + mediaType + "' given " + requestedMediaTypes);
}
attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
return candidateView;
}
}
}
}
return null;
}

【4】AbstractCachingViewResolver

除了​​ViewResolverComposite​​​、​​ContentNegotiatingViewResolver​​​以及​​BeanNameViewResolver​​​外,其余视图解析器都是​​AbstractCachingViewResolver​​​的子类。如下图所示其同样继承了​​WebApplicationObjectSupport​​​实现类,也就意味着其实现了​​ApplicationContextAware​​​和​​ServletContextAware​​​接口。这两个接口意味着什么,这里不再赘述。
SpringMVC常见组件之ViewResolver分析_spring_06
我们看其子类结构发现其子类仍然分为四类:

  • ① org.thymeleaf.spring5.view体系的ThymeleafViewResolver
  • ② XML结构处理的XmlViewResolver
  • ③ 处理资源国际化的ResourceBundleViewResolver
  • ④ 最常见的UrlBasedViewResolver

其提供了抽象方法loadView让子类实现,子类实现该方法返回一个View对象。​​AbstractCachingViewResolver​​​会缓存子类返回的View对象放入​​private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT);​​中

@Nullable
protected View createView(String viewName, Locale locale) throws Exception {
return loadView(viewName, locale);
}

protected abstract View loadView(String viewName, Locale locale) throws Exception;

AbstractCachingViewResolver解析视图的方法

放入如下所示,首先判断是否存在缓存实例,不存在直接走创建View逻辑。如果有缓存实例,那么根据viewName获取cacheKey(​​viewName + '_' + locale;​​​) ,先尝试从​​viewAccessCache​​​中获取View,如果View为null,则进入锁方法块-尝试从​​viewCreationCache​​​再次获取View,如果仍旧为null,则走创建逻辑。如果最终创建的View不为努力,则放入viewAccessCache和​​viewCreationCache​​。

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 如果没有缓存,直接走create逻辑
if (!isCache()) {
return createView(viewName, locale);
}
else {
// 如果有缓存,则拿到cacheKey 然后根据key获取对应的view
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
// 如果view为null,则加锁尝试创建view-联想双重校验锁?
if (view == null) {
synchronized (this.viewCreationCache) {
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
// Ask the subclass to create the View object.
view = createView(viewName, locale);
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
// 将创建后的view放入viewAccessCache与viewCreationCache
if (view != null && this.cacheFilter.filter(view, viewName, locale)) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
}
}
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace(formatKey(cacheKey) + "served from cache");
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}

【5】ThymeleafViewResolver

​ThymeleafViewResolver​​​将会解析视图返回一个​​ThymeleafView​​对象,ThymeleafView实现了render方法也就是视图渲染方法-将model与view结合起来,最后将信息写入到response中。

① 获取view

@Override
protected View createView(final String viewName, final Locale locale) throws Exception {
// First possible call to check "viewNames": before processing redirects and forwards
if (!this.alwaysProcessRedirectAndForward && !canHandle(viewName, locale)) {
vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
return null;
}
// Process redirects (HTTP redirects)
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
vrlogger.trace("[THYMELEAF] View \"{}\" is a redirect, and will not be handled directly by ThymeleafViewResolver.", viewName);
final String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length(), viewName.length());
final RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, REDIRECT_URL_PREFIX);
}
// Process forwards (to JSP resources)
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
// The "forward:" prefix will actually create a Servlet/JSP view, and that's precisely its aim per the Spring
// documentation. See http://docs.spring.io/spring-framework/docs/4.2.4.RELEASE/spring-framework-reference/html/mvc.html#mvc-redirecting-forward-prefix
vrlogger.trace("[THYMELEAF] View \"{}\" is a forward, and will not be handled directly by ThymeleafViewResolver.", viewName);
final String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length(), viewName.length());
return new InternalResourceView(forwardUrl);
}
// Second possible call to check "viewNames": after processing redirects and forwards
if (this.alwaysProcessRedirectAndForward && !canHandle(viewName, locale)) {
vrlogger.trace("[THYMELEAF] View \"{}\" cannot be handled by ThymeleafViewResolver. Passing on to the next resolver in the chain.", viewName);
return null;
}
vrlogger.trace("[THYMELEAF] View {} will be handled by ThymeleafViewResolver and a " +
"{} instance will be created for it", viewName, getViewClass().getSimpleName());
return loadView(viewName, locale);
}

这里很有意思,解释如下:

  • ① 其首先判断是否能够处理viewName,不能处理直接返回null;
  • ②​​REDIRECT_URL_PREFIX = "redirect:"​​判断是否为重定向view,如果是重定向view则截取viewName获得redirectUrl,创建RedirectView实例并进行其bean初始化过程;
  • ③​​FORWARD_URL_PREFIX = "forward:"​​判断是否为转发view,如果是转发View则截取viewName获取forwardUrl,返回一个InternalResourceView实例。
  • ④ 如果​​alwaysProcessRedirectAndForward​​ 被设置为true,但是不能处理当前viewName,返回null;
  • ⑤ 使用​​ThymeleafViewResolver​​ 解析viewName。

loadView方法如下所示:

@Override
protected View loadView(final String viewName, final Locale locale) throws Exception {

final AutowireCapableBeanFactory beanFactory = getApplicationContext().getAutowireCapableBeanFactory();

final boolean viewBeanExists = beanFactory.containsBean(viewName);
final Class<?> viewBeanType = viewBeanExists? beanFactory.getType(viewName) : null;

final AbstractThymeleafView view;
if (viewBeanExists && viewBeanType != null && AbstractThymeleafView.class.isAssignableFrom(viewBeanType)) {
// AppCtx has a bean with name == viewName, and it is a View bean. So let's use it as a prototype!
//
// This can mean two things: if the bean has been defined with scope "prototype", we will just use it.
// If it hasn't we will create a new instance of the view class and use its properties in order to
// configure this view instance (so that we don't end up using the same bean from several request threads).
//
// Note that, if Java-based configuration is used, using @Scope("prototype") would be the only viable
// possibility here.

final BeanDefinition viewBeanDefinition =
(beanFactory instanceof ConfigurableListableBeanFactory ?
((ConfigurableListableBeanFactory)beanFactory).getBeanDefinition(viewName) :
null);

if (viewBeanDefinition == null || !viewBeanDefinition.isPrototype()) {
// No scope="prototype", so we will just apply its properties. This should only happen with XML config.
final AbstractThymeleafView viewInstance = BeanUtils.instantiateClass(getViewClass());
view = (AbstractThymeleafView) beanFactory.configureBean(viewInstance, viewName);
} else {
// This is a prototype bean. Use it as such.
view = (AbstractThymeleafView) beanFactory.getBean(viewName);
}

} else {

final AbstractThymeleafView viewInstance = BeanUtils.instantiateClass(getViewClass());

if (viewBeanExists && viewBeanType == null) {
// AppCtx has a bean with name == viewName, but it is an abstract bean. We still can use it as a prototype.

// The AUTOWIRE_NO mode applies autowiring only through annotations
beanFactory.autowireBeanProperties(viewInstance, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
// A bean with this name exists, so we apply its properties
beanFactory.applyBeanPropertyValues(viewInstance, viewName);
// Finally, we let Spring do the remaining initializations (incl. proxifying if needed)
view = (AbstractThymeleafView) beanFactory.initializeBean(viewInstance, viewName);

} else {
// Either AppCtx has no bean with name == viewName, or it is of an incompatible class. No prototyping done.

// The AUTOWIRE_NO mode applies autowiring only through annotations
beanFactory.autowireBeanProperties(viewInstance, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
// Finally, we let Spring do the remaining initializations (incl. proxifying if needed)
view = (AbstractThymeleafView) beanFactory.initializeBean(viewInstance, viewName);

}

}

view.setTemplateEngine(getTemplateEngine());
view.setStaticVariables(getStaticVariables());


// We give view beans the opportunity to specify the template name to be used
if (view.getTemplateName() == null) {
view.setTemplateName(viewName);
}

if (!view.isForceContentTypeSet()) {
view.setForceContentType(getForceContentType());
}
if (!view.isContentTypeSet() && getContentType() != null) {
view.setContentType(getContentType());
}
if (view.getLocale() == null && locale != null) {
view.setLocale(locale);
}
if (view.getCharacterEncoding() == null && getCharacterEncoding() != null) {
view.setCharacterEncoding(getCharacterEncoding());
}
if (!view.isProducePartialOutputWhileProcessingSet()) {
view.setProducePartialOutputWhileProcessing(getProducePartialOutputWhileProcessing());
}
return view;
}

代码看起来比较长,其实核心就是获取Bean的实例然后进行bean的初始化过程最后设置诸如​​TemplateEngine​​​、​​StaticVariables​​​、​​TemplateName​​​以及​​CharacterEncoding​​​等属性。这里​​getViewClass​​​返回的是​​ThymeleafView.class​​。也就是说该方法会返回一个ThymeleafView实例,除非你自定义实现类继承自AbstractThymeleafView。

【6】UrlBasedViewResolver

SpringMVC常见组件之ViewResolver分析_视图解析器_07

除去​​ThymeleafViewResolver​​​(对应Thymeleaf)之外,我们最熟悉的可能就是​​InternalResourceViewResolver​​​(对应JSP)和​​FreeMarkerViewResolver​​(对应Freemarker)了。

① UrlBasedViewResolver的重要属性

//重定向前缀
public static final String REDIRECT_URL_PREFIX = "redirect:";

// 转发前缀
public static final String FORWARD_URL_PREFIX = "forward:";

// 视图类型
@Nullable
private Class<?> viewClass;

// prefix="/WEB-INF/jsp/", suffix=".jsp", viewname="test" ->
// "/WEB-INF/jsp/test.jsp"
private String prefix = "";

private String suffix = "";

//view的contentType
@Nullable
private String contentType;

//true表示重定向时相对于web application root
private boolean redirectContextRelative = true;

//true表示重定向时与HTTP 1.0 一致,如302;false则与HTTP 1.1一致,303
private boolean redirectHttp10Compatible = true;
//重定向主机host
@Nullable
private String[] redirectHosts;

//为view设置请求上下文中的属性
@Nullable
private String requestContextAttribute;

//Properties中的属性,你定义视图解析时的property
/** Map of static attributes, keyed by attribute name (String). */
private final Map<String, Object> staticAttributes = new HashMap<>();

// 是否将path variables放到model里面,默认为null,让具体view决定
@Nullable
private Boolean exposePathVariables;

//是否能够作为请求属性访问spring容器中的bean,默认是false
@Nullable
private Boolean exposeContextBeansAsAttributes;

//指定上下文中应该公开的bean的名称。如果该值为非null,则只有指定的bean有资格作为属性公开。
@Nullable
private String[] exposedContextBeanNames;

//设置能够被视图解析器处理的viewName
@Nullable
private String[] viewNames;

//Integer.MAX_VALUE
private int order = Ordered.LOWEST_PRECEDENCE;

为什么分析这些属性呢?我们看一下以前的一个配置实例:

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsp/"/>
<property name="suffix" value=".jsp"/>
</bean>

② createView方法

createView覆盖了父类的createViewf方法。如下所示,方法分为四大块:

  • ① 根据viewNames判断是否能够处理当前viewName;
  • ② 处理重定向请求,获取一个RedirectView然后调用initializeBean方法进行初始化;
  • ③ 处理转发请求,获取一个InternalResourceView,然后调用initializeBean方法进行初始化;
  • ④ 调用父类的createView方法
@Override
protected View createView(String viewName, Locale locale) throws Exception {
// If this resolver is not supposed to handle the given view,
// return null to pass on to the next resolver in the chain.
if (!canHandle(viewName, locale)) {
return null;
}

// Check for special "redirect:" prefix.
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl,
isRedirectContextRelative(), isRedirectHttp10Compatible());
String[] hosts = getRedirectHosts();
if (hosts != null) {
view.setHosts(hosts);
}
return applyLifecycleMethods(REDIRECT_URL_PREFIX, view);
}

// Check for special "forward:" prefix.
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
InternalResourceView view = new InternalResourceView(forwardUrl);
return applyLifecycleMethods(FORWARD_URL_PREFIX, view);
}

// Else fall back to superclass implementation: calling loadView.
return super.createView(viewName, locale);
}

我们看下​​applyLifecycleMethods​​​,如下所示调用​​BeanFactory​​​的​​initializeBean​​​方法对​​view​​进行初始化,核心步骤就是:

  • invokeAwareMethods
  • applyBeanPostProcessorsBeforeInitialization
  • invokeInitMethods
  • applyBeanPostProcessorsAfterInitialization
protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {
ApplicationContext context = getApplicationContext();
if (context != null) {
Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName);
if (initialized instanceof View) {
return (View) initialized;
}
}
return view;
}

③ loadView与buildView

loadView同样覆盖了父类的方法,如下所示委派buildView方法获取一个AbstractUrlBasedView 实例,然后调用其生命周期方法进行bean的初始化。

@Override
protected View loadView(String viewName, Locale locale) throws Exception {
AbstractUrlBasedView view = buildView(viewName);
View result = applyLifecycleMethods(viewName, view);
return (view.checkResource(locale) ? result : null);
}

​buildView​​​方法如下所示,首先根据viewClass使用方法​​(AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass)​​获取一个view实例。然后进行各种属性的赋值如url、properties、contentType等。

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
// 获取一个view实例
AbstractUrlBasedView view = instantiateView();
//如下都是属性的赋值
view.setUrl(getPrefix() + viewName + getSuffix());
view.setAttributesMap(getAttributesMap());

String contentType = getContentType();
if (contentType != null) {
view.setContentType(contentType);
}

String requestContextAttribute = getRequestContextAttribute();
if (requestContextAttribute != null) {
view.setRequestContextAttribute(requestContextAttribute);
}

Boolean exposePathVariables = getExposePathVariables();
if (exposePathVariables != null) {
view.setExposePathVariables(exposePathVariables);
}
Boolean exposeContextBeansAsAttributes = getExposeContextBeansAsAttributes();
if (exposeContextBeansAsAttributes != null) {
view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
}
String[] exposedContextBeanNames = getExposedContextBeanNames();
if (exposedContextBeanNames != null) {
view.setExposedContextBeanNames(exposedContextBeanNames);
}

return view;
}

【7】InternalResourceViewResolver

该解析器处理​​viewName​​​返回一个​​InternalResourceView​​​或​​JstlView​​​实例。当使用链式ViewResolvers时,​​InternalResourceViewResolver​​始终需要配置为最后一个,因为它将尝试解析任何视图名称,而不管底层资源是否实际存在。

① 构造函数

// 调用无参构造函数,设置前缀和后缀
public InternalResourceViewResolver(String prefix, String suffix) {
this();
setPrefix(prefix);
setSuffix(suffix);
}
//无参构造函数
public InternalResourceViewResolver() {
// 默认返回InternalResourceView
Class<?> viewClass = requiredViewClass();
if (InternalResourceView.class == viewClass && jstlPresent) {
viewClass = JstlView.class;
}
setViewClass(viewClass);
}

② 覆盖父类的instantiateView

方法如下所示,如果​​viewClass​​​是​​InternalResourceViewz​​​则​​new InternalResourceView();​​​如果是JstlView,则获取一个JstlView实例;否则最后调用父类的​​instantiateView​​方法。

@Override
protected AbstractUrlBasedView instantiateView() {
return (getViewClass() == InternalResourceView.class ? new InternalResourceView() :
(getViewClass() == JstlView.class ? new JstlView() : super.instantiateView()));
}

③ 覆盖父类的buildView方法

这里调用了父类的buildView方法,然后做了两个增强:

  • alwaysInclude,指定是否始终包含视图而不是转发视图。默认值为“false”。打开此标志以强制使用Servlet包含,即使可以转发
  • preventDispatchLoop,设置是否显式阻止转发回当前处理程序路径。默认值为“false”。对于基于约定的视图,如果将其转发回当前处理程序路径是一个确定错误,请将其切换为“true”。
@Override
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;
}