文档章节

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

乒乓狂魔
 乒乓狂魔
发布于 2015/02/07 11:09
字数 2091
阅读 235
收藏 1
首先我们先看看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;第二步才开始渲染view
	void 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 forward
			return;
		}

		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) {
				@Override
				protected 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是否大于0
		if (!isCache()) {
                        //不进行缓存,始终每次都创建
			return createView(viewName, locale);
		}
		else {
                        //viewAccessCache viewCreationCache两者的key
			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) {
						// 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) {
				@Override
				protected 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都是一样的。

© 著作权归作者所有

共有 人打赏支持
乒乓狂魔
粉丝 1012
博文 105
码字总数 271356
作品 0
长宁
程序员
私信 提问
Springmvc+mybatis+restful+bootstrap框架整合

Spring MVC工作流程图 图一 图二 Spring工作流程描述 1. 用户向服务器发送请求,请求被Spring 前端控制Servelt DispatcherServlet捕获; 2. DispatcherServlet对请求URL进行解析,得到请求资...

qq59410a0b9a625
06/27
0
0
网关 Spring-Cloud-Gateway 源码解析 —— 网关初始化

网关 Spring-Cloud-Gateway 源码解析 —— 网关初始化 Harries Blog™2017-12-135 阅读 SpringAppclasspathcatbeanAPIbuildbug 本文主要基于 Spring-Cloud-Gateway 2.0.X M4 摘要: 原创出处 ......

Harries Blog™
2017/12/13
0
0
【小马哥】Spring Cloud系列讲座

这里为大家推荐一个不错的Spring Cloud系列讲座,讲师介绍如下: 小马哥,阿里巴巴技术专家,从事十余年Java EE 开发,国内微服务技术讲师。目前主要负责微服务技术推广、架构设计、基础设施...

杜琪
03/02
0
0
Spring MVC 原理探秘 - 容器的创建过程

1.简介 在上一篇文章中,我向大家介绍了 Spring MVC 是如何处理 HTTP 请求的。Spring MVC 可对外提供服务时,说明其已经处于了就绪状态。再次之前,Spring MVC 需要进行一系列的初始化操作。...

coolblog.xyz
07/03
0
0
spring boot框架学习5-spring boot的web开发(1)

本章节主要内容: 通过前面的学习,我们了解并快速完成了spring boot第一个应用。spring boot企业级框架,那么spring boot怎么读取静态资源?如js文件夹,css文件以及png/jpg图片呢?怎么自定...

凯哥java
2017/11/01
0
0

没有更多内容

加载失败,请刷新页面

加载更多

微服务分布式事务实现

https://www.processon.com/view/link/5b2144d7e4b001a14d3d2d30

WALK_MAN
今天
2
0
《大漠烟尘》读书笔记及读后感文章3700字

《大漠烟尘》读书笔记及读后感文章3700字: 在这个浮躁的社会里,你有多久没有好好读完一本书了? 我们总觉得自己和别人不一样,所以当看到别人身上的问题时,很少有“反求诸己”,反思自己。...

原创小博客
今天
3
0
大数据教程(9.5)用MR实现sql中的jion逻辑

上一篇博客讲解了使用jar -jar的方式来运行提交MR程序,以及通过修改YarnRunner的源码来实现MR的windows开发环境提交到集群的方式。本篇博主将分享sql中常见的join操作。 一、需求 订单数据表...

em_aaron
今天
3
0
十万个为什么之什么是resultful规范

起源 越来越多的人开始意识到,网站即软件,而且是一种新型的软件。这种"互联网软件"采用客户端/服务器模式,建立在分布式体系上,通过互联网通信,具有高延时(high latency)、高并发等特点...

尾生
今天
3
0
Terraform配置文件(Terraform configuration)

Terraform配置文件 翻译自Terraform Configuration Terraform用文本文件来描述设备、设置变量。这些文件被称为Terraform配置文件,以.tf结尾。这一部分将讲述Terraform配置文件的加载与格式。...

buddie
今天
5
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部