SpringMVC常见组件之View分析

简介: SpringMVC常见组件之View分析

前面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的子类,如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


主要视图说明



6.png

【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

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


eacddfbfb105480bad4b681b187df7c3.png

① 构造函数

其提供了以下系列构造函数,参数不同,但是都调用了同一个方法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参考实例如下所示:

这里我们继续看一下其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>


我们看下这个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);

目录
相关文章
|
9月前
|
容器
SpringMVC常见组件之HandlerExceptionResolver分析-2
SpringMVC常见组件之HandlerExceptionResolver分析-2
55 0
|
4月前
|
设计模式
SpringMVC常见组件之DataBinder数据绑定器分析
SpringMVC常见组件之DataBinder数据绑定器分析
262 0
|
9月前
|
XML 缓存 Java
SpringMVC常见组件之ViewResolver分析
本文我们尝试总结分析SpringMVC体系中的视图解析器-ViewResolver。其根据name解析视图View,通常鼓励实现类考虑国际化策略实现。
94 0
|
4月前
|
XML 存储 Java
SpringMVC常见组件之HandlerMapping分析
SpringMVC常见组件之HandlerMapping分析
117 0
|
4月前
|
XML 缓存 前端开发
SpringMVC常见组件之HandlerAdapter分析
SpringMVC常见组件之HandlerAdapter分析
60 0
|
9月前
|
JSON 前端开发 Java
SpringMVC常见组件之HandlerExceptionResolver分析-1
SpringMVC常见组件之HandlerExceptionResolver分析-1
116 0
|
XML 存储 前端开发
解析 SpringMVC 父子容器及九大内置组件加载过程
解析 SpringMVC 父子容器及九大内置组件加载过程
104 1
java202304java学习笔记第六十二天-ssm-springMvc中组件分析
java202304java学习笔记第六十二天-ssm-springMvc中组件分析
42 0
|
存储 设计模式 前端开发
浅谈SpringMVC五大组件以及对执行原理的分析。
Spring MVC是包含在spring中的一个基于MVC设计思想的Web应用程序框架,目的是简化开发工作,提高开发效率。
浅谈SpringMVC五大组件以及对执行原理的分析。
|
前端开发 Java 程序员
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】(下)
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】(下)
CORS跨域资源共享(三):@CrossOrigin/CorsFilter处理跨域请求示例及原理分析【享学Spring MVC】(下)