Spring MVC学习笔记之Spring MVC组件ViewResolver

1、ViewResolver简介

  ViewResolver,是视图解析器,它主要的作用是根据视图名和Locale解析出对应的视图。ViewResolver视图解析器的类图结构,如下所示:
在这里插入图片描述

  通过上面类图,我们知道ViewResolver家族中,直接实现ViewResolver接口的类一共有四个,其中三个都只有一个实现类,而AbstractCachingViewResolver类则是有一个庞大的分支。我们下面,分别分析四类视图解析器的实现。

ViewResolver接口

  ViewResolver接口非常简单,只定义了一个根据viewName和locale解析视图的方法resolveViewName()。在实际的实现类中,有些用到了本地化Locale实例参数,有些则是忽略了该参数,后面再具体分析。

public interface ViewResolver {
	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;
}
2、BeanNameViewResolver类

  BeanNameViewResolver是根据ViewName从ApplicationContext容器中查找相应的bean做View的实现类。具体实现如下:

public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
	//定义该视图解析器级别,order越小优先级越高
	private int order = Ordered.LOWEST_PRECEDENCE; 
	
	//省略order的 get/set方法

	//解析视图的方法实现,从当前容器中获取指定名称的解析器,如果没有对应的解析器,就交给其他解析器处理
	@Override
	@Nullable
	public View resolveViewName(String viewName, Locale locale) throws BeansException {
		ApplicationContext context = obtainApplicationContext();
		if (!context.containsBean(viewName)) {
			// Allow for ViewResolver chaining...
			return null;
		}
		if (!context.isTypeMatch(viewName, View.class)) {
			if (logger.isDebugEnabled()) {
				logger.debug("Found bean named '" + viewName + "' but 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);
	}
}
3、ViewResolverComposite类

  解析器是一个特殊的视图解析器,类似前面提到的HandlerMethodArgumentResolverComposite参数解析器,它不实际解析任何视图,而是将多个别的视图解析器包含在其中(即存储在viewResolvers属性中),解析时调用其所包含的视图解析器具体解析相应的视图。

  ViewResolverComposite视图解析器实现了InitializingBean、ApplicationContextAware、ServletContextAware 等接口,所以该视图解析器除了利用其他解析器实现视图解析功能,还提供了一些初始化操作。

  因为实现了ApplicationContextAware接口,所以初始化时,会为了viewResolvers集合中实现了ApplicationContextAware接口的解析器设置ApplicationContext。同理,也会为了实现ServletContextAware接口的解析器设置ServletContext。同时,因为实现了InitializingBean接口,所以在初始化Bean的时候,会调用viewResolvers集合中所有实现了该接口的解析器的afterPropertiesSet()方法进行初始化配置的调用。具体实现如下:

@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();
		}
	}
}

  实际的视图解析功能,主要交由viewResolvers集合中的视图解析器进行解析,具体实现如下:

@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;
}
4、ContentNegotiatingViewResolver类

  ContentNegotiatingViewResolver视图解析器主要是在别的解析器解析的结果上增加了对媒体类型MediaType和后缀的支持。大致流程如下:首先根据内置的viewResolvers集合进行视图解析,可能发现多个适合的视图,然后再根据媒体类型MediaType和后缀进行判断,选择最优的视图进行使用。

字段属性
  为了实现对媒体类型和后缀的支持,需要用到contentNegotiationManager和cnmFactoryBean两个属性,为了使用真正的视图解析器,定义了viewResolvers集合属性等。具体如下:

//判断媒体类型的工具类
@Nullable
private ContentNegotiationManager contentNegotiationManager;
//创建ContentNegotiationManager的工厂类
private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
//是否启用状态码406
private boolean useNotAcceptableStatusCode = false;
//默认的视图解析器
@Nullable
private List<View> defaultViews;
//视图解析器集合
@Nullable
private List<ViewResolver> viewResolvers;
//视图优先级定义
private int order = Ordered.HIGHEST_PRECEDENCE;

// get/set相关方法省略

属性初始化
  ContentNegotiatingViewResolver类实现了Ordered、 InitializingBean接口,同时继承了WebApplicationObjectSupport类(直接或间接的实现了ApplicationContextAware和ServletContextAware接口),所以该类就有了几个进行初始化的方法,如下所示:

  • initServletContext()初始化方法
    主要实现了视图解析器集合viewResolvers属性的初始化工作。
@Override
protected void initServletContext(ServletContext servletContext) {
	//查询当前容器,所有的ViewResolver类型的Bean
	Collection<ViewResolver> matchingBeans =
			BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
	//当viewResolvers为null时,把出来自身之外的其他ViewResolver类型的Bean都添加到属性viewResolvers中
	if (this.viewResolvers == null) {
		this.viewResolvers = new ArrayList<>(matchingBeans.size());
		for (ViewResolver viewResolver : matchingBeans) {
			if (this != viewResolver) {
				this.viewResolvers.add(viewResolver);
			}
		}
	}
	else {
		//把viewResolvers集合中,没有进行实例化的解析器,进行实例化操作
		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);
		}

	}
	//排序
	AnnotationAwareOrderComparator.sort(this.viewResolvers);
	//为cnmFactoryBean设置servletContext
	this.cnmFactoryBean.setServletContext(servletContext);
}
  • afterPropertiesSet()初始化方法
    主要完成contentNegotiationManager属性的初始化,由cnmFactoryBean实例创建。
@Override
public void afterPropertiesSet() {
	if (this.contentNegotiationManager == null) {
		this.contentNegotiationManager = this.cnmFactoryBean.build();
	}
	if (this.viewResolvers == null || this.viewResolvers.isEmpty()) {
		logger.warn("No ViewResolvers configured");
	}
}

视图解析

  在视图解析过程中,首先由视图解析器集合中的解析器去解析视图,这个时候可能产生多个满足要求的视图,然后再根据request获取包含的媒体信息,最后根据媒体信息和前面已经查询到的候选视图,然后由getBestView()方法去判断最合适的视图。

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
	RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
	Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
	//查询当前请求中的媒体类型,使用contentNegotiationManager工具类实现,不再具体分析
	List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
	if (requestedMediaTypes != null) {
		//查询候选视图,通过视图解析器集合实现,其中包括了对扩展名的处理,这里不再详细分析
		List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
		//判断最佳视图,并返回(不等于null时)
		View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
		if (bestView != null) {
			return bestView;
		}
	}
	//日志信息处理
	String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
			" given " + requestedMediaTypes.toString() : "";
	//如果启用状态码406,当视图为空时,返回NOT_ACCEPTABLE_VIEW实例
	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;
	}
}

  在视图解析过程中,如何判断最佳视图呢?我们来看一下具体的代码实现:

@Nullable
	private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
		//判断是否是RedirectView类型
		for (View candidateView : candidateViews) {
			if (candidateView instanceof SmartView) {
				SmartView smartView = (SmartView) candidateView;
				if (smartView.isRedirectView()) {
					return candidateView;
				}
			}
		}
		//当候选解析器中的ContentType类型与要求的MediaType匹配,则认为是最佳的解析器
		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("Selected '" + mediaType + "' given " + requestedMediaTypes);
						}
						attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST);
						return candidateView;
					}
				}
			}
		}
		return null;
	}
5、InternalResourceViewResolver类

  在AbstractCachingViewResolver系列视图解析器中,AbstractCachingViewResolver抽象类提供了统一的缓存功能,当视图解析过一次就被缓存起来。该抽象类有三个直接的实现类,分别是:ResourceBundleViewResolver、XmlViewResolver和UrlBasedViewResolver。其中,

  • ResourceBundleViewResolver,通过使用properties属性配置文件解析视图的;
  • XmlViewResolver,通过XML文件中的视图bean来解析“逻辑视图”,默认会从/WEB-INF/views.xml中加载视图bean;
  • UrlBasedViewResolver,基于URL查找模板文件的视图解析器基类,设置了统一的查找模板的规则。

  在AbstractCachingViewResolver系列视图解析器中,我们以InternalResourceViewResolver为例,分析视图解析器的实现方式。InternalResourceViewResolver类继承了UrlBasedViewResolver类,UrlBasedViewResolver类又继承了AbstractCachingViewResolver类,AbstractCachingViewResolver实现了视图解析器接口ViewResolver。

5.1、AbstractCachingViewResolver抽象类

  AbstractCachingViewResolver抽象类主要提供了统一的缓存功能,当视图解析过一次就被缓存起来,后续都会直接从缓存中查询视图,提高了视图的查询效率。

字段属性

//默认缓存的最大个数
public static final int DEFAULT_CACHE_LIMIT = 1024;
//不可解析视图的标志
private static final View UNRESOLVED_VIEW = new View() {
	@Override
	@Nullable
	public String getContentType() {
		return null;
	}
	@Override
	public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) {
	}
};

//是否缓存的过滤器接口的实现,对应内部接口CacheFilter
private static final CacheFilter DEFAULT_CACHE_FILTER = (view, viewName, locale) -> true;
//缓存数量限制,默认1024
private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
//未解析时,是不是限制再次解析
private boolean cacheUnresolved = true;
//是否缓存的默认实现类
private CacheFilter cacheFilter = DEFAULT_CACHE_FILTER;
//View视图缓存,使用ConcurrentHashMap避免全局锁,实现快速查询
private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<>(DEFAULT_CACHE_LIMIT);

//主要提供了限制缓存最大数的功能,同步创建视图,避免超过最大值。创建时,操作最大时,同时删除两个Map对象中的值
@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;
				}
			}
		};

视图解析
  实现接口中解析视图的方法,通过调用createView()方法,实现视图的创建。创建视图时,分两种情况,一种时启用缓存的,一种时不启用缓存的,具体实现如下:

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
	if (!isCache()) {//当不启用缓存时,即cacheLimit < 0时,直接创建视图
		return createView(viewName, locale);
	}
	else {//启用缓存
		//获取缓存的key,格式: viewName + '_' + locale
		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) {
					// 创建视图
					view = createView(viewName, locale);
					//当创建视图,结果为null时
					if (view == null && this.cacheUnresolved) {
						view = UNRESOLVED_VIEW;
					}
					//视图不为空时,过滤视图,并把符合要求的视图,放到viewAccessCache和viewCreationCache中
					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()方法中,又通过调用loadView()方法实现视图的创建和加载,而该方法是一个抽象方法,交由子类进行实现。

@Nullable
protected View createView(String viewName, Locale locale) throws Exception {
	return loadView(viewName, locale);
}

@Nullable
protected abstract View loadView(String viewName, Locale locale) throws Exception;
5.2、UrlBasedViewResolver类

  UrlBasedViewResolver类主要是一个实现了基于URL查找模板文件的视图解析器基类,设置了统一的查找模板的规则。在该类中,主要实现了createView()、loadView()、getCacheKey()方法,同时定义了一个buildView()方法。其中getCacheKey()方法是直接返回了viewName,去掉了locale的,因此这类视图解析器不支持本地化配置。代码如下:

@Override
protected Object getCacheKey(String viewName, Locale locale) {
	return viewName;
}

createView()方法
  createView()方法重新了父类中的方法,增加了统一的Redirect类型和FORWARD类型的处理,然后再交由父类的createView()方法处理。在父类中,又调用了loadView()方法,这里又调用了子类的loadView()方法。

@Override
protected View createView(String viewName, Locale locale) throws Exception {
	//
	if (!canHandle(viewName, locale)) {
		return null;
	}

	// 检查是否以“redirect:”开头,作为Redirect类型处理
	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:”开头,作为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 fall back to superclass implementation: calling loadView.
	return super.createView(viewName, locale);
}

loadView()方法
  通过调用父类的createView()方法,最终又调用了子类的loadView()方法,这个时候,就调用了重新定义的一个buildView()方法,来构建视图。

@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);
}

buildView()方法
  真正创建视图的方法,通过BeanUtils工具类,根据viewClass参数实例化视图,并为视图设置相关参数。具体实现如下:

protected AbstractUrlBasedView buildView(String viewName) throws Exception {
	//获取视图的class
	Class<?> viewClass = getViewClass();
	Assert.state(viewClass != null, "No view class");
	//获取视图实例
	AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
	//设置视图URL,包括前缀、后缀
	view.setUrl(getPrefix() + viewName + getSuffix());
	//设置参数
	view.setAttributesMap(getAttributesMap());
	//设置媒体类型
	String contentType = getContentType();
	if (contentType != null) {
		view.setContentType(contentType);
	}
	//设置ContextAttribute参数
	String requestContextAttribute = getRequestContextAttribute();
	if (requestContextAttribute != null) {
		view.setRequestContextAttribute(requestContextAttribute);
	}
	//设置PathVariables参数
	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.3、InternalResourceViewResolver类

  经过前面两个类的实现,到InternalResourceViewResolver类时,只需要在创建出来的视图的基础上设置了一些属性,并限制viewClass为InternalResourceView类型。其中,定义alwaysInclude属性用于标示是否在可以使用forward的情况下也强制使用include,默认为false。具体实现如下:

public class InternalResourceViewResolver extends UrlBasedViewResolver {

	private static final boolean jstlPresent = ClassUtils.isPresent(
			"javax.servlet.jsp.jstl.core.Config", InternalResourceViewResolver.class.getClassLoader());
	//用于标示是否在可以使用forward的情况下也强制使用include
	@Nullable
	private Boolean alwaysInclude;
	//设置ViewClass,默认为InternalResourceView或者JstlView
	public InternalResourceViewResolver() {
		Class<?> viewClass = requiredViewClass();
		if (InternalResourceView.class == viewClass && jstlPresent) {
			viewClass = JstlView.class;
		}
		setViewClass(viewClass);
	}
	//设置前缀和后缀
	public InternalResourceViewResolver(String prefix, String suffix) {
		this();
		setPrefix(prefix);
		setSuffix(suffix);
	}
	//解析视图类型为InternalResourceView
	@Override
	protected Class<?> requiredViewClass() {
		return InternalResourceView.class;
	}

	public void setAlwaysInclude(boolean alwaysInclude) {
		this.alwaysInclude = alwaysInclude;
	}
	//重写父类,设置alwaysInclude和preventDispatchLoop属性
	@Override
	protected AbstractUrlBasedView buildView(String viewName) throws Exception {
		InternalResourceView view = (InternalResourceView) super.buildView(viewName);
		if (this.alwaysInclude != null) {
			view.setAlwaysInclude(this.alwaysInclude);
		}
		view.setPreventDispatchLoop(true);
		return view;
	}
}
6、总结

  在这一节中,我们主要分析了视图解析器的相关内容。和视图解析器息息相关的就是视图,我们下一节将详细分析视图的相关内容,然后再回头来看视图解析器,这样就会y有一个更全面的理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

姠惢荇者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值