SpringMVC 解毒4
5 ViewResolver
ViewResolver 视图解析器能够通过viewName视图名和locale用户区域获取View对象,从而渲染视图。 如果你想使用自定义的一个模板引擎,就得配置上相对应的ViewResolver。
5.1 BeanNameViewResolver 分析
我们还是从最简单的讲起,给你一个最初的印象。BeanNameViewResolver 可谓是真的简单,它会根据viewName从web应用上下文中获取到实现View接口的bean并返回,web应用上下文从哪来?当然是继承相应的接口,被web应用上下文在初始化时注入。
直接看代码即可:
@Override
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = getApplicationContext();
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);
}
5.2 ViewResolverComposite 分析
凡是以Composite结尾的类,都是包含一组实现类,由其挑选合适的实现类并执行。ViewResolverComposite 也不例外。
代码如下:
@Override
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;
}
5.3 ContentNegotiatingViewResolver 分析
ContentNegotiatingViewResolver 比 ViewResolverComposite 高级一点,高级在可以通过 MediaType 来筛选出更加合适的View。
5.3.1 ContentNegotiatingViewResolver 初始化分析
ContentNegotiatingViewResolver 是一个带有初始化的类,在初始化中,它会把web应用上下文和父上下文中所有的 ViewResolver 实现类bean加载到自己的属性viewResolvers中,当然,开发者也可以在xml配置中向viewResolvers属性注入bean。同时 ContentNegotiatingViewResolver 还有一个 defaultViews属性,用于存放一组默认的View实现类bean,这个是需要xml配置的。
代码如下:
@Override
protected void initServletContext(ServletContext servletContext) {
Collection<ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(getApplicationContext(), ViewResolver.class).values();
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.size());
for (ViewResolver viewResolver : matchingBeans) {
if (this != viewResolver) {
this.viewResolvers.add(viewResolver);
}
}
}
else {
for (int i = 0; i < viewResolvers.size(); i++) {
if (matchingBeans.contains(viewResolvers.get(i))) {
continue;
}
String name = viewResolvers.get(i).getClass().getName() + i;
getApplicationContext().getAutowireCapableBeanFactory().initializeBean(viewResolvers.get(i), name);
}
}
if (this.viewResolvers.isEmpty()) {
logger.warn("Did not find any ViewResolvers to delegate to; please configure them using the " +
"'viewResolvers' property on the ContentNegotiatingViewResolver");
}
AnnotationAwareOrderComparator.sort(this.viewResolvers);
this.cnmFactoryBean.setServletContext(servletContext);
}
5.3.2 ContentNegotiatingViewResolver 处理请求分析
在前面,我们说了它会通过MediaType来筛选出更合适的View,怎么筛选的呢?来看代码吧
@Override
public View resolveViewName(String viewName, Locale locale) throws Exception {
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.isInstanceOf(ServletRequestAttributes.class, attrs);
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;
}
}
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;
}
}
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
throws Exception {
List<View> candidateViews = new ArrayList<View>();
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
candidateViews.add(view);
}
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);
}
}
}
}
if (!CollectionUtils.isEmpty(this.defaultViews)) {
candidateViews.addAll(this.defaultViews);
}
return candidateViews;
}
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
for (View candidateView : candidateViews) {
if (candidateView instanceof SmartView) {
SmartView smartView = (SmartView) candidateView;
if (smartView.isRedirectView()) {
if (logger.isDebugEnabled()) {
logger.debug("Returning redirect view [" + candidateView + "]");
}
return candidateView;
}
}
}
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("Returning [" + candidateView + "] based on requested media type '" +
mediaType + "'");
}
attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
return candidateView;
}
}
}
}
return null;
}
代码第5行首先会获取request可接受的并且服务器支持的MediaType,如果没有且配置该bean的时候属性useNotAcceptableStatusCode设置为true,会返回Http状态码为406的结果,否则,返回null,让别的resolver再去处理,属性useNotAcceptableStatusCode默认为false。
代码第7行会调用getCandidateViews方法获取所有返回的View。实际上,除了根据viewName获取View外,还会给viewName加上一些后缀名再去获取。最后,如果defaultViews如果不为空,还会获取defaultViews中的View。
代码第8行调用getBestView方法挑选出从第7行获取的所有View。如何挑选?首先判断有没有重定向,如果有重定向,则返回重定向的view,否则从每一个view中获取ContentType,如果ContentType可以满足request的需求,则返回该view。
5.4 AbstractCachingViewResolver 分析
以上三种 ViewResolver 实现类基本上都是从web应用上下文获取View 或者在调用别的 ViewResolver。而接下来的类则会是我们经常用到的。
AbstractCachingViewResolver 顾名思义,就是带缓存的 ViewResolver,它利用了LinkedHashMap中的一个方法,就是在添加K-V时,会判断要不要删除最早加入的K-V,所以这个类实现的是FIFO类型的缓存,默认缓存数量是1024个。
接下来简单看看代码吧
/** Default maximum number of entries for the view cache: 1024 */
public static final int DEFAULT_CACHE_LIMIT = 1024;
/** The maximum number of entries in the cache */
private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
/** Whether we should refrain from resolving views again if unresolved once */
private boolean cacheUnresolved = true;
/** Fast access cache for Views, returning already cached instances without a global lock */
private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<Object, View>(DEFAULT_CACHE_LIMIT);
/** Map from view key to View instance, synchronized for View creation */
@SuppressWarnings("serial")
private final Map<Object, View> viewCreationCache =
new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {
if (size() > getCacheLimit()) {
viewAccessCache.remove(eldest.getKey());
return true;
}
else {
return false;
}
}
};
@Override
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.viewAccessCache.put(cacheKey, view);
this.viewCreationCache.put(cacheKey, view);
if (logger.isTraceEnabled()) {
logger.trace("Cached view [" + cacheKey + "]");
}
}
}
}
}
return (view != UNRESOLVED_VIEW ? view : null);
}
}
/**
* Create the actual View object.
* <p>The default implementation delegates to {@link #loadView}.
* This can be overridden to resolve certain view names in a special fashion,
* before delegating to the actual {@code loadView} implementation
* provided by the subclass.
* @param viewName the name of the view to retrieve
* @param locale the Locale to retrieve the view for
* @return the View instance, or {@code null} if not found
* (optional, to allow for ViewResolver chaining)
* @throws Exception if the view couldn't be resolved
* @see #loadView
*/
protected View createView(String viewName, Locale locale) throws Exception {
return loadView(viewName, locale);
}
/**
* Subclasses must implement this method, building a View object
* for the specified view. The returned View objects will be
* cached by this ViewResolver base class.
* <p>Subclasses are not forced to support internationalization:
* A subclass that does not may simply ignore the locale parameter.
* @param viewName the name of the view to retrieve
* @param locale the Locale to retrieve the view for
* @return the View instance, or {@code null} if not found
* (optional, to allow for ViewResolver chaining)
* @throws Exception if the view couldn't be resolved
* @see #resolveViewName
*/
protected abstract View loadView(String viewName, Locale locale) throws Exception;
这个类留给子类的接口是loadView方法。
5.5 XmlViewResolver 分析
XmlViewResolver 是一个比较奇葩的类,实现的功能比较奇葩,他会从指定的xml配置文件中根据beanName来获取View。是不是和 BeanNameViewResolver 很像?不过他俩的区别是:XmlViewResolver 会再生成一个应用上下文单独加载一套xml配置文件,还需要维护自己的应用上下文的生命周期。BeanNameViewResolver 则是简单地直接从现有的应用上下文获取View。
直接来看类的源码吧
public class XmlViewResolver extends AbstractCachingViewResolver
implements Ordered, InitializingBean, DisposableBean {
/** Default if no other location is supplied */
public final static String DEFAULT_LOCATION = "/WEB-INF/views.xml";
private int order = Integer.MAX_VALUE; // default: same as non-Ordered
private Resource location;
private ConfigurableApplicationContext cachedFactory;
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
/**
* Set the location of the XML file that defines the view beans.
* <p>The default is "/WEB-INF/views.xml".
* @param location the location of the XML file.
*/
public void setLocation(Resource location) {
this.location = location;
}
/**
* Pre-initialize the factory from the XML file.
* Only effective if caching is enabled.
*/
@Override
public void afterPropertiesSet() throws BeansException {
if (isCache()) {
initFactory();
}
}
/**
* This implementation returns just the view name,
* as XmlViewResolver doesn't support localized resolution.
*/
@Override
protected Object getCacheKey(String viewName, Locale locale) {
return viewName;
}
@Override
protected View loadView(String viewName, Locale locale) throws BeansException {
BeanFactory factory = initFactory();
try {
return factory.getBean(viewName, View.class);
}
catch (NoSuchBeanDefinitionException ex) {
// Allow for ViewResolver chaining...
return null;
}
}
/**
* Initialize the view bean factory from the XML file.
* Synchronized because of access by parallel threads.
* @throws BeansException in case of initialization errors
*/
protected synchronized BeanFactory initFactory() throws BeansException {
if (this.cachedFactory != null) {
return this.cachedFactory;
}
Resource actualLocation = this.location;
if (actualLocation == null) {
actualLocation = getApplicationContext().getResource(DEFAULT_LOCATION);
}
// Create child ApplicationContext for views.
GenericWebApplicationContext factory = new GenericWebApplicationContext();
factory.setParent(getApplicationContext());
factory.setServletContext(getServletContext());
// Load XML resource with context-aware entity resolver.
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.setEnvironment(getApplicationContext().getEnvironment());
reader.setEntityResolver(new ResourceEntityResolver(getApplicationContext()));
reader.loadBeanDefinitions(actualLocation);
factory.refresh();
if (isCache()) {
this.cachedFactory = factory;
}
return factory;
}
/**
* Close the view bean factory on context shutdown.
*/
@Override
public void destroy() throws BeansException {
if (this.cachedFactory != null) {
this.cachedFactory.close();
}
}
}
5.6 ResourceBundleViewResolver 分析
ResourceBundleViewResolver 类允许用户指定一个属性文件,然后在属性文件中配置viewName和对应View实现类,以及viewName和对应的模板页面。
例如在xml这样配置
<bean class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
<!-- 设定属性文件名为views 默认也为views -->
<property name="basename" value="views"></property>
</bean>
接下来在resources文件夹下创建views.properties文件,配置上以下内容
a.class=org.springframework.web.servlet.view.JstlView
a.url=/WEB-INF/jsp/a.jsp
b.class=org.springframework.web.servlet.view.JstlView
b.url=/WEB-INF/jsp/b.jsp
即可实现viewName为a时使用JstlView,模板为/WEB-INF/jsp/a.jsp。
5.7 UrlBasedViewResolver 分析
AbstractCachingViewResolver 的上两个实现类实际上用的频率很少,用的比较多的就是 UrlBasedViewResolver 的子类,在这里,我们先看看 UrlBasedViewResolver 都做了些什么事。
首先,需要设置一个viewClass,而这个class必须继承 requiredViewClass 方法获取到的类,在这个类中,requiredViewClass 方法获取到的类是 AbstractUrlBasedView
代码如下:
/**
* Set the view class that should be used to create views.
* @param viewClass class that is assignable to the required view class
* (by default, AbstractUrlBasedView)
* @see AbstractUrlBasedView
*/
public void setViewClass(Class<?> viewClass) {
if (viewClass == null || !requiredViewClass().isAssignableFrom(viewClass)) {
throw new IllegalArgumentException(
"Given view class [" + (viewClass != null ? viewClass.getName() : null) +
"] is not of type [" + requiredViewClass().getName() + "]");
}
this.viewClass = viewClass;
}
/**
* Return the view class to be used to create views.
*/
protected Class<?> getViewClass() {
return this.viewClass;
}
/**
* Return the required type of view for this resolver.
* This implementation returns AbstractUrlBasedView.
* @see AbstractUrlBasedView
*/
protected Class<?> requiredViewClass() {
return AbstractUrlBasedView.class;
}
@Override
protected void initApplicationContext() {
super.initApplicationContext();
if (getViewClass() == null) {
throw new IllegalArgumentException("Property 'viewClass' is required");
}
}
其次,它还重写了 AbstractCachingViewResolver 类的createView方法,在方法中先判断是否有匹配的viewName,在判断是不是重定向或forward,最后再调用父类方法。
代码如下:
/**
* Overridden to implement check for "redirect:" prefix.
* <p>Not possible in {@code loadView}, since overridden
* {@code loadView} versions in subclasses might rely on the
* superclass always creating instances of the required view class.
* @see #loadView
* @see #requiredViewClass
*/
@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());
view.setHosts(getRedirectHosts());
return applyLifecycleMethods(viewName, view);
}
// Check for special "forward:" prefix.
if (viewName.startsWith(FORWARD_URL_PREFIX)) {
String forwardUrl = viewName.substring(FORWARD_URL_PREFIX.length());
return new InternalResourceView(forwardUrl);
}
// Else fall back to superclass implementation: calling loadView.
return super.createView(viewName, locale);
}
/**
* Indicates whether or not this {@link org.springframework.web.servlet.ViewResolver} can
* handle the supplied view name. If not, {@link #createView(String, java.util.Locale)} will
* return {@code null}. The default implementation checks against the configured
* {@link #setViewNames view names}.
* @param viewName the name of the view to retrieve
* @param locale the Locale to retrieve the view for
* @return whether this resolver applies to the specified view
* @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
*/
protected boolean canHandle(String viewName, Locale locale) {
String[] viewNames = getViewNames();
return (viewNames == null || PatternMatchUtils.simpleMatch(viewNames, viewName));
}
重定向和forward这里暂时不考虑,我们先看看正常的怎么调用。
UrlBasedViewResolver 允许设置prefix和suffix,使得开发者返回viewName时,只用返回特征值即可。例如文件在/WEB-INF/static/
下,文件结尾是.html
,那么设置prefix为/WEB-INF/static/
,suffix为.html
,如果想获取/WEB-INF/static/a.html
,开发者只用设置viewName为a即可。
UrlBasedViewResolver 重写了loadView方法,实现了根据文件名加载View的基本逻辑。
/**
* Delegates to {@code buildView} for creating a new instance of the
* specified view class, and applies the following Spring lifecycle methods
* (as supported by the generic Spring bean factory):
* <ul>
* <li>ApplicationContextAware's {@code setApplicationContext}
* <li>InitializingBean's {@code afterPropertiesSet}
* </ul>
* @param viewName the name of the view to retrieve
* @return the View instance
* @throws Exception if the view couldn't be resolved
* @see #buildView(String)
* @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);
}
private View applyLifecycleMethods(String viewName, AbstractView view) {
return (View) getApplicationContext().getAutowireCapableBeanFactory().initializeBean(view, viewName);
}
/**
* Creates a new View instance of the specified view class and configures it.
* Does <i>not</i> perform any lookup for pre-defined View instances.
* <p>Spring lifecycle methods as defined by the bean container do not have to
* be called here; those will be applied by the {@code loadView} method
* after this method returns.
* <p>Subclasses will typically call {@code super.buildView(viewName)}
* first, before setting further properties themselves. {@code loadView}
* will then apply Spring lifecycle methods at the end of this process.
* @param viewName the name of the view to build
* @return the View instance
* @throws Exception if the view couldn't be resolved
* @see #loadView(String, java.util.Locale)
*/
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(getViewClass());
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;
}
5.8 InternalResourceViewResolver 分析
InternalResourceViewResolver ?内部资源视图解析器?干什么的呢?实际上,这是一个利用jstl标准标签库实现模板渲染的视图解析器。只要类路径中存在javax.servlet.jsp.jstl.core.Config
类,说明jstl标准标签库可以使用。来看看它是如何设置viewClass的吧。
/**
* Sets the default {@link #setViewClass view class} to {@link #requiredViewClass}:
* by default {@link InternalResourceView}, or {@link JstlView} if the JSTL API
* is present.
*/
public InternalResourceViewResolver() {
Class<?> viewClass = requiredViewClass();
if (InternalResourceView.class == viewClass && jstlPresent) {
viewClass = JstlView.class;
}
setViewClass(viewClass);
}
/**
* A convenience constructor that allows for specifying {@link #setPrefix prefix}
* and {@link #setSuffix suffix} as constructor arguments.
* @param prefix the prefix that gets prepended to view names when building a URL
* @param suffix the suffix that gets appended to view names when building a URL
* @since 4.3
*/
public InternalResourceViewResolver(String prefix, String suffix) {
this();
setPrefix(prefix);
setSuffix(suffix);
}
/**
* This resolver requires {@link InternalResourceView}.
*/
@Override
protected Class<?> requiredViewClass() {
return InternalResourceView.class;
}
5.9 AbstractTemplateViewResolver 分析
使用过jsp当模板引擎后,很多人觉得可能有点复杂,那我要是想使用别的模板引擎呢? 对喽,这里提供了一个类供模板引擎使用。这个类的作用是什么?因为模板引擎实质上就是修改字符串,跟httpRequest没有关系,所以如果要使用模板引擎,就要判断request中的属性、session中的属性要不要给模板引擎提供。AbstractTemplateViewResolver 就实现了这样的功能,它对应的View是 AbstractTemplateView。
5.10 FreeMarkerViewResolver 分析
FreeMarkerViewResolver 继承了 AbstractTemplateViewResolver,实际上,FreeMarkerViewResolver提供了View的实现类 FreeMarkerView 供 UrlBasedViewResolver 创建View。
5.11 小结
看完这么多 ViewResolver 的实现,我们发现实际上通过viewName获取View实现类分为这几种。
- 第一种是直接从web应用上下文找name为viewName的bean。
- 第二种是从指定配置文件加载应用上下文,再在上下文中查找name为viewName的bean。
- 第三种是通过判断viewName对应的文件是否存在来决定是否适用该View。
6 View
第5章讲完了 ViewResolver,我们发现一个干事的 ViewResolver 后边对应的时一个或多个View实现类。因此,我们来了解一下View及其实现类吧。
6.1 AbstractView 分析
6.1.1 AbstractView 初始化分析
首先,AbstractView 有两个常量,分别是:
/** Default content type. Overridable as bean property. */
public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1";
/** Initial size for the temporary output byte array (if any) */
private static final int OUTPUT_BYTE_ARRAY_INITIAL_SIZE = 4096;
第一个常量DEFAULT_CONTENT_TYPE
用于填充默认的ContentType,第二个常量OUTPUT_BYTE_ARRAY_INITIAL_SIZE
是生成ByteArrayOutputStream的默认初始值。
其次,AbstractView 实现了BeanNameAware接口,可以得到自身的name。
6.1.2 AbstractView 处理请求分析
AbstractView 重写了render方法,并将代码分为三步完成请求。
/**
* Prepares the view given the specified model, merging it with static
* attributes and a RequestContext attribute, if necessary.
* Delegates to renderMergedOutputModel for the actual rendering.
* @see #renderMergedOutputModel
*/
@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (logger.isTraceEnabled()) {
logger.trace("Rendering view with name '" + this.beanName + "' with model " + model +
" and static attributes " + this.staticAttributes);
}
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}
/**
* Creates a combined output Map (never {@code null}) that includes dynamic values and static attributes.
* Dynamic values take precedence over static attributes.
*/
protected Map<String, Object> createMergedOutputModel(Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) {
@SuppressWarnings("unchecked")
Map<String, Object> pathVars = (this.exposePathVariables ?
(Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null);
// Consolidate static and dynamic model attributes.
int size = this.staticAttributes.size();
size += (model != null ? model.size() : 0);
size += (pathVars != null ? pathVars.size() : 0);
Map<String, Object> mergedModel = new LinkedHashMap<String, Object>(size);
mergedModel.putAll(this.staticAttributes);
if (pathVars != null) {
mergedModel.putAll(pathVars);
}
if (model != null) {
mergedModel.putAll(model);
}
// Expose RequestContext?
if (this.requestContextAttribute != null) {
mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
}
return mergedModel;
}
/**
* Prepare the given response for rendering.
* <p>The default implementation applies a workaround for an IE bug
* when sending download content via HTTPS.
* @param request current HTTP request
* @param response current HTTP response
*/
protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
if (generatesDownloadContent()) {
response.setHeader("Pragma", "private");
response.setHeader("Cache-Control", "private, must-revalidate");
}
}
/**
* Subclasses must implement this method to actually render the view.
* <p>The first step will be preparing the request: In the JSP case,
* this would mean setting model objects as request attributes.
* The second step will be the actual rendering of the view,
* for example including the JSP via a RequestDispatcher.
* @param model combined output Map (never {@code null}),
* with dynamic values taking precedence over static attributes
* @param request current HTTP request
* @param response current HTTP response
* @throws Exception if rendering failed
*/
protected abstract void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
首先第一步是生成一个最终的model,在 AbstractView 中,会将其Map类型的属性 staticAttributes 合并到传入的model中。如果允许暴露 RequestContext,则将 RequestContext传入。
其次是准备响应,这里一般不会运行什么结果。
最后是将合并后的model交给子类渲染。
6.1.3 AbstractView 其他
除以上处理请求方法外,AbstractView 还提供了一些设置请求头、把ByteArrayOutputStream写入response等等供子类调用的方法,方便子类使用。
6.2 AbstractUrlBasedView 分析
AbstractUrlBasedView 是最简单的View之一,它提供了一个url属性。还有个checkResource方法,这个方法就是在 UrlBasedViewResolver 类中loadView方法内最终检测文件是否存在的方法。
6.3 InternalResourceView 和 JstlView
我们在5.8节讲到 InternalResourceViewResolver 使用的View就是InternalResourceView和JstlView。InternalResourceView 能够渲染jsp页面,代码如下:
/**
* Render the internal resource given the specified model.
* This includes setting the model as request attributes.
*/
@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose the model object as request attributes.
exposeModelAsRequestAttributes(model, request);
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
if (rd == null) {
throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
"]: Check that the corresponding file exists within your web application archive!");
}
// If already included or response already committed, perform include, else forward.
if (useInclude(request, response)) {
response.setContentType(getContentType());
if (logger.isDebugEnabled()) {
logger.debug("Including resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.include(request, response);
}
else {
// Note: The forwarded resource is supposed to determine the content type itself.
if (logger.isDebugEnabled()) {
logger.debug("Forwarding to resource [" + getUrl() + "] in InternalResourceView '" + getBeanName() + "'");
}
rd.forward(request, response);
}
}
如上代码,首先将model中的K-V值放到了request的属性中,接下来调用RequestDispatcher的include或forward方法执行渲染,这里就不做细究了。
如果项目中引入了JSTL标准标签库,则会使用 JstlView,引入一些JSTL需要使用的数据。
6.4 RedirectView 分析
由于在 UrlBasedViewResolver 中重写了 createView方法,判断如果viewName是以redirect开头的,就会重定向到别的页面,使用的就是 RedirectView。
在redirect时,会将相关数据保存在 FlashMap中,这个咱们在DispatcherServlet
讲过,用于保存重定向前相关数据。保存好相关数据后,再向response中写入Header信息,K为Location,V为跳转的URL。
相关方法如下:
/**
* Convert model to request parameters and redirect to the given URL.
* @see #appendQueryProperties
* @see #sendRedirect
*/
@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
HttpServletResponse response) throws IOException {
String targetUrl = createTargetUrl(model, request);
targetUrl = updateTargetUrl(targetUrl, model, request, response);
FlashMap flashMap = RequestContextUtils.getOutputFlashMap(request);
if (!CollectionUtils.isEmpty(flashMap)) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(targetUrl).build();
flashMap.setTargetRequestPath(uriComponents.getPath());
flashMap.addTargetRequestParams(uriComponents.getQueryParams());
FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request);
if (flashMapManager == null) {
throw new IllegalStateException("FlashMapManager not found despite output FlashMap having been set");
}
flashMapManager.saveOutputFlashMap(flashMap, request, response);
}
sendRedirect(request, response, targetUrl, this.http10Compatible);
}
/**
* Send a redirect back to the HTTP client
* @param request current HTTP request (allows for reacting to request method)
* @param response current HTTP response (for sending response headers)
* @param targetUrl the target URL to redirect to
* @param http10Compatible whether to stay compatible with HTTP 1.0 clients
* @throws IOException if thrown by response methods
*/
protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
String targetUrl, boolean http10Compatible) throws IOException {
String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : response.encodeRedirectURL(targetUrl));
if (http10Compatible) {
HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE);
if (this.statusCode != null) {
response.setStatus(this.statusCode.value());
response.setHeader("Location", encodedURL);
}
else if (attributeStatusCode != null) {
response.setStatus(attributeStatusCode.value());
response.setHeader("Location", encodedURL);
}
else {
// Send status code 302 by default.
response.sendRedirect(encodedURL);
}
}
else {
HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
response.setStatus(statusCode.value());
response.setHeader("Location", encodedURL);
}
}
6.5 AbstractTemplateView 分析
AbstractTemplateView 是在 AbstractTemplateViewResolver 中使用的,抽象模板视图的作用是决定要不要将HTTPrequest中的相关信息写入model。还记得6.3节InternalResourceView和JstlView吗?在那些View中,是需要将model写入到request的属性中。而模板引擎在非web程序中也能使用,和request一点关系都没有,所以,为了能让模板引擎能够拿到request中的数据,设置了该抽象类,用于决定要不要将request中的信息写入到model中。
以下是相关属性:
/**
* Variable name of the RequestContext instance in the template model,
* available to Spring's macros: e.g. for creating BindStatus objects.
*/
public static final String SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE = "springMacroRequestContext";
// 是否将request属性中的信息写入到model
private boolean exposeRequestAttributes = false;
// 是否允许request的信息覆盖model的
private boolean allowRequestOverride = false;
// 是否将session中的信息写入到model
private boolean exposeSessionAttributes = false;
// 是否允许session的信息覆盖model的
private boolean allowSessionOverride = false;
// 是否暴露spring上下文
private boolean exposeSpringMacroHelpers = true;
了解了这些信息,来看看相关代码吧
@Override
protected final void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if (this.exposeRequestAttributes) {
for (Enumeration<String> en = request.getAttributeNames(); en.hasMoreElements();) {
String attribute = en.nextElement();
if (model.containsKey(attribute) && !this.allowRequestOverride) {
throw new ServletException("Cannot expose request attribute '" + attribute +
"' because of an existing model object of the same name");
}
Object attributeValue = request.getAttribute(attribute);
if (logger.isDebugEnabled()) {
logger.debug("Exposing request attribute '" + attribute +
"' with value [" + attributeValue + "] to model");
}
model.put(attribute, attributeValue);
}
}
if (this.exposeSessionAttributes) {
HttpSession session = request.getSession(false);
if (session != null) {
for (Enumeration<String> en = session.getAttributeNames(); en.hasMoreElements();) {
String attribute = en.nextElement();
if (model.containsKey(attribute) && !this.allowSessionOverride) {
throw new ServletException("Cannot expose session attribute '" + attribute +
"' because of an existing model object of the same name");
}
Object attributeValue = session.getAttribute(attribute);
if (logger.isDebugEnabled()) {
logger.debug("Exposing session attribute '" + attribute +
"' with value [" + attributeValue + "] to model");
}
model.put(attribute, attributeValue);
}
}
}
if (this.exposeSpringMacroHelpers) {
if (model.containsKey(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE)) {
throw new ServletException(
"Cannot expose bind macro helper '" + SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE +
"' because of an existing model object of the same name");
}
// Expose RequestContext instance for Spring macros.
model.put(SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE,
new RequestContext(request, response, getServletContext(), model));
}
applyContentType(response);
renderMergedTemplateModel(model, request, response);
}
/**
* Apply this view's content type as specified in the "contentType"
* bean property to the given response.
* <p>Only applies the view's contentType if no content type has been
* set on the response before. This allows handlers to override the
* default content type beforehand.
* @param response current HTTP response
* @see #setContentType
*/
protected void applyContentType(HttpServletResponse response) {
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
}
/**
* Subclasses must implement this method to actually render the view.
* @param model combined output Map, with request attributes and
* session attributes merged into it if required
* @param request current HTTP request
* @param response current HTTP response
* @throws Exception if rendering failed
*/
protected abstract void renderMergedTemplateModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;
做完了这些数据整合后,把渲染视图的任务再次交给子类。
6.6 FreeMarkerView 分析
我们在开发中一般会使用的视图解析器有各种模板引擎或者jsp等等,在 AbstractUrlBasedView 和 AbstractTemplateView中,分别处理好了url和model,接下来就该真正的视图工作了。这里我们试着分析一下 FreeMarkerView,其他的模板引擎工作原理也差不多。
6.6.1 FreeMarkerView 初始化分析
FreeMarkerView 初始化会从web应用上下文中加载相关配置。
/**
* Invoked on startup. Looks for a single FreeMarkerConfig bean to
* find the relevant Configuration for this factory.
* <p>Checks that the template for the default Locale can be found:
* FreeMarker will check non-Locale-specific templates if a
* locale-specific one is not found.
* @see freemarker.cache.TemplateCache#getTemplate
*/
@Override
protected void initServletContext(ServletContext servletContext) throws BeansException {
if (getConfiguration() != null) {
this.taglibFactory = new TaglibFactory(servletContext);
}
else {
FreeMarkerConfig config = autodetectConfiguration();
setConfiguration(config.getConfiguration());
this.taglibFactory = config.getTaglibFactory();
}
GenericServlet servlet = new GenericServletAdapter();
try {
servlet.init(new DelegatingServletConfig());
}
catch (ServletException ex) {
throw new BeanInitializationException("Initialization of GenericServlet adapter failed", ex);
}
this.servletContextHashModel = new ServletContextHashModel(servlet, getObjectWrapper());
}
/**
* Autodetect a {@link FreeMarkerConfig} object via the ApplicationContext.
* @return the Configuration instance to use for FreeMarkerViews
* @throws BeansException if no Configuration instance could be found
* @see #getApplicationContext
* @see #setConfiguration
*/
protected FreeMarkerConfig autodetectConfiguration() throws BeansException {
try {
return BeanFactoryUtils.beanOfTypeIncludingAncestors(
getApplicationContext(), FreeMarkerConfig.class, true, false);
}
catch (NoSuchBeanDefinitionException ex) {
throw new ApplicationContextException(
"Must define a single FreeMarkerConfig bean in this web application context " +
"(may be inherited): FreeMarkerConfigurer is the usual implementation. " +
"This bean may be given any name.", ex);
}
}
这个 FreeMarkerConfig 类就是需要我们在xml中配置的FreeMarker配置对象,其中可能有模板路径,文件字符格式等等。
6.6.2 FreeMarkerView 处理请求分析
之前讲到 UrlBasedViewResolver 在loadView方法中会调用 AbstractUrlBasedView 的checkResource方法判断资源是否存在。
/**
* Check that the FreeMarker template used for this view exists and is valid.
* <p>Can be overridden to customize the behavior, for example in case of
* multiple templates to be rendered into a single view.
*/
@Override
public boolean checkResource(Locale locale) throws Exception {
try {
// Check that we can get the template, even if we might subsequently get it again.
getTemplate(getUrl(), locale);
return true;
}
catch (FileNotFoundException ex) {
if (logger.isDebugEnabled()) {
logger.debug("No FreeMarker view found for URL: " + getUrl());
}
return false;
}
catch (ParseException ex) {
throw new ApplicationContextException(
"Failed to parse FreeMarker template for URL [" + getUrl() + "]", ex);
}
catch (IOException ex) {
throw new ApplicationContextException(
"Could not load FreeMarker template for URL [" + getUrl() + "]", ex);
}
}
而最终的实现就是判断是否能获得相关模板文件,如果能获得就返回true。
当确认能够渲染后,最终会调用renderMergedTemplateModel方法进行最终的渲染,该方法又会调用doRender方法执行真正的渲染,
/**
* Process the model map by merging it with the FreeMarker template.
* Output is directed to the servlet response.
* <p>This method can be overridden if custom behavior is needed.
*/
@Override
protected void renderMergedTemplateModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
exposeHelpers(model, request);
doRender(model, request, response);
}
/**
* Render the FreeMarker view to the given response, using the given model
* map which contains the complete template model to use.
* <p>The default implementation renders the template specified by the "url"
* bean property, retrieved via {@code getTemplate}. It delegates to the
* {@code processTemplate} method to merge the template instance with
* the given template model.
* <p>Adds the standard Freemarker hash models to the model: request parameters,
* request, session and application (ServletContext), as well as the JSP tag
* library hash model.
* <p>Can be overridden to customize the behavior, for example to render
* multiple templates into a single view.
* @param model the model to use for rendering
* @param request current HTTP request
* @param response current servlet response
* @throws IOException if the template file could not be retrieved
* @throws Exception if rendering failed
* @see #setUrl
* @see org.springframework.web.servlet.support.RequestContextUtils#getLocale
* @see #getTemplate(java.util.Locale)
* @see #processTemplate
* @see freemarker.ext.servlet.FreemarkerServlet
*/
protected void doRender(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Expose model to JSP tags (as request attributes).
exposeModelAsRequestAttributes(model, request);
// Expose all standard FreeMarker hash models.
SimpleHash fmModel = buildTemplateModel(model, request, response);
if (logger.isDebugEnabled()) {
logger.debug("Rendering FreeMarker template [" + getUrl() + "] in FreeMarkerView '" + getBeanName() + "'");
}
// Grab the locale-specific version of the template.
Locale locale = RequestContextUtils.getLocale(request);
processTemplate(getTemplate(locale), fmModel, response);
}
/**
* Process the FreeMarker template to the servlet response.
* <p>Can be overridden to customize the behavior.
* @param template the template to process
* @param model the model for the template
* @param response servlet response (use this to get the OutputStream or Writer)
* @throws IOException if the template file could not be retrieved
* @throws TemplateException if thrown by FreeMarker
* @see freemarker.template.Template#process(Object, java.io.Writer)
*/
protected void processTemplate(Template template, SimpleHash model, HttpServletResponse response)
throws IOException, TemplateException {
template.process(model, response.getWriter());
}
6.7 小结
View接口相比较而言还算简单,但其中门门还真不少,谁能想到 ViewResolver 竟然需要调用 AbstractUrlBasedView 的checkResource方法判断这个View是否能渲染数据。AbstractUrlBasedView 使得View的获取从通过beanName的方式转换为通过View自身的判断,真是一个神转折。