ViewResolver作为SpringMVC的组件之一,当然还是要从DispatcherServlet#doDispatch来看这个组件作用是什么及组件是如何被调用的。在doDispatch方法里面执行Handler里的逻辑后会执行processDispatchResult对Handler处理返回的ModelAndView进行视图渲染,这个方法主要是会去调用render方法,这个方法的实现为:
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);
View view;
//这一步是关键,从ModelAndView中拿到viewName
String viewName = mv.getViewName();
if (viewName != null) { //返回了逻辑视图
// We need to resolve the view name.
//从这里调用了ViewResolver
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {//非逻辑视图
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}
// Delegate to the View object for rendering.
if (logger.isDebugEnabled()) {
logger.debug("Rendering view [" + view + "] in DispatcherServlet with name '" + getServletName() + "'");
}
try {
if (mv.getStatus() != null) {
response.setStatus(mv.getStatus().value());
}
//调用真实的视图来渲染视图
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "] in DispatcherServlet with name '" +
getServletName() + "'", ex);
}
throw ex;
}
}
需要理解的是视图View及视图解析器ViewResolver是两个不同的概念,ViewResolver是根据ViewName和Local去找到真正需要的View。DispatcherServlet会将Model里面的数据渲染到View中,这个View就是返回给用户的数据的展现形式。展现形式可以是多种多样的,可以是json格式,也可以是xml方式,也可以是jsp页面等。可以看到render会对modelAndView调用getViewName方法,如果得到viewName,说明需要将逻辑视图转换为真实的视图,这就是ViewResolver的作用。它的具体实现为:
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}
可以看到DispatcherServlet的resolveViewName其实就是遍历自己的viewResolvers属性,这个viewResolvers就是个泛型为ViewResolver的list,然后看其是否能够通过viewName和Locale来解析视图,可以的话就直接返回解析出的View。而这个变量的初始化其实也是在onRefresh方法中完成的,这个在之前也已经分析过了,这里就不再次展开了。而ViewResolver的定义为:
public interface ViewResolver {
View resolveViewName(String viewName, Locale locale) throws Exception;
}
它的继承体系为:
可以看出ViewResolver有四大继承类,
BeanNameViewResolver,将viewName当做beanName从容器中查找View.
ContentNegotiatingViewResolver,这个viewResolver根据request请求头的accept来选择合适的视图返回,可以用来实现RestFul。
ViewResolverComposite,这种XXXComposite的类就是XXX的一个容器,在处理请求是遍历自身存储的ViewResolver进行解析的。
AbstractCachingViewResolver,这个是所有可以缓存解析过的视图的基类,逻辑视图和视图的关系一般是不变的,所以不需要每次都重新解析,最好解析过一次就缓存起来。它的继承结构是:
public class BeanNameViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered {
private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered
public void setOrder(int order) {
this.order = order;
}
@Override
public int getOrder() {
return this.order;
}
@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws BeansException {
ApplicationContext context = obtainApplicationContext();
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);
}
}
可以看出BeanNameViewResolver是非常简单的,它的resolverViewName就是将参数viewName作beanName在容器中去查找,找不到或这找到的bean类型不对就直接返回了,否则就返回视图。
ContentNegotiatingViewResolver可以用于实现RestFul,即选择最合适的方式表述资源。它的实现原理是将视图解析的任务分派给它自己的成员变量viewResolvers,这可能会解析出多个视图,然后再获取request的Accept请求头,这也可能会有多个值,将这两个结构进行匹配,返回最优的视图给请求端,决定是否是匹配是交给ContentNegotiationManager去完成的。
刚才有谈到ContentNegotiatingViewResolver它自身是不会解析视图的,是分派给他的成员变量viewResolvers的,那viewResolvers是怎么初试化的呢?
protected void initServletContext(ServletContext servletContext) {
Collection<ViewResolver> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
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;
}
//配置的viewResolver没有在容器中,对它进行初始化,其实就是执行Bean的生命周期函数
String name = vr.getClass().getName() + i;
obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, 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);
}
viewResolvers的初始化逻辑是:先从容器中获取所有类型为ViewResolver的bean,然后根据viewResolvers是否已经被setViewResolvers方法设置过。如果没有设置过的话,则将所有查找到的VireResolver添加到viewResolvers中,当然还排除了它自身。如果已经设置过的话,则判断已经设置过的viewResolver是否在容器中,如果不在Spring容器中的话,就对其进行初始化。
现在关于ContentNegotiatingViewResolver的成员变量viewResolvers的初试化已经完成了,在去看它是如何让利用这个成员变量进行视图解析的:
public View resolveViewName(String viewName, Locale locale) throws Exception {
//这几步适用于获取request的media-type,也就是accpet表示的能够接受的数据表现方式
RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
if (requestedMediaTypes != null) {
//根据requestedMediaTypes获取到所有的候选view
List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
//从候选view中获取最合适的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;
}
}
可以看出,先获取request表明的客户端能够接收的MediaType,将其作为条件之一获取所有可以满足客户端需求的视图,然后再这些都能满足客户端需求的视图中选择最合适的一个,这个过程中调用了两个方法;
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
throws Exception {
//临时变量,用于存储所有备选的视图
List<View> candidateViews = new ArrayList<>();
if (this.viewResolvers != null) {
Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
//遍历成员变量viewResolvers,看其是否能够解析当前的viewName
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
//如果能够解析出来
if (view != null) {
//加入到备选视图中
candidateViews.add(view);
}
//将ViewName加上MediaType的后缀,看当前遍历到的视图解析器是否能够解析这个生成的新的viewName,如果可以的话将其加入到候选视图中
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;
}
getCandidateViews用于找到所有适合的视图,它的处理逻辑是遍历viewResolvers变量,看其是否能够解析传入的viewName或者viewName加上mediaType对应的后缀名,如果可以的话就将其加入候选视图中,当遍历完viewResolvers后,再将设置的默认视图一起放入到候选视图中,再返回给调用者。
private View getBestView(List<View> candidateViews, List<MediaType> requestedMediaTypes, RequestAttributes attrs) {
//用于判断候选的视图中有没有重定向视图,如果有的话就直接返回
for (View candidateView : candidateViews) {
//smartView是View的子类,用于判断这个视图是否会重定向
if (candidateView instanceof SmartView) {
SmartView smartView = (SmartView) candidateView;
if (smartView.isRedirectView()) {
if (logger.isDebugEnabled()) {
logger.debug("Returning redirect view [" + candidateView + "]");
}
return candidateView;
}
}
}
//可以看到查找过程是MediaType是优于View的,
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;
}
在刚才的步骤中已经得到了可以返回给用户的所有视图,现在就需要挑一个最合适的视图进行返回了,具体逻辑是:看候选View中是否有redirectView,如果有的话就直接返回,否者遍历从request中获取的media-type,对于客户端支持的每一个media-type都去遍历候选view,看view能支持的media-type,如果view支持的media-type和当前遍历到的media可以兼容的话,就返回当前view,并且将该media-type设置到request域中。到此,我们对ContentNegotiatingViewResolver的分析就完毕了,其实真要用RestFul的话更合适的方式是使用ResponseEntity。
几乎所有的XXXComposite类的作用就是存储一系列的XXX,然后遍历这些XXX来完成接口定义的功能,ViewResolverComposite也是样的。我们看看其源码
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;
}
可以看到其过程就是简单的调用其viewResolvers属性,返回第一个找到的view,如果找不到的话就返回一个null。至于这个属性就是通过调用Setter方法设置的。
AbstarctCachingViewResolver是ViewResolver里面最丰富的一个体系了,它提供了统一的缓存功能,使得视图被解析过一次就缓存起来(视图解析就是视图调用其render方法,将model,request,response里面的数据设置到View里合适的地方的一个过程),当视图被缓存起来后,就可以直接从缓存里面取视图了,不过既然是缓存,那必定会有设置缓存失效的方法了,稍后在看其是如何实现缓存的。AbstarctCachingViewResolver的直接子类有三个ResourceBundleViewResolver,XmlViewResolver和UrlBasedViewResolver。前两个分别通过配置properties和xml文件来解析视图的,至于UrlbasedViewResolver则是所有将逻辑视图(viewName)当做url去查找模版文件的ViewResolver的基类,最常见的就是InternalResourceViewResolver了。现在来看看AbstractCachingViewResolver是如何解析视图的:
public View resolveViewName(String viewName, Locale locale) throws Exception {
//判断是否开启了缓存功能,默认是开启了的,其容量为1024个视图
if (!isCache()) {
//如果没有开启的话,实际创建视图
return createView(viewName, locale);
}
else {
//获得key 这个值就是:viewName + '_' + locale;
Object cacheKey = getCacheKey(viewName, locale);
//从viewAccessCache中获取视图,这个属性是ConcurrentHashMap类型的
View view = this.viewAccessCache.get(cacheKey);
if (view == null) {
//viewCreationCache是LinkedHashMap类型的
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) {
//UNRESOLVED_VIEW 定义的一个Content-Type为空的view
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);
}
}
先判断是否开启了缓存功能,如果没有开启的话就创建一个视图,否则就从缓存中获取,但是从缓存中获取数据又分为命中和没有命中两种情况。命中的话就直接返回,没有命中的话就创建视图,如果创建成功返回并放到缓存中。可以看到其缓存实现是通过了两个Map,ConcurrentHashMap与LinkedHashMap,这里结合了这两种Map的优势,及ConcurretHashMap在并发上的优势与LinkedHashMap在实现LRU算法上的优势。创建View的代码为:
protected View createView(String viewName, Locale locale) throws Exception {
//loadView是一个模版方法
return loadView(viewName, locale);
}
可以看出AbstractCachingViewResolver的resolveViewName的做法概述为就是从缓存中获取,获取不到就创建视图,而创建视图的方式就交给了子类去实现,所以其子类的入口就是loadView方法了。
这两个类的loadView方法是一样的,
protected View loadView(String viewName, Locale locale) throws Exception {
BeanFactory factory = initFactory(locale);
try {
return factory.getBean(viewName, View.class);
}
catch (NoSuchBeanDefinitionException ex) {
// Allow for ViewResolver chaining...
return null;
}
}
可以看到这两个类都是先建立一个BeanFactory然后从这个factory里面去获取View的,他们的不同就在与这个BeanFactory的创建过程了,一个是读取properties文件,一个是读取XMl文件。
实现,所以其子类的入口就是loadView方法了。
UrlBasedViewResolver不仅重写了模版方法loadView,它还重写了父类的getCacheKey和createView方法。
父类的getCacheKey返回的是viewName+"_"+locale,而UrlBasedViewResolver重写后getCacheKey则直接返回viewName,因此可以知道UrlBasedViewResolver是不支持Locale的。
而它的createView方法是:
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.
//判断是否能够解析当前传入的viewName
//通过看viewName是否和属性viewNames里的某项匹配,如果viewNames没有配置,则说明支持所有的viewName
if (!canHandle(viewName, locale)) {
return null;
}
// Check for special "redirect:" prefix.
//判断是不是以“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);
}
// Check for special "forward:" prefix.
//判断是否是以“forward”开头
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.
//普通的逻辑视图名调用父类的createView方法,实际就是调用loadView方法
return super.createView(viewName, locale);
}
UrlBasedViewResolver的createView做的就是首先判断是否可以解析传入的逻辑视图,不可以的话返回null交由别的ViewResolver处理,可以的话就再检查逻辑视图名是不是以direct或者forward开头,如果是的话则返回相应的视图。如果不是的话就通过父类的createView去调用loadView方法。而loadView方法为:
protected View loadView(String viewName, Locale locale) throws Exception {
AbstractUrlBasedView view = buildView(viewName);
View result = applyLifecycleMethods(viewName, view);
return (view.checkResource(locale) ? result : null);
}
UrlBasedViewResolver的loadView方法就是用viewName当参数调用buildView创建了一个类型为AbstractUrlBasedView的View,然后对这个View进行了初始化操作,然后看这个View的url对于的模版是否存在,如果存在就将初始化的视图返回,否则返回null交给下一个ViewResolver来处理。
protected AbstractUrlBasedView buildView(String viewName) throws Exception {
//viewClass是UrlBasedViewResolver的一个属性 private Class> viewClass;
Class<?> viewClass = getViewClass();
Assert.state(viewClass != null, "No view class");
//通过反射的形式创建出一个View,类型为AbstractUrlBasedView的子类
AbstractUrlBasedView view = (AbstractUrlBasedView) BeanUtils.instantiateClass(viewClass);
//可以看到view的url被设置为前缀+name+后缀
view.setUrl(getPrefix() + viewName + getSuffix());
//接下来就是给view设置一系列的属性
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;
}
可以看到其创建过程就是根据属性viewClass利用反射的方式创建一个View,这个view是AbstarctUrlBasedView的子类,然后设置了view的url及其他属性。AbstractUrlBasedView的继承结构为:
可以看出UrlBasedViewResolver的功能已经很完善了,唯一需要做的就是配置其viewClasses属性。因此可以直接在容器中配置一个UrlBasedViewResolver,然后给他的viewClass配置为一个AbstractUrlBasedView的子类就可以使用了。但是实际一般也是直接使用UrlBasedViewResolver其子类,比如InternalResourceViewResolver,FreeMarkerViewResolver等。在此我们就看看用的最多的
InternalResourceViewResolver。刚才有说到UrlBasedViewResolver的关键在于配置其ViewClass属性,这是调用方法setViewClass来完成的:
public void setViewClass(@Nullable Class<?> viewClass) {
if (viewClass != null && !requiredViewClass().isAssignableFrom(viewClass)) {
throw new IllegalArgumentException("Given view class [" + viewClass.getName() +
"] is not of type [" + requiredViewClass().getName() + "]");
}
this.viewClass = viewClass;
}
很简单的一个Setter方法,只不过还调用了哈requiredViewClass().isAssignableFrom(viewClass)方法。而在InternalResourceViewResolver中是通过其构造函数调用的这个方法的:
public InternalResourceViewResolver() {
Class<?> viewClass = requiredViewClass();
if (InternalResourceView.class == viewClass && jstlPresent) {
viewClass = JstlView.class;
}
setViewClass(viewClass);
}
protected Class<?> requiredViewClass() {
return InternalResourceView.class;
}
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;
}
在这把InternalResourceViewResolver相关的代码贴出,可以看到其主要完成的工作是 1.重写requiredViewClass来将类型限制为自身能够解析的类型 2.通过setViewClass来将自身支持的view的类型设置到viewClass属性中,之后会根据这个类类型通过反射的方式生成一个view 3.在buildView中调用父类的buildView后再对view设置一些特有的属性。
基本所有UrlBasedViewResolver的子类都是这么一个过程,就不再展开了。
在这一篇笔记中记录了ViewResolver在DispatcherServlet中的初始化过程和调用流程,然后再细致的分析了ViewResolver类族。ViewResolver的作用就是根据逻辑视图找到视图,所谓的逻辑视图其实就是一个String类型的值,它代表的是视图的名字,也就是viewName,而这个viewName就是通过mv.getViewName()获得的,这里的mv就是HandlerAdapter执行handle函数后返回的处理结果。根据viewName查找view可以将viewName当做url去查找view,这是通过UrlBasedViewResolver去实现的,也可以将viewName当做beanName在容器中查找view,这是通过BeanNameViewResolver实现的。而ResourceBundleViewResolve和XmlViewResolver通过properties文件或xml文件来将viewName解析为view。而ContentNegotiatingViewResolver和ViewResolverComposite则将视图解析的任务交给其自身的属性viewResolvers来完成。