基本概念
所有的 MVC 框架都有一套它自己的解析视图的机制,SpringMVC 也不例外。
它使用 ViewResolver 进行视图解析,让用户在浏览器中渲染模型。
ViewResolver 接口在视图名称和真正的视图之间提供映射,它是一种开箱即用的技术,能够解析 JSP、Velocity 模板和 XSLT 等多种视图:
视图解析器在 SpringMVC 中配置如下(以 InternalResourceViewResolver 为例 ):
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />
<property name="prefix" value="/WEB-INF/page/" />
<property name="suffix" value=".jsp" />
</bean>
bean:表示指定的视图解析器
viewClass: 表示要解析的视图类型
prefix/suffix: 表示路径前缀/后缀,假设 ViewName 为 hello,则完整路径为 /WEB-INF/page/hello.jsp
内部构造
ViewResolver ,即视图解析器,该接口的主要作用就是根据视图名 (ViewName),返回视图 (View)。下面来它的源码:
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
再它的继承关系:
AbstractCachingViewResolver
该类是 ViewResolver 接口的抽象实现类,用来表示一个带有缓存功能的视图解析器。
下面来看它的 resolveViewName:
// 成员变量
private boolean cacheUnresolved = true;
public View resolveViewName(String viewName, Locale locale) throws Exception {
// 1.判断缓存是否打开
if (!isCache()) {
// 2.缓存关闭,则创建视图
return createView(viewName, locale);
} else {
// 3.1.缓存开启,从 viewAccessCache 取出视图
Object cacheKey = getCacheKey(viewName, locale);
View view = this.viewAccessCache.get(cacheKey);
// 3.2.为空,从 viewCreationCache 取出视图
if (view == null) {
synchronized (this.viewCreationCache) {
// 3.3.仍然为空,则创建视图
view = this.viewCreationCache.get(cacheKey);
if (view == null) {
view = createView(viewName, locale);
// 3.3.1.仍然为空,标记为[不可解析视图]
if (view == null && this.cacheUnresolved) {
view = UNRESOLVED_VIEW;
}
// 3.3.2.不为空,添加进缓存
if (view != null) {
this.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
// 省略部分代码...
}
}
}
}
return ( view != UNRESOLVED_VIEW ? view : null );
}
}
再来看看它的 createView 方法,该方法负责视图的创建。
protected View createView(String viewName, Locale locale) throws Exception {
// 空方法
return loadView(viewName, locale);
}
观察代码,可以该类的视图解析过程如下:
缓存关闭,则创建视图。
缓存开启,则步骤如下:
- 依次从 viewCreationCache,viewCreationCache 缓存中查找。
- 存在,则返回视图;不存在,则创建视图。
- 创建视图完再次判断,为空则标记为不可解析视图,否则添加到两个缓存。
UrlBasedViewResolver
继承自 AbstractCachingViewResolver 抽象类、并实现 Ordered 接口的类,是 ViewResolver 接口简单的实现类,该类重写了 createView、loadView 这两个方法。
首先来看它的签名:
public class UrlBasedViewResolver extends AbstractCachingViewResolver implements Ordered
1.createView
public static final String REDIRECT_URL_PREFIX = "redirect:";
public static final String FORWARD_URL_PREFIX = "forward:";
protected View createView(String viewName, Locale locale) throws Exception {
// 是否支持该视图的名的处理
if (!canHandle(viewName, locale)) {
return null;
}
// 判断视图名是否以 redirect: 开头
if (viewName.startsWith(REDIRECT_URL_PREFIX)) {
// 获取真正的视图名
String redirectUrl = viewName.substring(REDIRECT_URL_PREFIX.length());
RedirectView view = new RedirectView(redirectUrl,
isRedirectContextRelative(), isRedirectHttp10Compatible());
return applyLifecycleMethods(viewName, view);
}
// 判断视图名是否以 forward: 开头
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
// 返回视图
return new InternalResourceView(forwardUrl);
}
// 其他情况由父类处理,其实就是调用该类的 loadView 方法
return super.createView(viewName, locale);
}
再来看看 applyLifecycleMethods 方法:
private View applyLifecycleMethods(String viewName, AbstractView view) {
// 从 SpringMVC 容器中手动获取 Bean
return (View) getApplicationContext().getAutowireCapableBeanFactory().
initializeBean(view, viewName);
}
2.loadView
再来看看它 的 loadView 方法,如果视图名不包含 redirect,forward 时,调用父类的 createView 的时会调用到它。
protected View loadView(String viewName, Locale locale) throws Exception {
// 创建 View
AbstractUrlBasedView view = buildView(viewName);
// 从 SpringMVC 容器中获取 View
View result = applyLifecycleMethods(viewName, view);
return ( view.checkResource(locale) ? result : null );
}
再来看看 buildView 方法:
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
// 1.创建 View ,利用反射实例类
AbstractUrlBasedView view =
(AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
// 2.设置 View 的 url
view.setUrl(getPrefix() + viewName + getSuffix());
// 3.设置 View 的 contentType
String contentType = getContentType();
if (contentType != null) {
view.setContentType(contentType);
}
// 4.设置 View 的其他相关属性
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;
}
实例探究
利用 ContentNegotiatingViewResolver 集成多种视图
- 首先来看配置文件(这里集成 vm,jsp,json,xml 这四种视图显示)
<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<!-- 内容协商管理器 -->
<property name="contentNegotiationManager" ref="contentNegotiationManager"/>
<!-- 视图解析器 -->
<property name="viewResolvers">
<list>
<!--Veocity 视图解析器 -->
<bean class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
<property name="order" value="0"/>
<property name="cache" value="false" />
<property name="suffix" value=".vm" />
</bean>
<!--JSP 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/page/"/>
<property name="suffix" value=".jsp"></property>
</bean>
</list>
</property>
<!-- 默认视图 -->
<property name="defaultViews">
<list>
<!-- json 视图解析 -->
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" />
<!-- xml 视图解析 -->
<bean class="org.springframework.web.servlet.view.xml.MarshallingView" >
<property name="marshaller">
<bean class="org.springframework.oxm.xstream.XStreamMarshaller"/>
</property>
</bean>
</list>
</property>
</bean>
<bean id="contentNegotiationManager" class="org.springframework.web.accept.ContentNegotiationManagerFactoryBean">
<!-- 根据请求参数或拓展名映射到相应的MIME类型 -->
<property name="mediaTypes">
<map>
<entry key="json" value="application/json"/>
<entry key="xml" value="application/xml"/>
</map>
</property>
<!-- 设置默认的MIME类型,如果没有指定拓展名或请求参数,则使用此默认MIME类型解析视图 -->
<property name="defaultContentType" value="text/html"/>
<!-- 是否不适用请求头确定MIME类型 -->
<property name="ignoreAcceptHeader" value="true"/>
<!-- 是否根据路径拓展名确定MIME类型 -->
<property name="favorPathExtension" value="true"/>
<!-- 是否使用参数来确定MIME类型 -->
<property name="favorParameter" value="false" />
</bean>
<!-- Veocity 模版配置 -->
<bean class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
<property name="configLocation" value="/WEB-INF/velocity.properties" />
</bean>
- Velocity 属性文件配置
input.encoding=UTF-8
output.encoding=UTF-8
contentType=text/html;charset=UTF-8
resource.loader=webapp
webapp.resource.loader.class=org.apache.velocity.tools.view.WebappResourceLoader
webapp.resource.loader.path=/WEB-INF/velocity/
- 测试
// 先用 velocity 视图解析(order = 0),然后再用 jsp 视图解析
http://localhost:8080/demo/hello
// 返回 json 视图
http://localhost:8080/demo/hello.json
// 返回 xml 视图
http://localhost:8080/demo/hello.xml
注意:
这里利用路径扩展名(favorPathExtension)区分 MIME 类型,以此确定要返回的视图类型。也可以利用参数(默认为 fomart)来区分。
VelocityViewResolver(velocity 视图解析)的 order 优先级必须比 InternalResourceViewResolver(jsp 视图解析器)高。因为 InternalResourceViewResolver 不管路径下是否存在指定 jsp 文件都会返回 View(视图),这样会导致 VelocityViewResolver 不生效。