SpringMVC 九大组件之 ViewResolver 深入分析,java高级工程师面试视频


ContentNegotiatingViewResolver 其实是目前广泛使用的一个视图解析器,主要是添加了对 MediaType 的支持。ContentNegotiatingViewResolver 这个是 Spring3.0 中引入的的视图解析器,它不负责具体的视图解析,而是根据当前请求的 MIME 类型,从上下文中选择一个合适的视图解析器,并将请求工作委托给它。

这里我们就先来看看 ContentNegotiatingViewResolver#resolveViewName 方法:

public View resolveViewName(String viewName, Locale locale) throws Exception {

RequestAttributes attrs = RequestContextHolder.getRequestAttributes();

List requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());

if (requestedMediaTypes != null) {

List 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 getCandidateViews(String viewName, Locale locale, List requestedMediaTypes)

throws Exception {

List 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 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 是从哪里来的?这个有两种来源:默认的和手动配置的。我们来看如下一段初始化代码:

@Override

protected void initServletContext(ServletContext servletContext) {

Collection 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

@Nullable

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) {

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,我们也来稍微看一下:

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

可以看到,createView 中调用了 loadView,而 loadView 则是一个抽象方法,具体的实现要去子类中查看了。

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

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

4.1 UrlBasedViewResolver

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

getCacheKey

@Override

protected Object getCacheKey(String viewName, Locale locale) {

return viewName;

}

父类的 getCacheKey 是 viewName + '_' + locale,现在变成了 viewName。

createView

@Override

protected 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

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

}

这里边就干了三件事:

  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;

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

架构学习资料

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

准备两个月,面试五分钟,Java中高级岗面试为何越来越难?

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

架构学习资料

[外链图片转存中…(img-Hdsw1hqx-1712188362619)]

[外链图片转存中…(img-6qHA5SNG-1712188362620)]

[外链图片转存中…(img-jRjxlNPn-1712188362620)]

[外链图片转存中…(img-KkLkElv5-1712188362620)]

[外链图片转存中…(img-xaby4Lgr-1712188362621)]

由于篇幅限制小编,pdf文档的详解资料太全面,细节内容实在太多啦,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!

一个人可以走的很快,但一群人才能走的更远。如果你从事以下工作或对以下感兴趣,欢迎戳这里加入程序员的圈子,让我们一起学习成长!

AI人工智能、Android移动开发、AIGC大模型、C C#、Go语言、Java、Linux运维、云计算、MySQL、PMP、网络安全、Python爬虫、UE5、UI设计、Unity3D、Web前端开发、产品经理、车载开发、大数据、鸿蒙、计算机网络、嵌入式物联网、软件测试、数据结构与算法、音视频开发、Flutter、IOS开发、PHP开发、.NET、安卓逆向、云计算

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值