ViewResolver主要的作用是根据视图名和Locale解析出视图,解析过程主要做两件事:解析出使用的模板和视图的类型。
文章目录
1. 继承结构图
Spring MVC中ViewResolver整体可以分为四大类:AbstractCachingViewResolver、BeanNameViewResolver、ContentNegotiatingViewResolver和ViewResolverComposite。
2. ViewResolverComposite
ViewResolverComposite大家看名字就明白了,它是一个封装着多个ViewResolver的容器,解析视图时遍历封装着的ViewResolver具体解析,不过这里的ViewResolverComposite除了遍历成员解析视图外还给成员进行了必要的初始化,其中包括对实现了ApplicationContextAware接口的ViewResolver设置ApplicationContext、给实现ServletContextAware接口的ViewResolver设置ServletContext以及对实现InitializingBean接口的ViewResolver调用afterPropertiesSet方法。
public class ViewResolverComposite implements ViewResolver, Ordered, InitializingBean,
ApplicationContextAware, ServletContextAware {
private final List<ViewResolver> viewResolvers = new ArrayList<>();
private int order = Ordered.LOWEST_PRECEDENCE;
/ * *
*设置要委托的视图视图解析器列表。
* /
public void setViewResolvers(List<ViewResolver> viewResolvers) {
this.viewResolvers.clear();
if (!CollectionUtils.isEmpty(viewResolvers)) {
this.viewResolvers.addAll(viewResolvers);
}
}
/**
* Return the list of view viewResolvers to delegate to.
*/
public List<ViewResolver> getViewResolvers() {
return Collections.unmodifiableList(this.viewResolvers);
}
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@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();
}
}
}
@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;
}
}
差不多就是一个组合模式。
3. ContentNegotiatingViewResolver
ContentNegotiatingViewResolver解析器的作用是在别的解析器解析的结果上增加了对MediaType和后缀的支持,MediaType即媒体类型,有的地方也叫Content-Type,比如,常用的text/html、text/javascript以及表示上传表单的multipart/form-data等。
对视图的解析并不是自己完成的而是使用所封装的ViewResolver来进行的。整个过程是这样的:首先遍历所封装的ViewResolver具体解析视图,可能会解析出多个视图,然后再使用request获取MediaType,也可能会有多个结果,最后对这两个结果进行匹配查找出最优的视图。
protected void initServletContext(ServletContext servletContext) { //获取容器中所有匹配的Bean
Collection<ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
//没有手动注册就将找到的ViewResolvers设置进去
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;
obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
}
}
//按照order进行排序
AnnotationAwareOrderComparator.sort(this.viewResolvers);
this.cnmFactoryBean.setServletContext(servletContext);
}
解析视图的过程在resolveViewName方法中:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
//静态方法获取RequestAttributes 进而获取request
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
//获取需要满足的requestedMediaTypes 条件
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
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;
}
}
RequestAttributes是在FrameworkServlet内进行初始化的。
还有两个getCandidateViews,getBestView方法,感兴趣的可以去了解下。
4. AbstractCachingViewResolver系列
AbstractCachingViewResolver提供了统一的缓存功能,当视图解析过一次就被缓存起来,直到缓存被删除前视图的解析都会自动从缓存中获取。
它的直接继承类有三个:ResourceBundleViewResolver、XmlViewResolver和UrlBased-ViewResolver。
第一个的用法在前面已经介绍过了,它是通过使用properties属性配置文件解析视图的;
第二个跟第一个非常相似,只不过它使用了xml配置文件;
第三个是所有直接将逻辑视图作为url查找模板文件的ViewResolver的基类,因为它设置了统一的查找模板的规则,所以它的子类只需要确定渲染方式也就是视图类型就可以了,它的每一个子类对应一种视图类型。
重要字段:
前者是ConcurrentHashMap类型,它内部使用了细粒度的锁,支持并发访问,效率非常高,而后者主要提供了限制缓存最大数的功能,效率不如前者高。使用的最多的获取缓存是从前者获取的,而添加缓存会给两者同时添加,后者如果发现缓存数量已达到上限时会在删除自己最前面的缓存的同时也删除前者对应的缓存。
接下来看看它解析视图的过程:
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
if (!isCache()) {
return createView(viewName, locale);
}
else {
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
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;
}
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);
}
}
逻辑非常简单,首先判断是否开启了缓存功能,如果没开启则直接调用createView创建视图,否则检查是否已经存在缓存中,如果存在则直接获取并返回,否则使用createView创建一个,然后保存到缓存中并返回。createView内部直接调用了loadView方法,而loadView是一个模板方法,留给子类实际创建视图,这也是子类解析视图的入口方法。
4.1 UrlBasedViewResolver
UrlBasedViewResolver里面重写了父类的getCacheKey、createView和loadView三个方法。
/ * *
*这个实现只返回视图名,
*因为这个视图解析器不支持本地化解析。
* /
@Override
protected Object getCacheKey(String viewName, Locale locale) {
return viewName;
}
/ * *
*重写来实现对“重定向”前缀的检查。
*
在{@code loadView}中不可能,因为被重写了
* {@code loadView}版本的子类可能依赖于
超类总是创建所需视图类的实例。
* @see #未经
* @see # requiredViewClass
* /
@Override
protected View createView(String viewName, Locale locale) throws Exception {
//如果这个解析器不应该处理给定的视图,
//返回null传递给链中的下一个解析器。
if (!canHandle(viewName, locale)) {
return null;
}
//检查特殊的“重定向:”前缀。
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);
}
//检查特殊的“forward:”前缀。
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返回到超类实现:调用loadView。
return super.createView(viewName, locale);
}
/ * *
属性的新实例委托给{@code buildView}
*指定视图类。应用以下Spring生命周期方法
*(由通用的Spring bean工厂支持):
* < ul >
*
applicationcontext taware 's {@code setApplicationContext}
*
初始化bean的{@code afterPropertiesSet}
* < / ul >
要检索的视图的名称
返回视图实例
* @抛出异常,如果无法解决视图
* @see # buildView(字符串)
* @see org.springframework.context.ApplicationContextAware # setApplicationContext
* @see org.springframework.beans.factory.InitializingBean # afterPropertiesSet
*/
@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);
}
在createView中:
首先检查是否可以解析传入的逻辑视图,如果不可以则返回null让别的ViewResolver解析,接着分别检查是不是redirect视图或者forward视图,检查的方法是看是不是以“redirect:”或“forward:”开头,如果是则返回相应视图,如果都不是则交给父类的createView,父类中又调用了loadView。
loadView一共执行了三句代码:
①使用buildView方法创建View;②使用applyLifecycle-Methods方法对创建的View初始化;③检查view对应的模板是否存在,如果存在则将初始化的视图返回,否则返回null交给下一个ViewResolver处理。
接下来看bulid view 方法:
/ * *
*创建指定视图类的新视图实例并配置它。
* 不对预定义视图实例执行任何查找吗?
bean容器定义的Spring生命周期方法不必这样做
*被称为此处;这些将由{@code loadView}方法应用
*此方法返回后。
*
子类通常会调用{@code super.buildView(viewName)}
*首先,在设置进一步属性之前。{@code loadView}
*将在这个过程的最后应用Spring生命周期方法。
要构建的视图的名称
返回视图实例
* @抛出异常,如果无法解决视图
* @see #loadView(字符串,java.util.Locale)
* /
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
Class<?> viewClass = getViewClass();
Assert.state(viewClass != null, "No view class");
AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
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;
}
用于具体创建View,理解了这个方法就知道AbstractUrlBasedView系列中View是怎么创建的了,它的子类只是在这里创建出来的视图的基础上设置了一些属性。
View的创建过程也非常简单,首先根据使用BeanUtils根据getViewClass方法的返回值创建出view,然后将viewName加上前缀、后缀设置为url,前缀和后缀可以在配置ViewResolver时进行设置,这样View就创建完了,接下来根据配置给View设置一些参数,具体内容已经注释到代码上了。这里的getViewClass返回其中的viewClass属性,代表View的视图类型,可以在子类通过setViewClass方法进行设置。
UrlBasedViewResolver的子类主要做三件事:
①通过重写requiredViewClass方法修改了必须符合的视图类型的值;
②使用setViewClass方法设置了所用的视图类型;
③给创建出来的视图设置一些属性。
小结:
解析视图的核心工作就是查找模板文件和视图类型,而查找的主要参数只有viewName一个(Locale只能起辅助作用)。这就产生了三种解析思路:
①使用viewName查找模板文件;
②使用viewName查找视图类型;
③使用viewName同时查找模板文件和视图类型。
Spring MVC对这三种思路都提供了实现的方式,
第一种思路对应的是UrlBasedViewResolver系列;
第二种思路对应的是BeanNameViewResolver;
第三种思路对应的是ResourceBundleViewResolver和XmlViewResolver。