1. ViewResolver
如果ModelAndView对象中的view属性值是String类型,则根据其视图名称解析成View对象,View是用来渲染页面的,将ModelAndView中的Model填入模板中生成HTML或其他格式的文件。可以设置多个解析策略,它们将通过order属性来排序,order越小优先级越高,当前面的视图解析器解析成功后将直接返回View对象,不再执行后续的解析器。
我们前面介绍过DispatcherServlet中取得ViewResolver的集合循环遍历,调用ViewResolver的resolveViewName方法来解析视图名获取View对象,源码如下:
protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,
HttpServletRequest request) throws Exception {
//遍历ViewResolver,调用resolveViewName方法返回View
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
return null;
}
ViewResolver接口的实现类如下:
1.1 AbstractCachingViewResolver(带有缓存的ViewResolver)
AbstractCachingViewResolver是带有缓存的ViewResolver,它每次解析时先从缓存里查找,如果找到视图就返回,如果没有就创建新的视图,而创建新视图的方法由其子类实现,源码如下:
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
//判断是否有缓存数据,没有则创建新视图
if (!isCache()) {
return createView(viewName, locale);
} else {
// 获取View在缓存时的key值,不同的实现类不一样
Object cacheKey = getCacheKey(viewName, locale);
// ConcurrentHashMap用于缓存View
View view = this.viewAccessCache.get(cacheKey);
// 如果没有找到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);
// cacheUnresolved表示是否缓存没有解析成功的,默认为true
if (view == null && this.cacheUnresolved) {
// 重写View的render方法为空方法,使用没有渲染能力
view = UNRESOLVED_VIEW;
}
// 缓存View
if (view != null) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
if (logger.isTraceEnabled()) {
logger.trace("Cached view [" + cacheKey + "]");
}
}
}
}
}
// 返回View或null
return (view != UNRESOLVED_VIEW ? view : null);
}
}
1.1.1 ResourceBundleViewResolver
使用ResourceBundleViewResolver可以让视图解释器支持解析多种视图,它会默认读取classpath路径下的view.properties配置文件,也可通过basename或basenames属性指定一个或多个配置文件,使用方式如下:
视图解析器注册
<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<property name="basename" value="view"/>
</bean>
properties配置文件view.properties
#指定逻辑视图的视图类型
index.(class)=org.springframework.web.servlet.view.JstlView
#指定逻辑视图的url
index.url=/WEB-INF/views/success.jsp
Controller返回的逻辑视图“index”将会跳转到success.jsp页面。
1.1.2 XmlViewResolver
XmlViewResolver与ResourceBundleViewResolver的作用类似,默认加载/WEB-INF/views.xml
,也可以通过location属性指定。
视图解析器注册
<bean class="org.springframework.web.servlet.view.XmlViewResolver">
<property name="order" value="2"/>
<property name="location" value="WEB-INF/spring-view.xml"/>
</bean>
spring-view.xml:视图bean就是一个普通的Spring bean,在Spring bean配置文件中声明,通过XmlViewResolver加载
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
id : 逻辑视图名
class : 视图类型
url : 视图url位置
-->
<bean id="index"
class="org.springframework.web.servlet.view.JstlView">
<property name="url" value="/WEB-INF/views/index1.jsp" />
</bean>
</beans>
1.1.3 UrlBasedViewResolver及其子类
UrlBasedViewResolver 提供了拼接 URL 的方式来解析视图,通过 prefix 属性拼接一个前缀,通过 suffix 属性拼接一个后缀,就得到了视图的 URL。还可以加入 redirect: 与 forword: 前缀,使用 redirect: 前缀会调用 HttpServletResponse对象的 sendRedirect() 方法进行重定向,使用 forword: 前缀会利用 RequestDispatcher的forword 方式请求转发到指定的地址。另外,使用时还要指定 viewClass 属性,表示要解析成哪种 View,比如使用FreeMarkerViewResolver时需要指定属性:<property name="viewClass" value="org.springframework.web.servlet.view.freemarker.FreeMarkerView"/>
源码分析:
@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.
// 如果是redirect:开头的viewName,那么就采用RedirectView
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl, isRedirectContextRelative(), isRedirectHttp10Compatible());
view.setHosts(getRedirectHosts());
return applyLifecycleMethods(viewName, view);
}
// Check for special "forward:" prefix.
// 如果是forward:开头的viewName,直接返回InternalResourceView
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
return new InternalResourceView(forwardUrl);
}
// 如果不是redirect:和forward:开头就调用父类AbstractUrlBasedView的createView
//createView会继续调用UrlBasedViewResolver的loadView
return super.createView(viewName, locale);
}
@Override
protected View loadView(String viewName, Locale locale) throws Exception {
//构建一个AbstractUrlBasedView view
AbstractUrlBasedView view = buildView(viewName);
View result = applyLifecycleMethods(viewName, view);
//检查资源是否存在并返回
return (view.checkResource(locale) ? result : null);
}
/**
* UrlBasedViewResolver的子类重写了buildView也会先调用UrlBasedViewResolver的buildView
*/
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
//根据配置的ViewClass去创建一个`AbstractUrlBasedView`对象
AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
//以下会根据配置设置View的各种属性
//根据前缀和后缀去设置相对路径
view.setUrl(getPrefix() + viewName + getSuffix());
//设置View的ContentType
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();
// true all Views resolved by this resolver will expose path variables
// false - no Views resolved by this resolver will expose path variables
// null - individual Views can decide for themselves (this is used by the default)
if (exposeContextBeansAsAttributes != null) {
view.setExposeContextBeansAsAttributes(exposeContextBeansAsAttributes);
}
String[] exposedContextBeanNames = getExposedContextBeanNames();
if (exposedContextBeanNames != null) {
view.setExposedContextBeanNames(exposedContextBeanNames);
}
return view;
}
查看源码可以发现:UrlBasedViewResolver子类一些相似的特性:
- 添加了一些新的属性
- 重写了buildView:先调用UrlBasedViewResolver的buildView方法获取View对象,再给新属性赋值
- 重写了requiredViewClass:支持的View类型
- 默认构造器调用setViewClass()方法设置ViewClass属性值
常用的UrlBasedViewResolver | 支持的视图类型 |
---|---|
InternalResourceViewResolver | JstlView |
FreeMarkerViewResolver | FreeMarkerView |
GroovyMarkupViewResolver | GroovyMarkupView |
VelocityViewResolver | VelocityView |
VelocityLayoutViewResolver | VelocityLayoutView |
XsltViewResolver | XsltView |
注意事项:loadView方法返回return (view.checkResource(locale) ? result : null);
,其中view.checkResource(locale)
表示检查资源文件是否存在,当我们使用多个视图解析器且包含InternalResourceViewResolver时,我们应该将InternalResourceViewResolver的order设置为最大值,因为jstlView没有重写AbstractUrlBasedView的checkResource方法,总是返回true,导致后续的ViewResolver无法执行(可能是个bug)。
1.2 BeanNameViewResolver
BeanNameViewResolver 是通过视图名称去容器中获取对应的View对象,所以在使用前需要将View 对象注册到容器中,源码如下:
@Override
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = getApplicationContext();
//根据viewName去容器中查找View对象
if (!context.containsBean(viewName)) {
if (logger.isDebugEnabled()) {
logger.debug("No matching bean found for view name '" + viewName + "'");
}
// Allow for ViewResolver chaining...
return null;
}
if (!context.isTypeMatch(viewName, View.class)) {
if (logger.isDebugEnabled()) {
logger.debug("Found matching bean for view name '" + viewName +
"' - to be ignored since 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);
}
1.3 ContentNegotiatingViewResolver
ContentNegotiatingViewResolver本身不解析解析视图,而是用来整合所有的ViewResolver类,每次请求都会遍历所有的ViewResolver,然后找到最合适的处理View。源码如下:
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
//获取Request的MediaType集合
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
//通过遍历ViewResolver,获取所有符合条件的View
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
//遍历所有的SmartView,SmartView默认是RedirectView返回
//否则,根据MediaType最合适的第一个View返回
View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
if (bestView != null) {
return bestView;
}
}
if (this.useNotAcceptableStatusCode) {
if (logger.isDebugEnabled()) {
logger.debug("No acceptable view found; returning 406 (Not Acceptable) status code");
}
return NOT_ACCEPTABLE_VIEW;
}
else {
logger.debug("No acceptable view found; returning null");
return null;
}
}