1. 概述
本文,我们来分享 ViewResolver 组件。在 《精尽 Spring MVC 源码分析 —— 组件一览》 中,我们对它已经做了介绍:
org.springframework.web.servlet.ViewResolver
,实体解析器接口,根据视图名和国际化,获得最终的视图 View 对象。代码如下:
// ViewResolver.java |
2. 类图
ViewResolver 的类图如下:
虽然实现类比较多,ViewResolver 分成五类实现类,就是 ViewResolver 的五个直接实现类。
3. 初始化
我们以默认配置的 Spring Boot 场景下为例,来一起看看 DispatcherServlet 的 #initViewResolvers(ApplicationContext context)
方法,初始化 viewResolvers
变量。代码如下:
// DispatcherServlet.java |
- 一共有三种情况,初始化
viewResolvers
属性。 - 默认情况下,
detectAllViewResolvers
为true
,所以走情况一的逻辑,自动扫描 ViewResolver 类型的 Bean 们。在默认配置的 Spring Boot 场景下,viewResolvers
的结果是:- ContentNegotiatingViewResolver
- BeanNameViewResolver
- ThymeleafViewResolver
- ViewResolverComposite
- InternalResourceViewResolver
从实现上来说,ContentNegotiatingViewResolver 是最最最重要的 ViewResolver 实现类,所以我们先开始瞅瞅它。
4. ContentNegotiatingViewResolver
org.springframework.web.servlet.view.ContentNegotiatingViewResolver
,实现 ViewResolver、Ordered、InitializingBean 接口,继承 WebApplicationObjectSupport 抽象类,基于内容类型来获取对应 View 的 ViewResolver 实现类。
其中,内容类型指的是 "Content-Type"
和拓展后缀。
4.1 构造方法
// ContentNegotiatingViewResolver.java |
viewResolvers
属性,ViewResolver 数组。对于来说,ContentNegotiatingViewResolver 会使用这些viewResolvers
们,解析出所有的 View 们,然后基于内容类型来获取对应的 View 们。此时的 View 结果,可能是一个,可能是多个,所以需要比较获取到最优的 View 对象。defaultViews
属性,默认 View 数组。那么此处的默认是什么意思呢?在viewResolvers
们解析出所有的 View 们的基础上,也会添加defaultViews
到 View 结果中。😈 如果听起来有点绕,下面看具体的代码,会更加易懂。
order
属性,顺序,优先级最高。所以,这也是为什么在 「3. 初始化」 中排行第一。
4.2 initServletContext
实现 #initServletContext(ServletContext servletContext)
方法,初始化 viewResolvers
属性。代码如下:
// ContentNegotiatingViewResolver.java |
<1>
处,扫描所有 ViewResolver 的 Bean 们。- 【重要】
<1.1>
处,情况一,如果viewResolvers
为空,则将matchingBeans
作为viewResolvers
。默认情况下,走的是这段逻辑。所以此时viewResolvers
会有 BeanNameViewResolver、ThymeleafViewResolver、ViewResolverComposite、InternalResourceViewResolver 四个对象。 <1.2>
处,情况二,如果viewResolvers
非空,则和matchingBeans
进行比对,判断哪些未进行初始化,那么需要进行初始化。有点点绕,艿艿也懵逼了下,胖友在瞅瞅。<1.3>
处,排序viewResolvers
数组。
- 【重要】
<2>
处,设置cnmFactoryBean
的servletContext
属性。
4.3 afterPropertiesSet
实现 #afterPropertiesSet()
方法,初始化 contentNegotiationManager
属性。代码如下:
// ContentNegotiatingViewResolver.java |
4.4 resolveViewName
实现 #resolveViewName(String viewName, Locale locale)
方法,代码如下:
// ContentNegotiatingViewResolver.java |
<1>
处,调用#getCandidateViews(HttpServletRequest request)
方法,获得 MediaType 数组。详细解析,见 「4.4.1 getMediaTypes」 。<2.1>
处,调用#getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
方法,获得匹配的 View 数组。详细解析,见 「4.4.2 getCandidateViews」 。<2.3>
处,调用#getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs)
方法,筛选最匹配的 View 对象。详细解析,见 「4.4.3 getBestView」 。<3>
处,如果匹配不到 View 对象,则根据useNotAcceptableStatusCode
,返回NOT_ACCEPTABLE_VIEW
或null
。其中,NOT_ACCEPTABLE_VIEW
变量,代码如下:// ContentNegotiatingViewResolver.java
private static final View NOT_ACCEPTABLE_VIEW = new View() {
public String getContentType() {
return null;
}
public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
}
};- 这个视图的渲染,只会设置响应状态码为
SC_NOT_ACCEPTABLE
。
- 这个视图的渲染,只会设置响应状态码为
😈 逻辑有丢丢上,胖友耐心了,嘿嘿。
4.4.1 getMediaTypes
#getCandidateViews(HttpServletRequest request)
方法,获得 MediaType 数组。代码如下:
// ContentNegotiatingViewResolver.java |
- 逻辑虽然灰常长,但是在 《精尽 Spring MVC 源码解析 —— HandlerAdapter 组件(四)之 HandlerMethodReturnValueHandler》 中的 「5.4.1 HandlerMethodReturnValueHandler」 中,已经看过类似的 MediaType 的匹配逻辑,所以就不重复赘述。
4.4.2 getCandidateViews
#getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
方法,获得匹配的 View 数组。代码如下:
// ContentNegotiatingViewResolver.java |
candidateViews
属性,View 数组。下面,一共有两个来源。- ========== 来源一 ==========
<1>
处,来源一,通过viewResolvers
解析出 View 数组结果,添加到candidateViews
中。<1.1>
处,遍历viewResolvers
数组。- 【重要】
<1.2>
处,情况①,获得 View 对象,添加到candidateViews
中。 <1.3>
处,情况②,带有文拓展后缀的方式,获得 View 对象,添加到candidateViews
中。😈 当然,默认情况下,这个逻辑,我们可以无视,因为在<1.3.2>
处,我们在默认情况下,并未配置 MediaType 对应的拓展后缀。<1.3.1>
处,遍历 MediaType 数组。<1.3.2>
处,获得 MediaType 对应的拓展后缀的数组。<1.3.3>
处,遍历拓展后缀的数组。- 【重要】
<1.3.4>
处,带有文拓展后缀的方式,获得 View 对象,添加到candidateViews
中。
<2>
处,来源二,添加defaultViews
到candidateViews
中。
4.4.3 getBestView
#getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs)
方法,筛选最匹配的 View 对象。代码如下:
// ContentNegotiatingViewResolver.java |
<1>
处,遍历candidateView
数组,如果有重定向的 View 类型,则返回它。也就是说,重定向的 View ,优先级更高。<2>
处,遍历requestedMediaTypes
和candidateViews
数组,先找到一个 MediaType 类型匹配,则返回该 View 对象,然后返回它。也就是说,优先级的匹配规则,由 ViewResolver 在viewResolvers
的位置,越靠前,优先级越高。
5. BeanNameViewResolver
org.springframework.web.servlet.view.BeanNameViewResolver
,实现 ViewResolver、Ordered 接口,继承 WebApplicationObjectSupport 抽象类,基于 Bean 的名字获得 View 对象的 ViewResolver 实现类。
5.1 构造方法
// BeanNameViewResolver.java |
5.2 resolveViewName
实现 #resolveViewName(String viewName, Locale locale)
方法,获得 Bean 的名字获得 View 对象。代码如下:
// BeanNameViewResolver.java |
6. ViewResolverComposite
org.springframework.web.servlet.view.ViewResolverComposite
,实现 ViewResolver、Ordered、InitializingBean、ApplicationContextAware、ServletContextAware 接口,复合的 ViewResolver 实现类。
6.1 构造方法
// ViewResolverComposite.java |
6.2 afterPropertiesSet
实现 #afterPropertiesSet()
方法,进一步初始化。代码如下:
// ViewResolverComposite.java |
6.3 resolveViewName
实现 #resolveViewName(String viewName, Locale locale)
方法,代码如下:
// ViewResolverComposite.java |
7. AbstractCachingViewResolver
org.springframework.web.servlet.view.AbstractCachingViewResolver
,实现 ViewResolver 接口,继承 WebApplicationObjectSupport 抽象类,提供通用的缓存的 ViewResolver 抽象类。对于相同的视图名,返回的是相同的 View 对象,所以通过缓存,可以进一步提供性能。
7.1 构造方法
// AbstractCachingViewResolver.java |
大多数变量比较易懂。比较有趣的是
viewAccessCache
和viewCreationCache
属性的存在。- 通过
viewAccessCache
属性,提供更快的访问 View 缓存。 - 通过
viewCreationCache
属性,提供缓存的上限的功能。可能有胖友不太了解为什么 LinkedHashMap 能实现 LRU 缓存过期的功能,可以看看 《LRU 缓存实现(Java)》 。 KEY 是通过
#getCacheKey(String viewName, Locale locale)
方法,获得缓存 KEY 。代码如下:// AbstractCachingViewResolver.java
/**
* Return the cache key for the given view name and the given locale.
* <p>Default is a String consisting of view name and locale suffix.
* Can be overridden in subclasses.
* <p>Needs to respect the locale in general, as a different locale can
* lead to a different view resource.
*/
protected Object getCacheKey(String viewName, Locale locale) {
return viewName + '_' + locale;
}- 😈
- 通过
7.2 loadView
#loadView(String viewName, Locale locale)
抽象方法,加载 viewName
对应的 View 对象。代码如下:
// AbstractCachingViewResolver.java |
7.3 createView
#createView(String viewName, Locale locale)
方法,创建 viewName
对应的 View 对象。代码如下:
// AbstractCachingViewResolver.java |
- 在方法内部,就会调用 「7.2 loadView」 方法。
7.4 resolveViewName
实现 #resolveViewName(String viewName, Locale locale)
方法,代码如下:
// AbstractCachingViewResolver.java |
- 😈 虽然代码略长,但是逻辑还是非常清晰的。胖友自己瞅瞅,妥妥的。
7.5 子类
关于 AbstractCachingViewResolver 抽象类,有三个子类:
- UrlBasedViewResolver
- XmlViewResolver
- ResourceBundleViewResolver
其中,UrlBasedViewResolver 是相比更关键的子类,所以在 「8. UrlBasedViewResolver」 中,我们一起来瞅瞅。
而另外两个子类,感兴趣的胖友,自己去看看罗。
8. UrlBasedViewResolver
org.springframework.web.servlet.view.UrlBasedViewResolver
,实现 Ordered 接口,继承 AbstractCachingViewResolver 抽象类,基于 Url 的 ViewResolver 实现类。
8.1 构造方法
// UrlBasedViewResolver.java |
- 那个,还是变量有点多,我们随着下面的方法,一起来瞅瞅。哈哈哈哈
8.2 initApplicationContext
实现 #initApplicationContext()
方法,进一步初始化。代码如下:
// UrlBasedViewResolver.java |
- 子类中,我们会看到,
viewClass
属性一般会在构造中法中设置。
8.3 getCacheKey
重写 #getCacheKey(String viewName, Locale locale)
方法,忽略 locale
参数,仅仅使用 viewName
作为缓存 KEY 。代码如下:
// UrlBasedViewResolver.java |
😈 也就是说,不支持 Locale 特性。
8.4 canHandle
#canHandle(String viewName, Locale locale)
方法,判断传入的视图名是否可以被处理。代码如下:
// UrlBasedViewResolver.java |
- 一般情况下,
viewNames
为空,所以会满足viewNames == null
代码块。也就说,所有视图名都可以被处理。
8.5 applyLifecycleMethods
#applyLifecycleMethods(String viewName, AbstractUrlBasedView view)
方法,代码如下:
// UrlBasedViewResolver.java |
这个方法的逻辑比较易懂,但是不太明白具体的使用场景。😈 感觉先不用理解它的用途也可以。
8.6 createView
重写 #createView(String viewName, Locale locale)
方法,增加了对 REDIRECT、FORWARD 的情况的处理。代码如下:
// UrlBasedViewResolver.java |
8.9 loadView
实现 #loadView(String viewName, Locale locale)
方法,加载 viewName 对应的 View 对象。代码如下:
// UrlBasedViewResolver.java |
其中,
<x>
处,调用#buildView(String viewName)
方法,创建viewName
对应的 View 对象。代码如下:// UrlBasedViewResolver.java
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
Class<?> viewClass = getViewClass();
Assert.state(viewClass != null, "No view class");
// 创建 AbstractUrlBasedView 对象
AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
// 设置各种属性
view.setUrl(getPrefix() + viewName + getSuffix());
String contentType = getContentType();
if (contentType != null) {
view.setContentType(contentType);
}
view.setRequestContextAttribute(getRequestContextAttribute());
view.setAttributesMap(getAttributesMap());
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;
}
8.10 requiredViewClass
#requiredViewClass()
方法,定义了产生的视图。代码如下:
// UrlBasedViewResolver.java |
8.11 子类
关于 UrlBasedViewResolver 抽象类,有三个子类:
- AbstractTemplateViewResolver
- InternalResourceViewResolver
- TilesViewResolver
- ScriptTemplateViewResolver
- XsltViewResolver
其中,InternalResourceViewResolver 和 AbstractTemplateViewResolver 是相比更关键的子类,所以在 「9. InternalResourceViewResolver」 和 「10. AbstractTemplateViewResolver」 中,我们一起来瞅瞅。
而另外三个子类,感兴趣的胖友,自己去看看罗。
9. InternalResourceViewResolver
org.springframework.web.servlet.view.InternalResourceViewResolver
,继承 UrlBasedViewResolver 类,解析出 JSP 的 ViewResolver 实现类。
9.1 构造方法
// InternalResourceViewResolver.java |
- 从构造方法中,可以看出,视图名会是 InternalResourceView 或 JstlView 类。😈 实际上,JstlView 是 InternalResourceView 的子类。
9.2 buildView
重写 #buildView(String viewName)
方法,代码如下:
// InternalResourceViewResolver.java |
- 增加设置两个属性。
10. AbstractTemplateViewResolver
org.springframework.web.servlet.view.AbstractTemplateViewResolver
,继承 UrlBasedViewResolver 类,解析出 AbstractTemplateView 的 ViewResolver 抽象类。
10.1 构造方法
// AbstractTemplateViewResolver.java |
10.2 requiredViewClass
重写 #requiredViewClass()
方法,返回 AbstractTemplateView 类。代码如下:
// AbstractTemplateViewResolver.java |
10.3 buildView
重写 #buildView(String viewName)
方法,代码如下:
// AbstractTemplateViewResolver.java |
- 增加设置五个属性。
10.4 子类
关于 AbstractTemplateViewResolver 抽象类,有二个子类:
FreeMarkerViewResolver
// FreeMarkerViewResolver.java
public class FreeMarkerViewResolver extends AbstractTemplateViewResolver {
public FreeMarkerViewResolver() {
setViewClass(requiredViewClass());
}
public FreeMarkerViewResolver(String prefix, String suffix) {
this();
setPrefix(prefix);
setSuffix(suffix);
}
/**
* Requires {@link FreeMarkerView}.
*/
protected Class<?> requiredViewClass() {
return FreeMarkerView.class;
}
}GroovyMarkupViewResolver
// GroovyMarkupViewResolver.java
public class GroovyMarkupViewResolver extends AbstractTemplateViewResolver {
public GroovyMarkupViewResolver() {
setViewClass(requiredViewClass());
}
public GroovyMarkupViewResolver(String prefix, String suffix) {
this();
setPrefix(prefix);
setSuffix(suffix);
}
protected Class<?> requiredViewClass() {
return GroovyMarkupView.class;
}
/**
* This resolver supports i18n, so cache keys should contain the locale.
*/
protected Object getCacheKey(String viewName, Locale locale) {
return viewName + '_' + locale;
}
}
666. 彩蛋
本文涉及的,还有一个非常重要的组件没有进行分享,org.springframework.web.servlet.View
体系。整体类图如下:类图
艿艿暂时不会去详细解析这块。😈 估计,也没什么人感兴趣,哈哈哈哈。
参考和推荐如下文章: