前面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提供了属性ajaxHandler
的get / 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
主要视图说明
【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换一下…”,简单来说就是重定向。
① 构造函数
其提供了以下系列构造函数,参数不同,但是都调用了同一个方法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);