SpringMVC源码总结(十二)ViewResolver介绍

2023-11-03 23:30

本文主要是介绍SpringMVC源码总结(十二)ViewResolver介绍,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

首先我们先看看ModelAndView中重要的View接口。
View接口:

String getContentType();/*** Render the view given the specified model.* <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 Map with name Strings as keys and corresponding model* objects as values (Map can also be {@code null} in case of empty model)* @param request current HTTP request* @param response HTTP response we are building* @throws Exception if rendering failed*/
//上面说的很清楚,对于jsp来说,第一步就是将model作为request的attributes;第二步才开始渲染viewvoid render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

再看下ViewResolver接口:
View resolveViewName(String viewName, Locale locale) throws Exception;

它是对给定的viewName找到对应的View对象,然后使用该view对象的render方法将本身的内容写到response中。
然后就看下,当我们的处理函数返回一个viewName时,SpringMVC是如何渲染的。

try {// Actually invoke the handler.mv = ha.handle(processedRequest, response, mappedHandler.getHandler());}finally {if (asyncManager.isConcurrentHandlingStarted()) {return;}}applyDefaultViewName(request, mv);mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}
//这里是我们的关注重点,就是进行视图渲染的过程processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Error err) {triggerAfterCompletionWithError(processedRequest, response, mappedHandler, err);}

继续看下processDispatchResult是如何来渲染的
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {boolean errorView = false;if (exception != null) {if (exception instanceof ModelAndViewDefiningException) {logger.debug("ModelAndViewDefiningException encountered", exception);mv = ((ModelAndViewDefiningException) exception).getModelAndView();}else {Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);mv = processHandlerException(request, response, handler, exception);errorView = (mv != null);}}// Did the handler return a view to render?
//这里是我们关注的重点if (mv != null && !mv.wasCleared()) {render(mv, request, response);if (errorView) {WebUtils.clearErrorRequestAttributes(request);}}else {if (logger.isDebugEnabled()) {logger.debug("Null ModelAndView returned to DispatcherServlet with name '" + getServletName() +"': assuming HandlerAdapter completed request handling");}}if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {// Concurrent handling started during a forwardreturn;}if (mappedHandler != null) {mappedHandler.triggerAfterCompletion(request, response, null);}}

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.resolveLocale(request);response.setLocale(locale);View view;if (mv.isReference()) {// We need to resolve the view name.view = resolveViewName(mv.getViewName(), 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 {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是不是一个视图的名称,若是需要找到这个视图名称对应的View对象,然后便是调用view对象的render方法,渲染到response中。
由于我们的处理函数经常仅仅是返回一个view名称,所以我们重点要看看它是如何根据视图名称来找到对应的View对象的,即resolveViewName方法内容。其实上文已经说明了View接口和ViewResolver 接口,ViewResolver 接口就是根据view名称来找到对应的View对象的,所以看下面就会很清晰明白

protected View resolveViewName(String viewName, Map<String, Object> model, Locale locale,HttpServletRequest request) throws Exception {for (ViewResolver viewResolver : this.viewResolvers) {View view = viewResolver.resolveViewName(viewName, locale);if (view != null) {return view;}}return null;}

这里就是对DispatcherServlet的private List<ViewResolver> viewResolvers属性进行遍历找到一个能够获取View对象的ViewResolver,并返回这个view对象。
至此整个流程便走通了,接下来就是要看看有哪些ViewResolver以及它们的注册来源是什么?

常用的ViewResolver有:FreeMarkerViewResolver、InternalResourceViewResolver、VelocityViewResolver等。

接下来就是如何来注册这些ViewResolver:

protected void initStrategies(ApplicationContext context) {initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);
//我们关注的重点initViewResolvers(context);initFlashMapManager(context);}

还是在DispatcherServlet的初始化策略中,调用了initViewResolvers,如下:
private void initViewResolvers(ApplicationContext context) {this.viewResolvers = null;if (this.detectAllViewResolvers) {// Find all ViewResolvers in the ApplicationContext, including ancestor contexts.Map<String, ViewResolver> matchingBeans =BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false);if (!matchingBeans.isEmpty()) {this.viewResolvers = new ArrayList<ViewResolver>(matchingBeans.values());// We keep ViewResolvers in sorted order.OrderComparator.sort(this.viewResolvers);}}else {try {ViewResolver vr = context.getBean(VIEW_RESOLVER_BEAN_NAME, ViewResolver.class);this.viewResolvers = Collections.singletonList(vr);}catch (NoSuchBeanDefinitionException ex) {// Ignore, we'll add a default ViewResolver later.}}// Ensure we have at least one ViewResolver, by registering// a default ViewResolver if no other resolvers are found.if (this.viewResolvers == null) {this.viewResolvers = getDefaultStrategies(context, ViewResolver.class);if (logger.isDebugEnabled()) {logger.debug("No ViewResolvers found in servlet '" + getServletName() + "': using default");}}}

这和HandleMapping和HandlerAdapter的初始化过程基本类似。this.detectAllViewResolvers是DispatcherServlet的一个boolean属性,可以在web.xml文件中修改这个值,默认是true。
/** Detect all ViewResolvers or just expect "viewResolver" bean? */private boolean detectAllViewResolvers = true;

当detectAllViewResolvers为true,意味着就会获取从xml文件中解析出来的ViewResolver。如果为false,则直接去找bean name为"viewResolver"并且是ViewResolver类型的作为DispatcherServlet的ViewResolver。
当上述两种情况都没有找到,则会启用默认的ViewResolver,在this.viewResolvers = getDefaultStrategies(context, ViewResolver.class)中,这个过程已经多次说过,可以见本系列第一篇HandleMapping的来源。它就是依据DispatcherServlet.properties文件中所配置的ViewResolver,如下:

org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver

也就是默认采用的是InternalResourceViewResolver。
再说说在xml文件中配置ViewResolver的情况,如下:

<bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer"><property name="templateLoaderPath" value="/WEB-INF/views" /><property name="defaultEncoding" value="utf-8" /><property name="freemarkerSettings"><props><prop key="locale">zh_CN</prop></props></property></bean><bean class="org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver"><property name="suffix" value=".html" /><property name="contentType" value="text/html;charset=utf-8" /><property name="requestContextAttribute" value="request" /><property name="exposeRequestAttributes" value="true" /><property name="exposeSessionAttributes" value="true" /></bean>

这里是以FreeMarkerViewResolver为例来说明,它的配置内容还是需要有待继续研究。这里只是粗略的说下它的继承情况。
FreeMarkerViewResolver继承AbstractTemplateViewResolver继承UrlBasedViewResolver继承AbstractCachingViewResolver。
首先是抽象类AbstractCachingViewResolver:它加入了缓存功能,它有几个重要的属性。

/** 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;/** 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) {@Overrideprotected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {if (size() > getCacheLimit()) {viewAccessCache.remove(eldest.getKey());return true;}else {return false;}}};

属性一:cacheLimit 最大的缓存数量,默认为1024。
属性二:viewAccessCache 是ConcurrentHashMap类型的,适合高并发。
属性三:viewCreationCache是LinkedHashMap类型的
我们再来看下,由view名称来解析到view视图对象的具体过程:

public View resolveViewName(String viewName, Locale locale) throws Exception {
//这里进行了是否进行缓存的判断,即cacheLimit是否大于0if (!isCache()) {//不进行缓存,始终每次都创建return createView(viewName, locale);}else {//viewAccessCache viewCreationCache两者的keyObject 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);}}

对于Object cacheKey = getCacheKey(viewName, locale);默认为viewName + "_" + locale;
但是可以被子类覆盖,子类UrlBasedViewResolver覆盖了它,变成只有viewName。

先从viewAccessCache中看能否找到已缓存的view视图,若能找到则返回。若未找到则加上同步锁synchronized (this.viewCreationCache),进入这个方法之后,最关键的是仍需要进行一次判断view = this.viewCreationCache.get(cacheKey),看看是否已经创建过了,并不是viewAccessCache和viewCreationCache他们所缓存的内容不一样而是如果没有这个判断,则会有多线程问题。

如线程1和线程2同时要解析相同的view名称,他们都来到同步锁synchronized (this.viewCreationCache)之前,线程2先拿到锁,线程1等待,线程2创建好view视图后,加入viewCreationCache和viewAccessCache,并释放锁。此时线程1获得锁,进入同步锁synchronized (this.viewCreationCache)内部,若不进行判断,则线程1又会去创建一次view视图。所以view = this.viewCreationCache.get(cacheKey)并判断view是否为null这一步骤是十分有用的。

创建View视图的任务就交给了子类来实现。resolveViewName这个方法基本上就分析完了,应该还会想到,它的那个cacheLimit限制好像还没发挥出作用。
继续回看

private final Map<Object, View> viewAccessCache = new ConcurrentHashMap<Object, View>(DEFAULT_CACHE_LIMIT);private final Map<Object, View> viewCreationCache =new LinkedHashMap<Object, View>(DEFAULT_CACHE_LIMIT, 0.75f, true) {@Overrideprotected boolean removeEldestEntry(Map.Entry<Object, View> eldest) {if (size() > getCacheLimit()) {viewAccessCache.remove(eldest.getKey());return true;}else {return false;}}};

viewCreationCache 的类型是LinkedHashMap,但是它复写了protected boolean removeEldestEntry(Map.Entry<Object, View> eldest)方法,当该方法返回true时,LinkedHashMap则会删除最老的key。在这里我们可以看到,当viewCreationCache 的所存的View数量达到cacheLimit时,就会删除最老的那个key和value,同时也会使viewAccessCache删除这个key和value。

viewAccessCache主要是用来高并发的访问,viewCreationCache 则是用来统计最老的key。他们所存储的view都是一样的。

转载于:https://my.oschina.net/Sheamus/blog/393591

这篇关于SpringMVC源码总结(十二)ViewResolver介绍的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/341627

相关文章

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Android实现定时任务的几种方式汇总(附源码)

《Android实现定时任务的几种方式汇总(附源码)》在Android应用中,定时任务(ScheduledTask)的需求几乎无处不在:从定时刷新数据、定时备份、定时推送通知,到夜间静默下载、循环执行... 目录一、项目介绍1. 背景与意义二、相关基础知识与系统约束三、方案一:Handler.postDel

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推