SpringMVC常见组件之View分析

关联博文:
SpringMVC中支持的那些视图解析技术
SpringMVC常见组件之ViewResolver分析
SpringMVC中重定向请求时传输参数原理分析与实践

前面SpringMVC常见组件之ViewResolver分析我们分析了根据视图解析器获取视图的过程。本文我们尝试总结分析SpringMVC体系中的视图-View,主要就是render方法进行视图渲染的过程。

前置流程

DispatcherServlet#doDispatch--
DispatcherServlet#processDispatchResult--
DispatcherServlet#render--
DispatcherServlet#resolveViewName--
--view.render(mv.getModelInternal(), request, response);

前置流程如下所示,首先尝试使用localeResolver 获取locale对象,为response设置locale。然后resolveViewName获取一个view,如果view不为null,则使用view.render(mv.getModelInternal(), request, response);进行渲染。

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;
	String viewName = mv.getViewName();
	if (viewName != null) {
		// We need to resolve the view name.
		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.isTraceEnabled()) {
		logger.trace("Rendering view [" + view + "] ");
	}
	try {
	// 将mv的status值为response的status赋值
		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 + "]", ex);
		}
		throw ex;
	}
}

这里面我们先说下什么是视图渲染。简单来说就是页面+数据,页面这里可以理解为View,数据就是model。将数据解析到页面中组装最后的报文写入到response的过程就是视图渲染。

【1】View接口

在这里插入图片描述
如上图所示,其分为了SmartView、AjaxEnabledView、AbstractThymeleafView即其他(AbstractView)。

SmartView

SmartView提供了一个方法isRedirectView表示当前view是否是重定向view。目前其只有唯一实现类RedirectView。

boolean isRedirectView();

AjaxEnabledView

AjaxEnabledView提供了属性ajaxHandlerget / set方法,以使View可以被用在Spring Ajax环境中,其实现类有AjaxThymeleafView。

AbstractThymeleafView

属于org.thymeleaf体系下的(包路径是org.thymeleaf.spring5.view),主要给thymeleaf使用。
在这里插入图片描述

AbstractView

除了上面的,其他的都是AbstractView的子类,如InternalResourceViewAbstractView提供了静态属性支持,你可以通过xml配置property来定义AbstractView。其继承自WebApplicationObjectSupport提供了ServletContextApplicationContext注入能力。

属性如下所示:

/** Default content type. Overridable as bean property. */
public static final String DEFAULT_CONTENT_TYPE = "text/html;charset=ISO-8859-1";

/** Initial size for the temporary output byte array (if any). */
private static final int OUTPUT_BYTE_ARRAY_INITIAL_SIZE = 4096;

// view的contentType,默认是text/html
@Nullable
private String contentType = DEFAULT_CONTENT_TYPE;

//请求上下文中的属性
@Nullable
private String requestContextAttribute;

// 配置view类的 properties
private final Map<String, Object> staticAttributes = new LinkedHashMap<>();

// 是否暴露path variables给model
private boolean exposePathVariables = true;

//是否能够作为请求属性访问spring容器中的bean,默认是false
private boolean exposeContextBeansAsAttributes = false;

//指定上下文中应该公开的bean的名称。如果该值为非null,则只有指定的bean有资格作为属性公开。
@Nullable
private Set<String> exposedContextBeanNames;

//设置view的Name,便于你跟踪
@Nullable
private String beanName;

我们看一下其家族体系:

  • AbstractJackson2View
    • MappingJackson2JsonView
    • MappingJackson2XmlView
  • AbstractPdfView
  • MarshallingView
  • AbstractUrlBasedView
    • AbstractPdfStamperView
    • RedirectView
    • AbstractTemplateView
    • TilesView
    • XsltView
    • InternalResourceView
    • ScriptTemplateView
  • AbstractXlsView
    • AbstractXlsxView
  • AbstractFeedView
    • AbstractAtomFeedView
    • AbstractRssFeedView

主要视图说明

视图说明
MappingJackson2JsonView针对返回json格式, Jackson 2.9 to 2.12,使用Jackson 2’s的ObjectMapper序列化model,呈现为JSON。默认使用Jackson2ObjectMapperBuilder构造
MappingJackson2XmlView与 MappingJackson2JsonView 不同的是,这里将model序列化并以XML形式呈现
RedirectView重定向视图,重定向到一个绝对的或者相对的URL
InternalResourceViewweb应用程序中JSP或其他资源的包装器,将model暴露为请求属性并使用RequestDispatcher转发请求到指定的resource url
AbstractTemplateView用于基于模板的视图技术(如FreeMarker)的适配器基类,能够在其模型中使用请求和会话属性,并可以选择为Spring的FreeMarker宏库公开辅助对象
ScriptTemplateView被设计用来基于JSR-223脚本引擎运行任何模板库
AbstractXlsView用于传统XLS格式的Excel文档视图的方便超类。与Apache POI 3.5及更高版本兼容。
AbstractXlsxView与AbstractXlsView 不同的是,该类处理 Office 2007 XLSX格式(被POI-OOXML支持)

【2】AbstractView

① render

如下所示,其render是一个模板方法,三个步骤三个方法:

  • createMergedOutputModel获取数据
  • prepareResponse设置请求和响应头
  • renderMergedOutputModel,合并数据然后将数据刷入到响应
@Override
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
		HttpServletResponse response) throws Exception {

	if (logger.isDebugEnabled()) {
		logger.debug("View " + formatViewName() +
				", model " + (model != null ? model : Collections.emptyMap()) +
				(this.staticAttributes.isEmpty() ? "" : ", static attributes " + this.staticAttributes));
	}

	Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
	prepareResponse(request, response);
	// 抽象方法,让子类实现
	renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

② createMergedOutputModel

源码如下所示:

protected Map<String, Object> createMergedOutputModel(@Nullable Map<String, ?> model,
		HttpServletRequest request, HttpServletResponse response) {
// 获取URL上的变量-值 也就是uri template path variables
	@SuppressWarnings("unchecked")
	Map<String, Object> pathVars = (this.exposePathVariables ?
			(Map<String, Object>) request.getAttribute(View.PATH_VARIABLES) : null);

	// Consolidate static and dynamic model attributes.
	int size = this.staticAttributes.size();
	size += (model != null ? model.size() : 0);
	size += (pathVars != null ? pathVars.size() : 0);
// 拿到一个指定大小的map
	Map<String, Object> mergedModel = CollectionUtils.newLinkedHashMap(size);
	// 放入静态属性,如你定义view时候设置的property
	mergedModel.putAll(this.staticAttributes);
	if (pathVars != null) {
		// 放入uri template path variables
		mergedModel.putAll(pathVars);
	}
	if (model != null) {
	// 这也是动态属性的一部分,是在请求流程中放入的属性-值
		mergedModel.putAll(model);
	}

	// Expose RequestContext? 是否将RequestContext作为请求属性暴露?
	if (this.requestContextAttribute != null) {
		mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
	}
// 返回最终的结果
	return mergedModel;
}

获取model如下所示,主要步骤如下:

  • ① 尝试获取uri template path variables
  • ② 根据size创建一个mergedModel
  • ③ 将staticAttributes放入mergedModel 中
  • ④ 将①中的路径变量放入mergedModel 中;
  • ⑤ 将请求流程中的动态属性值model放入mergedModel 中;
  • ⑥ 尝试将RequestContext作为属性放入mergedModel 中。

prepareResponse方法如下所示,首先判断是否有下载内容比如PDF,如果是则设置响应头。generatesDownloadContent方法默认返回false,子类可以根据需要返回true。

protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
	if (generatesDownloadContent()) {
		response.setHeader("Pragma", "private");
		response.setHeader("Cache-Control", "private, must-revalidate");
	}
}

renderMergedOutputModel方法是个抽象方法,让子类实现。

protected abstract void renderMergedOutputModel(
			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

下面我们主要看一下AbstractThymeleafViewRedirectViewInternalResourceView 的实践过程。

【3】RedirectView

先来一张家族树图示吧。从接口上看其父类有ApplicationContextAwareServletContextAwareBeanNameAware、View、InitializingBean以及SmartView接口。前面几个我们在专栏系列里面都分析过,SmartView是第一次出现。这个“聪明视图”是什么意思呢?有点鸡贼奸猾的意思,“哦,事情大条了,我们赶紧溜,把URL换一下…”,简单来说就是重定向。
在这里插入图片描述

① 构造函数

其提供了以下系列构造函数,参数不同,但是都调用了同一个方法setExposePathVariables(false);

public RedirectView() {
	setExposePathVariables(false);
}
public RedirectView(String url) {
	super(url);
	setExposePathVariables(false);
}
public RedirectView(String url, boolean contextRelative) {
	super(url);
	this.contextRelative = contextRelative;
	setExposePathVariables(false);
}

public RedirectView(String url, boolean contextRelative, boolean http10Compatible) {
	super(url);
	this.contextRelative = contextRelative;
	this.http10Compatible = http10Compatible;
	setExposePathVariables(false);
}

public RedirectView(String url, boolean contextRelative, boolean http10Compatible, boolean exposeModelAttributes) {
	super(url);
	this.contextRelative = contextRelative;
	this.http10Compatible = http10Compatible;
	this.exposeModelAttributes = exposeModelAttributes;
	setExposePathVariables(false);
}

setExposePathVariables如下所示,就是设置变量exposePathVariables 。其表示是否将path variables暴露给model,默认是true。

public void setExposePathVariables(boolean exposePathVariables) {
	this.exposePathVariables = exposePathVariables;
}

② renderMergedOutputModel

RedirectView覆盖了父类的renderMergedOutputModel方法,源码如下所示:

@Override
protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request,
		HttpServletResponse response) throws IOException {
// 获取目标URL 如 /testr?redirectAttributes=redirectAttributesValue
	String targetUrl = createTargetUrl(model, request);
	// 尝试用requestDataValueProcessor更新URL
	targetUrl = updateTargetUrl(targetUrl, model, request, response);

	// Save flash attributes 
	RequestContextUtils.saveOutputFlashMap(targetUrl, request, response);

	// Redirect
	sendRedirect(request, response, targetUrl, this.http10Compatible);
}

方法解释如下:

  • createTargetUrl:解析目标URL这里会涉及到ContextPathpath variables以及query String parameter解析替换;如 /testr?redirectAttributes=redirectAttributesValue
  • updateTargetUrl:尝试找到一个requestDataValueProcessor处理URL
  • ③ 保存outputFlashMap,这里先从请求属性中获取到outputFlashMap,然后设置TargetRequestPathTargetRequestParams属性,之后调用FlashMapManagersaveOutputFlashMap方法
  • ④ 转发重定向,如response.sendRedirect(encodedURL);。默认设置statusCode(http1.0 302;http1.1 303)

③ saveOutputFlashMap

方法如下所示,首先获取OutputFlashMap,如果为空直接返回。然后根据location获取uriComponents ,拿到其path与query ParamsOutputFlashMap赋值。最后使用FlashMapManager 保存。

public static void saveOutputFlashMap(String location, HttpServletRequest request, HttpServletResponse response) {
// 从请求中获取属性OUTPUT_FLASH_MAP_ATTRIBUTE对应的值对象
//(FlashMap) request.getAttribute(DispatcherServlet.OUTPUT_FLASH_MAP_ATTRIBUTE);
// DispatcherServlet.class.getName() + ".OUTPUT_FLASH_MAP";
	FlashMap flashMap = getOutputFlashMap(request);
	if (CollectionUtils.isEmpty(flashMap)) {
		return;
	}
// 拿到location中的path  queryParams为flashMap更新值
	UriComponents uriComponents = UriComponentsBuilder.fromUriString(location).build();
	// 设置targetRequestPath 如/testr
	flashMap.setTargetRequestPath(uriComponents.getPath());
	// 更新targetRequestParams
	flashMap.addTargetRequestParams(uriComponents.getQueryParams());

//(FlashMapManager) request.getAttribute(DispatcherServlet.FLASH_MAP_MANAGER_ATTRIBUTE);
// DispatcherServlet.class.getName() + ".FLASH_MAP_MANAGER";
	FlashMapManager manager = getFlashMapManager(request);
	Assert.state(manager != null, "No FlashMapManager. Is this a DispatcherServlet handled request?");
	manager.saveOutputFlashMap(flashMap, request, response);
}

这里UriComponents参考实例如下所示:
在这里插入图片描述

这里我们继续看一下其manager.saveOutputFlashMap(flashMap, request, response);的实现。

@Override
public final void saveOutputFlashMap(FlashMap flashMap, HttpServletRequest request, HttpServletResponse response) {
	if (CollectionUtils.isEmpty(flashMap)) {
		return;
	}
//对path进行解码等处理
	String path = decodeAndNormalizePath(flashMap.getTargetRequestPath(), request);
	// 再次为TargetRequestPath属性赋值
	flashMap.setTargetRequestPath(path);
// 设置过期时间
	flashMap.startExpirationPeriod(getFlashMapTimeout());
// 获取锁
	Object mutex = getFlashMapsMutex(request);
	if (mutex != null) {
		synchronized (mutex) {
		// 获取session中的flashMap
			List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
			allFlashMaps = (allFlashMaps != null ? allFlashMaps : new CopyOnWriteArrayList<>());
			//添加当前flashMap
			allFlashMaps.add(flashMap);
			// 更新session中的flashmap
			updateFlashMaps(allFlashMaps, request, response);
		}
	}
	else {
		List<FlashMap> allFlashMaps = retrieveFlashMaps(request);
		allFlashMaps = (allFlashMaps != null ? allFlashMaps : new ArrayList<>(1));
		allFlashMaps.add(flashMap);
		updateFlashMaps(allFlashMaps, request, response);
	}
}

从session中获取FlashMap与更新操作源码如下所示:

// 从session中获取flashmap
@Override
@SuppressWarnings("unchecked")
@Nullable
protected List<FlashMap> retrieveFlashMaps(HttpServletRequest request) {
	HttpSession session = request.getSession(false);
	return (session != null ? (List<FlashMap>) session.getAttribute(FLASH_MAPS_SESSION_ATTRIBUTE) : null);
}
// 更新session的flashmap
@Override
protected void updateFlashMaps(List<FlashMap> flashMaps, HttpServletRequest request, HttpServletResponse response) {
	WebUtils.setSessionAttribute(request, FLASH_MAPS_SESSION_ATTRIBUTE, (!flashMaps.isEmpty() ? flashMaps : null));
}

④ RedirectView的重定向

这里我们再看一下RedirectView#sendRedirect方法。如下所示首先对URL进行编码(如果你的host为空),然后分别根据http协议1.0与1.1不同进行对应处理。

protected void sendRedirect(HttpServletRequest request, HttpServletResponse response,
		String targetUrl, boolean http10Compatible) throws IOException {

	String encodedURL = (isRemoteHost(targetUrl) ? targetUrl : response.encodeRedirectURL(targetUrl));
	// 如果是http1.0协议
	if (http10Compatible) {
		HttpStatus attributeStatusCode = (HttpStatus) request.getAttribute(View.RESPONSE_STATUS_ATTRIBUTE);
		if (this.statusCode != null) {
			response.setStatus(this.statusCode.value());
			response.setHeader("Location", encodedURL);
		}
		else if (attributeStatusCode != null) {
			response.setStatus(attributeStatusCode.value());
			response.setHeader("Location", encodedURL);
		}
		else {
		// 默认设置302 
			// Send status code 302 by default.
			response.sendRedirect(encodedURL);
		}
	}
	else {
	// http1.1协议 默认设置303
		HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
		response.setStatus(statusCode.value());
		response.setHeader("Location", encodedURL);
	}
}

【4】InternalResourceView

JSP或者其他应用资源的包装器,将model数据作为request属性暴露出去并使用RequestDispatcher转发请求到具体的目标资源URL。
在这里插入图片描述

常见的使用配置如下所示:

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

每一个被handler返回的视图名称都会被转换为一个具体的JSP资源,如 "myView" -> "/WEB-INF/jsp/myView.jsp"

我们看下这个renderMergedOutputModel方法。

@Override
protected void renderMergedOutputModel(
		Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

	// Expose the model object as request attributes.
	exposeModelAsRequestAttributes(model, request);

	// Expose helpers as request attributes, if any.
	exposeHelpers(request);

	// Determine the path for the request dispatcher.
	String dispatcherPath = prepareForRendering(request, response);

	// Obtain a RequestDispatcher for the target resource (typically a JSP).
	RequestDispatcher rd = getRequestDispatcher(request, dispatcherPath);
	if (rd == null) {
		throw new ServletException("Could not get RequestDispatcher for [" + getUrl() +
				"]: Check that the corresponding file exists within your web application archive!");
	}

	// If already included or response already committed, perform include, else forward.
	if (useInclude(request, response)) {
		response.setContentType(getContentType());
		if (logger.isDebugEnabled()) {
			logger.debug("Including [" + getUrl() + "]");
		}
		rd.include(request, response);
	}

	else {
		// Note: The forwarded resource is supposed to determine the content type itself.
		if (logger.isDebugEnabled()) {
			logger.debug("Forwarding to [" + getUrl() + "]");
		}
		rd.forward(request, response);
	}
}

方法解释如下:

  • ① 暴露model数据作为request中的属性;
  • ② 暴露helpers作为request属性,默认方法为空,在子类JstlView有实现
  • ③ 获取请求path;
  • ④ 获取RequestDispatcher,用于转发或者include;
  • ⑤ 决定是rd.include(request, response);或者rd.forward(request, response);
  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值