SpringMVC 中的九大组件后面曾经和大家分享了好几个了,明天咱们来持续视图解析器的剖析。

对于视图解析器,松哥其实在之前的文章中有和大家分享过,那一次是为了解决多个视图共存的问题,如果小伙伴们还没看过那篇文章,能够先看看:

  • SpringMVC 中如何同时存在多个视图解析器

ViewResolver 其实就是咱们心心念念的视图解析器,用过 SpringMVC 的小伙伴都晓得 SpringMVC 中有一个视图解析器,明天咱们就来剖析一下这个视图解析器到底是怎么工作的。

1.概览

首先咱们来大略看一下 ViewResolver 接口是什么样子的:

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

这个接口中只有一个办法,能够看到,非常简单,就是通过视图名和 Locale,找到对应的 View 返回即可。

如图间接继承自 ViewResolver 接口的类有四个,作用如下:

  • ContentNegotiatingViewResolver:反对 MediaType 和后缀的视图解析器。
  • BeanNameViewResolver:这个是间接依据视图名去 Spring 容器中查找相应的 Bean 并返回。
  • AbstractCachingViewResolver:具备缓存性能的视图解析器。
  • ViewResolverComposite:这是一个组合的视图解析器,届时能够用来代理其余具体干活的视图解析器。

接下来咱们就对这四个视图解析器逐个进行介绍,先从最简略的 BeanNameViewResolver 开始吧。

2.BeanNameViewResolver

BeanNameViewResolver 的解决形式非常简单粗犷,间接依据 viewName 去 Spring 容器中查找相应的 Bean 并返回,如下:

@Override@Nullablepublic View resolveViewName(String viewName, Locale locale) throws BeansException {    ApplicationContext context = obtainApplicationContext();    if (!context.containsBean(viewName)) {        return null;    }    if (!context.isTypeMatch(viewName, View.class)) {        return null;    }    return context.getBean(viewName, View.class);}

先去判断下有没有相应的 Bean,而后再查看下 Bean 的类型对不对,都没问题,间接查找返回即可。

3.ContentNegotiatingViewResolver

ContentNegotiatingViewResolver 其实是目前宽泛应用的一个视图解析器,次要是增加了对 MediaType 的反对。ContentNegotiatingViewResolver 这个是 Spring3.0 中引入的的视图解析器,它不负责具体的视图解析,而是依据以后申请的 MIME 类型,从上下文中抉择一个适合的视图解析器,并将申请工作委托给它。

这里咱们就先来看看 ContentNegotiatingViewResolver#resolveViewName 办法:

public View resolveViewName(String viewName, Locale locale) throws Exception {    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();    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) {        return NOT_ACCEPTABLE_VIEW;    }    else {        return null;    }}

这里的代码逻辑也比较简单:

  • 首先是获取到以后的申请对象,能够间接从 RequestContextHolder 中获取。而后从以后申请对象中提取出 MediaType。
  • 如果 MediaType 不为 null,则依据 MediaType,找到适合的视图解析器,并将解析进去的 View 返回。
  • 如果 MediaType 为 null,则为两种状况,如果 useNotAcceptableStatusCode 为 true,则返回 NOT_ACCEPTABLE_VIEW 视图,这个视图其实是一个 406 响应,示意客户端谬误,服务器端无奈提供与 Accept-Charset 以及 Accept-Language 音讯头指定的值相匹配的响应;如果 useNotAcceptableStatusCode 为 false,则返回 null。

当初问题的外围其实就变成 getCandidateViews 办法和 getBestView 办法了,看名字就晓得,前者是获取所有的候选 View,后者则是从这些候选 View 中抉择一个最佳的 View,咱们一个一个来看。

先来看 getCandidateViews:

private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)        throws Exception {    List<View> candidateViews = new ArrayList<>();    if (this.viewResolvers != null) {        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;}

获取所有的候选 View 分为两个步骤:

  1. 调用各个 ViewResolver 中的 resolveViewName 办法去加载出对应的 View 对象。
  2. 依据 MediaType 提取出扩展名,再依据扩展名去加载 View 对象,在理论利用中,这一步咱们都很少去配置,所以一步基本上是加载不进去 View 对象的,次要靠第一步。

第一步去加载 View 对象,其实就是依据你的 viewName,再联合 ViewResolver 中配置的 prefix、suffix、templateLocation 等属性,找到对应的 View,办法执行流程顺次是 resolveViewName->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);}

在这个办法中,View 加载进去后,会调用其 checkResource 办法判断 View 是否存在,如果存在就返回 View,不存在就返回 null。

这是一个十分要害的步骤,然而咱们罕用的视图对此的解决却不尽相同:

  • FreeMarkerView:会老老实实查看。
  • ThymeleafView:没有查看这个环节(Thymeleaf 的整个 View 体系不同于 FreeMarkerView 和 JstlView)。
  • JstlView:查看后果总是返回 true。

至此,咱们就找到了所有的候选 View,然而大家须要留神,这个候选 View 不肯定存在,在有 Thymeleaf 的状况下,返回的候选 View 不肯定可用,在 JstlView 中,候选 View 也不肯定真的存在。

接下来调用 getBestView 办法,从所有的候选 View 中找到最佳的 View。getBestView 办法的逻辑比较简单,就是查找看所有 View 的 MediaType,而后和申请的 MediaType 数组进行匹配,第一个匹配上的就是最佳 View,这个过程它不会查看视图是否真的存在,所以就有可能选出来一个压根没有的视图,最终导致 404。

这就是 ContentNegotiatingViewResolver#resolveViewName 办法的工作过程。

那么这里还波及到一个问题,ContentNegotiatingViewResolver 中的 ViewResolver 是从哪里来的?这个有两种起源:默认的和手动配置的。咱们来看如下一段初始化代码:

@Overrideprotected 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;            }            String name = vr.getClass().getName() + i;            obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);        }    }    AnnotationAwareOrderComparator.sort(this.viewResolvers);    this.cnmFactoryBean.setServletContext(servletContext);}
  1. 首先获取到 matchingBeans,这个是获取到了 Spring 容器中的所有视图解析器。
  2. 如果 viewResolvers 变量为 null,也就是开发者没有给 ContentNegotiatingViewResolver 配置视图解析器,此时会把查到的 matchingBeans 赋值给 viewResolvers。
  3. 如果开发者为 ContentNegotiatingViewResolver 配置了相干的视图解析器,则去查看这些视图解析器是否存在于 matchingBeans 中,如果不存在,则进行初始化操作。

这就是 ContentNegotiatingViewResolver 所做的事件。

4.AbstractCachingViewResolver

视图这种文件有一个特点,就是一旦开发好了不怎么变,所以将之缓存起来进步加载速度就显得尤为重要了。事实上咱们应用的大部分视图解析器都是反对缓存性能,也即 AbstractCachingViewResolver 实际上有很多用武之地。

咱们先来大抵理解一下 AbstractCachingViewResolver,而后再来学习它的子类。

@Override@Nullablepublic 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) {                    view = createView(viewName, locale);                    if (view == null && this.cacheUnresolved) {                        view = UNRESOLVED_VIEW;                    }                    if (view != null && this.cacheFilter.filter(view, viewName, locale)) {                        this.viewAccessCache.put(cacheKey, view);                        this.viewCreationCache.put(cacheKey, view);                    }                }            }        }        else {        }        return (view != UNRESOLVED_VIEW ? view : null);    }}
  1. 首先如果没有开启缓存,则间接调用 createView 办法创立视图返回。
  2. 调用 getCacheKey 办法获取缓存的 key。
  3. 去 viewAccessCache 中查找缓存 View,找到了就间接返回。
  4. 去 viewCreationCache 中查找缓存 View,找到了就间接返回,没找到就调用 createView 办法创立新的 View,并将 View 放到两个缓存池中。
  5. 这里有两个缓存池,两个缓存池的区别在于,viewAccessCache 的类型是 ConcurrentHashMap,而 viewCreationCache 的类型是 LinkedHashMap。前者反对并发拜访,效率十分高;后者则限度了缓存最大数,效率低于前者。当后者缓存数量达到下限时,会主动删除它里边的元素,在删除本身元素的过程中,也会删除前者 viewAccessCache 中对应的元素。

那么这里还波及到一个办法,那就是 createView,咱们也来略微看一下:

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

能够看到,createView 中调用了 loadView,而 loadView 则是一个形象办法,具体的实现要去子类中查看了。

这就是缓存 View 的查找过程。

间接继承 AbstractCachingViewResolver 的视图解析器有四种:ResourceBundleViewResolver、XmlViewResolver、UrlBasedViewResolver 以及 ThymeleafViewResolver,其中前两种从 Spring5.3 开始就曾经被废除掉了,因而这里松哥就不做过多介绍,咱们次要来看下后两者。

4.1 UrlBasedViewResolver

UrlBasedViewResolver 重写了父类的 getCacheKey、createView、loadView 三个办法:

getCacheKey

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

父类的 getCacheKey 是 viewName + '_' + locale,当初变成了 viewName。

createView

@Overrideprotected View createView(String viewName, Locale locale) throws Exception {    if (!canHandle(viewName, locale)) {        return null;    }    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);    }    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);    }    return super.createView(viewName, locale);}
  1. 首先调用 canHandle 办法判断是否反对这里的逻辑视图。
  2. 接下来判断逻辑视图名前缀是不是 redirect:,如果是,则示意这是一个重定向视图,则结构 RedirectView 进行解决。
  3. 接下来判断逻辑视图名前缀是不是 forward:,如果是,则示意这是一个服务端跳转,则结构 InternalResourceView 进行解决。
  4. 如果后面都不是,则调用父类的 createView 办法去构建视图,这最终会调用到子类的 loadView 办法。

loadView

@Overrideprotected View loadView(String viewName, Locale locale) throws Exception {    AbstractUrlBasedView view = buildView(viewName);    View result = applyLifecycleMethods(viewName, view);    return (view.checkResource(locale) ? result : null);}

这里边就干了三件事:

  1. 调用 buildView 办法构建 View。
  2. 调用 applyLifecycleMethods 办法实现 View 的初始化。
  3. 检车 View 是否存在并返回。

第三步比较简单,没啥好说的,次要就是查看视图文件是否存在,像咱们罕用的 Jsp 视图解析器以及 Freemarker 视图解析器都会去查看,然而 Thymeleaf 不会去查看(具体参见:SpringMVC 中如何同时存在多个视图解析器一文)。这里次要是前两步,松哥要和大家着重说一下,这里又波及到两个办法 buildView 和 applyLifecycleMethods。

4.1.1 buildView

这个办法就是用来构建视图的:

protected AbstractUrlBasedView buildView(String viewName) throws Exception {    AbstractUrlBasedView view = instantiateView();    view.setUrl(getPrefix() + viewName + getSuffix());    view.setAttributesMap(getAttributesMap());    String contentType = getContentType();    if (contentType != null) {        view.setContentType(contentType);    }    String requestContextAttribute = getRequestContextAttribute();    if (requestContextAttribute != null) {        view.setRequestContextAttribute(requestContextAttribute);    }    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;}
  1. 首先调用 instantiateView 办法,依据咱们在配置视图解析器时提供的 viewClass,构建一个 View 对象返回。
  2. 给 view 配置 url,就是前缀+viewName+后缀,其中前缀后缀都是咱们在配置视图解析器的时候提供的。
  3. 同理,如果用户在配置视图解析器时提供了 content-type,也将其设置给 View 对象。
  4. 配置 requestContext 的属性名称。
  5. 配置 exposePathVariables,也就是通过 @PathVaribale 注解标记的参数信息。
  6. 配置 exposeContextBeansAsAttributes,示意是否能够在 View 中应用容器中的 Bean,该参数咱们能够在配置视图解析器时提供。
  7. 配置 exposedContextBeanNames,示意能够在 View 中应用容器中的哪些 Bean,该参数咱们能够在配置视图解析器时提供。

就这样,视图就构建好了,是不是十分 easy!

4.1.2 applyLifecycleMethods

protected View applyLifecycleMethods(String viewName, AbstractUrlBasedView view) {    ApplicationContext context = getApplicationContext();    if (context != null) {        Object initialized = context.getAutowireCapableBeanFactory().initializeBean(view, viewName);        if (initialized instanceof View) {            return (View) initialized;        }    }    return view;}

这个就是 Bean 的初始化,没啥好说的。

UrlBasedViewResolver 的子类还是比拟多的,其中有两个比拟有代表性的,别离是咱们应用 JSP 时所用的 InternalResourceViewResolver 以及当咱们应用 Freemarker 时所用的 FreeMarkerViewResolver,因为这两个咱们比拟常见,因而松哥在这里再和大家介绍一下这两个组件。

4.2 InternalResourceViewResolver

当咱们应用 JSP 时,可能会用到这个视图解析器。

InternalResourceViewResolver 次要干了 4 件事:

  1. 通过 requiredViewClass 办法规定了视图。
@Overrideprotected Class<?> requiredViewClass() {    return InternalResourceView.class;}
  1. 在构造方法中调用 requiredViewClass 办法去确定视图,如果我的项目中引入了 JSTL,则会将视图调整为 JstlView。
  2. 重写了 instantiateView 办法,会依据理论状况初始化不同的 View:
@Overrideprotected AbstractUrlBasedView instantiateView() {    return (getViewClass() == InternalResourceView.class ? new InternalResourceView() :            (getViewClass() == JstlView.class ? new JstlView() : super.instantiateView()));}

会依据理论状况初始化 InternalResourceView 或者 JstlView,或者调用父类的办法实现 View 的初始化。

  1. buildView 办法也重写了,如下:
@Overrideprotected 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;}

这里首先调用父类办法构建出 InternalResourceView,而后配置 alwaysInclude,示意是否容许在应用 forward 的状况下也容许应用 include,最初面的 setPreventDispatchLoop 办法则是避免循环调用。

4.3 FreeMarkerViewResolver

FreeMarkerViewResolver 和 UrlBasedViewResolver 之间还隔了一个 AbstractTemplateViewResolver,AbstractTemplateViewResolver 比较简单,里边只是多进去了五个属性而已,这五个属性松哥在之前和大家分享 Freemarker 用法的时候都曾经说过了(参见:Spring Boot + Freemarker 中的弯弯绕!),这里再和大家啰嗦下:

  1. exposeRequestAttributes:是否将 RequestAttributes 裸露给 View 应用。
  2. allowRequestOverride:当 RequestAttributes 和 Model 中的数据同名时,是否容许 RequestAttributes 中的参数笼罩 Model 中的同名参数。
  3. exposeSessionAttributes:是否将 SessionAttributes 裸露给 View 应用。
  4. allowSessionOverride:当 SessionAttributes 和 Model 中的数据同名时,是否容许 SessionAttributes 中的参数笼罩 Model 中的同名参数。
  5. exposeSpringMacroHelpers:是否将 RequestContext 裸露进去供 Spring Macro 应用。

这就是 AbstractTemplateViewResolver 个性,比较简单,再来看 FreeMarkerViewResolver。

public class FreeMarkerViewResolver extends AbstractTemplateViewResolver {    public FreeMarkerViewResolver() {        setViewClass(requiredViewClass());    }    public FreeMarkerViewResolver(String prefix, String suffix) {        this();        setPrefix(prefix);        setSuffix(suffix);    }    @Override    protected Class<?> requiredViewClass() {        return FreeMarkerView.class;    }    @Override    protected AbstractUrlBasedView instantiateView() {        return (getViewClass() == FreeMarkerView.class ? new FreeMarkerView() : super.instantiateView());    }}

FreeMarkerViewResolver 的源码就很简略了,配置一下前后缀、重写 requiredViewClass 办法提供 FreeMarkerView,重写 instantiateView 办法实现 View 的初始化。

ThymeleafViewResolver 继承自 AbstractCachingViewResolver,具体的工作流程和后面的差不多,因而这里也就不做过多介绍了。须要留神的是,ThymeleafViewResolver#loadView 办法并不会去查看视图模版是否存在,所以有可能会最终会返回一个不存在的视图(参见:SpringMVC 中如何同时存在多个视图解析器一文)。

5.ViewResolverComposite

最初咱们再来看下 ViewResolverComposite,ViewResolverComposite 其实咱们在后面的源码剖析中曾经屡次见到过这种模式了,通过 ViewResolverComposite 来代理其余的 ViewResolver,不同的是,这里的 ViewResolverComposite 还为其余 ViewResolver 做了一些初始化操作。为对应的 ViewResolver 别离配置了 applicationContext 以及 servletContext。这里的代码比较简单,我就不贴出来了,最初在 ViewResolverComposite#resolveViewName 办法中,遍历其余视图解析器进行解决:

@Override@Nullablepublic 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;}

6.小结

好啦,明天次要和小伙伴们聊了下 SpringMVC 中视图解析器的工作流程,联合松哥之前的文章SpringMVC 中如何同时存在多个视图解析器,置信大家对于 SpringMVC 中的视图解析器的了解会更进一步。

好啦,明天就先和大家聊这么多~