0
点赞
收藏
分享

微信扫一扫

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接口

SpringMVC常见组件之View分析_spring
如上图所示,其分为了SmartView、AjaxEnabledView、AbstractThymeleafView即其他(AbstractView)。

SmartView

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

boolean isRedirectView();

AjaxEnabledView

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

AbstractThymeleafView

属于org.thymeleaf体系下的(包路径是org.thymeleaf.spring5.view),主要给thymeleaf使用。
SpringMVC常见组件之View分析_spring mvc_02

AbstractView

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

属性如下所示:

/** 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

InternalResourceView

web应用程序中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;

下面我们主要看一下​​AbstractThymeleafView​​​ 、​​RedirectView​​​ 与​​InternalResourceView​​ 的实践过程。

【3】RedirectView

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

① 构造函数

其提供了以下系列构造函数,参数不同,但是都调用了同一个方法​​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这里会涉及到​​ContextPath​​​、​​path variables​​​以及​​query String parameter​​​解析替换;如​​/testr?redirectAttributes=redirectAttributesValue​
  • ②​​updateTargetUrl​​​:尝试找到一个​​requestDataValueProcessor​​处理URL
  • ③ 保存​​outputFlashMap​​​,这里先从请求属性中获取到​​outputFlashMap​​​,然后设置​​TargetRequestPath​​​和​​TargetRequestParams​​​属性,之后调用​​FlashMapManager​​​的​​saveOutputFlashMap​​方法
  • ④ 转发重定向,如​​response.sendRedirect(encodedURL);​​​。默认设置​​statusCode(http1.0 302;http1.1 303)​

③ saveOutputFlashMap

方法如下所示,首先获取​​OutputFlashMap​​​,如果为空直接返回。然后根据location获取​​uriComponents​​​ ,拿到其path与​​query Params​​​为​​OutputFlashMap​​​赋值。最后使用​​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​​​参考实例如下所示:
SpringMVC常见组件之View分析_spring mvc_04

这里我们继续看一下其​​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。
SpringMVC常见组件之View分析_spring_05

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

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


举报

相关推荐

0 条评论