Spring MVC : 概念模型 View

本文深入探讨SpringMVC框架中View接口的实现原理,包括其核心职责、接口定义及抽象基类AbstractView的特性。重点讲解了如何通过静态属性和动态属性构建数据模型,并介绍了一系列内置View实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

Spring MVC中接口View是对MVC模式中V的抽象建模。一个View实现负责使用给定的Model(MVC中的M)渲染一个页面给用户。
接口View定义了如下两个方法 :

  • String getContentType()

    返回Content-Type字符串。如果不能提前确定,返回null

  • render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)

    使用指定的数据模型model渲染页面。如果没有模型数据,model可以是null或者空对象。

为了提供更多便利,Spring MVC对接口View提供了抽象实现AbstractView,封装了一些通用逻辑。框架某处,或者开发人员如果要实现一个View,继承AbstractView做自己的扩展定制即可。

以下是Spring框架自身提供的一些View实现,从此清单可以看出,绝大多数继承自AbstractView:

  • View
    • AbstractView
      • AbstractUrlBasedView
        • AbstractTemplateView
          • FreeMarkerViewFreeMarker视图渲染
          • TilesViewTiles视图渲染
          • InternalResourceView
            • JstlViewJSP视图渲染
          • (+ SmartView ==> ) RedirectView – 重定向
          • XsltView
          • GroovyMarkupView
        • AbstractPdfStamperView
        • ScriptTemplateView
      • MarshallingView
      • AbstractJackson2View
        • MappingJackson2JsonView
      • AbstractFeedView
        • AbstractRssFeedView
        • AbstractAtomFeedView
      • AbstractXlsView
        • AbstractXlsxView
          • AbstractXlsxStreamingView
      • AbstractPdfViewPDF文档生成
    • ErrorMvcAutoConfiguration$StaticView

Spring MVC实现AbstractView 时,对数据模型做了进一步的细化管理。它将数据模型内的属性分成两部分,静态属性和动态属性。静态属性指的是视图对象实例化时提供的数据模型属性,动态属性指的是控制器方法返回的数据模型属性。而一次渲染,也就是render方法会将静态属性和动态属性合并在一起作为最终要使用的数据模型。在合并过程中,如果遇到同名静态属性和动态属性,使用动态属性的值。

源代码

源代码版本 : spring-webmvc-5.1.5.RELEASE

View接口

package org.springframework.web.servlet;

import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.lang.Nullable;


public interface View {

	/**
	 * Name of the HttpServletRequest attribute that contains the response status code.
	 * Note: This attribute is not required to be supported by all View implementations.
	 * @since 3.0
	 */
	String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus";

	/**
	 * Name of the HttpServletRequest attribute that contains a Map with path variables.
	 * The map consists of String-based URI template variable names as keys and their corresponding
	 * Object-based values -- extracted from segments of the URL and type converted.
	 * Note: This attribute is not required to be supported by all View implementations.
	 * @since 3.1
	 */
	String PATH_VARIABLES = View.class.getName() + ".pathVariables";

	/**
	 * The org.springframework.http.MediaType selected during content negotiation,
	 * which may be more specific than the one the View is configured with. For example:
	 * "application/vnd.example-v1+xml" vs "application/*+xml".
	 * @since 3.2
	 */
	String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType";


	/**
	 * Return the content type of the view, if predetermined.
	 * Can be used to check the view's content type upfront,
	 * i.e. before an actual rendering attempt.
	 * @return the content type String (optionally including a character set),
	 * or null if not predetermined
	 */
	@Nullable
	default String getContentType() {
		return null;
	}

	/**
	 * Render the view given the specified model.
	 * 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 a Map with name Strings as keys and corresponding model
	 * objects as values (Map can also be null in case of empty model)
	 * @param request current HTTP request
	 * @param response he HTTP response we are building
	 * @throws Exception if rendering failed
	 */
	void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
			throws Exception;

}

抽象基类AbstractView

package org.springframework.web.servlet.view;

// 省略 import 行

public abstract class AbstractView extends WebApplicationObjectSupport implements View, BeanNameAware {

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


	@Nullable
	private String contentType = DEFAULT_CONTENT_TYPE;

    // 如果要将 RequestContext 作为一个属性放到数据模型中,就使用该变量
    // 指定相应的属性名称。缺省值为 null,表示不把 RequestContext 放到
    // 数据模型中
	@Nullable
	private String requestContextAttribute;

    // 静态属性,在当前 View 对象创建时被填充内容
	private final Map<String, Object> staticAttributes = new LinkedHashMap<>();

    // 是否暴露路径变量到模型,缺省使用 true
	private boolean exposePathVariables = true;

    // 是否将所有 Spring IoC 容器中的 bean 作为请求属性暴露,属性名称使用 bean 名称。
    // 缺省为 false。
    // 如果将该值设置为 true,则所有 bean 会作为请求属性暴露,在 JSP 2.0
    // 中,通过 ${...}这种方式就能经可以访问到 bean 。
    // 一旦开启该功能,请求属性的覆盖的优先级为 :
    // model 属性 > bean > 自定义的 request/session 属性
    // 跟该属性功能类似的属性是 exposedContextBeanNames,但二者不同
	private boolean exposeContextBeansAsAttributes = false;

   // 要暴露到请求属性空间的 bean 的名称清单
   // 这个属性 exposedContextBeanNames 和 exposeContextBeansAsAttributes=true 的
   // 区别是 exposedContextBeanNames 仅仅暴露部分bean,而
   // exposeContextBeansAsAttributes=true  暴露所有 bean
	@Nullable
	private Set<String> exposedContextBeanNames;

    // 记录当前View的 bean 名称,方便跟踪使用,框架构造View对象时会设置该值
	@Nullable
	private String beanName;



	/**
	 * Set the content type for this view.
	 * Default is "text/html;charset=ISO-8859-1".
	 * May be ignored by subclasses if the view itself is assumed
	 * to set the content type, e.g. in case of JSPs.
	 */
	public void setContentType(@Nullable String contentType) {
		this.contentType = contentType;
	}

	/**
	 * Return the content type for this view.
	 */
	@Override
	@Nullable
	public String getContentType() {
		return this.contentType;
	}

	/**
	 * Set the name of the RequestContext attribute for this view.
	 * Default is none.
	 */
	public void setRequestContextAttribute(@Nullable String requestContextAttribute) {
		this.requestContextAttribute = requestContextAttribute;
	}

	/**
	 * Return the name of the RequestContext attribute, if any.
	 */
	@Nullable
	public String getRequestContextAttribute() {
		return this.requestContextAttribute;
	}

	/**
	 * 使用 CSV 格式字符串添加静态属性
     * Set static attributes as a CSV string.
	 * Format is: attname0={value1},attname1={value1}
	 * "Static" attributes are fixed attributes that are specified in
	 * the View instance configuration. "Dynamic" attributes, on the other hand,
	 * are values passed in as part of the model.
	 */
	public void setAttributesCSV(@Nullable String propString) throws IllegalArgumentException {
		if (propString != null) {
			StringTokenizer st = new StringTokenizer(propString, ",");
			while (st.hasMoreTokens()) {
				String tok = st.nextToken();
				int eqIdx = tok.indexOf('=');
				if (eqIdx == -1) {
					throw new IllegalArgumentException(
							"Expected '=' in attributes CSV string '" + propString + "'");
				}
				if (eqIdx >= tok.length() - 2) {
					throw new IllegalArgumentException(
							"At least 2 characters ([]) required in attributes CSV string '" + propString + "'");
				}
				String name = tok.substring(0, eqIdx);
				String value = tok.substring(eqIdx + 1);

				// Delete first and last characters of value: { and }
				value = value.substring(1);
				value = value.substring(0, value.length() - 1);

				addStaticAttribute(name, value);
			}
		}
	}

	/**
	 * 通过 Properties 对象形式添加静态属性
     * Set static attributes for this view from a
	 * java.util.Properties object.
	 * "Static" attributes are fixed attributes that are specified in
	 * the View instance configuration. "Dynamic" attributes, on the other hand,
	 * are values passed in as part of the model.
	 * This is the most convenient way to set static attributes. Note that
	 * static attributes can be overridden by dynamic attributes, if a value
	 * with the same name is included in the model.
	 * Can be populated with a String "value" (parsed via PropertiesEditor)
	 * or a "props" element in XML bean definitions.
	 * @see org.springframework.beans.propertyeditors.PropertiesEditor
	 */
	public void setAttributes(Properties attributes) {
		CollectionUtils.mergePropertiesIntoMap(attributes, this.staticAttributes);
	}

	/**
	 *  通过 Map 对象形式添加静态属性
     * Set static attributes for this view from a Map. This allows to set
	 * any kind of attribute values, for example bean references.
	 * "Static" attributes are fixed attributes that are specified in
	 * the View instance configuration. "Dynamic" attributes, on the other hand,
	 * are values passed in as part of the model.
	 * Can be populated with a "map" or "props" element in XML bean definitions.
	 * @param attributes a Map with name Strings as keys and attribute objects as values
	 */
	public void setAttributesMap(@Nullable Map<String, ?> attributes) {
		if (attributes != null) {
			attributes.forEach(this::addStaticAttribute);
		}
	}

	/**
	 *  返回静态属性
     * Allow Map access to the static attributes of this view,
	 * with the option to add or override specific entries.
	 * Useful for specifying entries directly, for example via
	 * "attributesMap[myKey]". This is particularly useful for
	 * adding or overriding entries in child view definitions.
	 */
	public Map<String, Object> getAttributesMap() {
		return this.staticAttributes;
	}

	/**
	 * 添加一个静态属性
     * Add static data to this view, exposed in each view.
	 * "Static" attributes are fixed attributes that are specified in
	 * the View instance configuration. "Dynamic" attributes, on the other hand,
	 * are values passed in as part of the model.
	 * Must be invoked before any calls to render.
	 * @param name the name of the attribute to expose
	 * @param value the attribute value to expose
	 * @see #render
	 */
	public void addStaticAttribute(String name, Object value) {
		this.staticAttributes.put(name, value);
	}

	/**
	 *  返回静态属性
     * Return the static attributes for this view. Handy for testing.
	 * Returns an unmodifiable Map, as this is not intended for
	 * manipulating the Map but rather just for checking the contents.
	 * @return the static attributes in this view
	 */
	public Map<String, Object> getStaticAttributes() {
		return Collections.unmodifiableMap(this.staticAttributes);
	}

	/**
	 * Specify whether to add path variables to the model or not.
	 * Path variables are commonly bound to URI template variables through the @PathVariable
	 * annotation. They're are effectively URI template variables with type conversion applied to
	 * them to derive typed Object values. Such values are frequently needed in views for
	 * constructing links to the same and other URLs.
	 * Path variables added to the model override static attributes (see #setAttributes(Properties))
	 * but not attributes already present in the model.
	 * By default this flag is set to true. Concrete view types can override this.
	 * @param exposePathVariables true to expose path variables, and false otherwise
	 */
	public void setExposePathVariables(boolean exposePathVariables) {
		this.exposePathVariables = exposePathVariables;
	}

	/**
	 * Return whether to add path variables to the model or not.
	 */
	public boolean isExposePathVariables() {
		return this.exposePathVariables;
	}

	/**
	 * Set whether to make all Spring beans in the application context accessible
	 * as request attributes, through lazy checking once an attribute gets accessed.
	 * This will make all such beans accessible in plain ${...}
	 * expressions in a JSP 2.0 page, as well as in JSTL's c:out
	 * value expressions.
	 * Default is "false". Switch this flag on to transparently expose all
	 * Spring beans in the request attribute namespace.
	 * NOTE: Context beans will override any custom request or session
	 * attributes of the same name that have been manually added. However, model
	 * attributes (as explicitly exposed to this view) of the same name will
	 * always override context beans.
	 * @see #getRequestToExpose
	 */
	public void setExposeContextBeansAsAttributes(boolean exposeContextBeansAsAttributes) {
		this.exposeContextBeansAsAttributes = exposeContextBeansAsAttributes;
	}

	/**
	 * Specify the names of beans in the context which are supposed to be exposed.
	 * If this is non-null, only the specified beans are eligible for exposure as
	 * attributes.
	 * If you'd like to expose all Spring beans in the application context, switch
	 * the #setExposeContextBeansAsAttributes "exposeContextBeansAsAttributes"
	 * flag on but do not list specific bean names for this property.
	 */
	public void setExposedContextBeanNames(String... exposedContextBeanNames) {
		this.exposedContextBeanNames = new HashSet<>(Arrays.asList(exposedContextBeanNames));
	}

	/**
	 * Set the view's name. Helpful for traceability.
	 * Framework code must call this when constructing views.
	 */
	@Override
	public void setBeanName(@Nullable String beanName) {
		this.beanName = beanName;
	}

	/**
	 * Return the view's name. Should never be null,
	 * if the view was correctly configured.
	 */
	@Nullable
	public String getBeanName() {
		return this.beanName;
	}


	/**
	 * Prepares the view given the specified model, merging it with static
	 * attributes and a RequestContext attribute, if necessary.
	 * Delegates to renderMergedOutputModel for the actual rendering.
	 * @see #renderMergedOutputModel
     * 这里参数 model 是控制器返回的动态属性
	 */
	@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));
		}

       // 合并静态属性和动态属性到最终使用的数据模型  mergedModel
       // 静态属性和动态属性同名时,采用动态属性的值
		Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
		
        // 渲染数据模型到结果页面前准备响应对象
        prepareResponse(request, response);
       
       // 渲染数据模型到结果页面
		renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
	}

	/**
	 * Creates a combined output Map (never null) that includes dynamic values and static attributes.
	 * Dynamic values take precedence over static attributes.
	 */
	protected Map<String, Object> createMergedOutputModel(@Nullable Map<String, ?> model,
			HttpServletRequest request, HttpServletResponse response) {

       // 获取路径变量名称值对儿到  Map pathVars
		@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<String, Object> mergedModel = new LinkedHashMap<>(size);
		mergedModel.putAll(this.staticAttributes);
		if (pathVars != null) {
			mergedModel.putAll(pathVars);
		}
		if (model != null) {
			mergedModel.putAll(model);
		}

		// Expose RequestContext?
		if (this.requestContextAttribute != null) {
			mergedModel.put(this.requestContextAttribute, createRequestContext(request, response, mergedModel));
		}

		return mergedModel;
	}

	/**
	 * Create a RequestContext to expose under the specified attribute name.
	 * The default implementation creates a standard RequestContext instance for the
	 * given request and model. Can be overridden in subclasses for custom instances.
	 * @param request current HTTP request
	 * @param model combined output Map (never null),
	 * with dynamic values taking precedence over static attributes
	 * @return the RequestContext instance
	 * @see #setRequestContextAttribute
	 * @see org.springframework.web.servlet.support.RequestContext
	 */
	protected RequestContext createRequestContext(
			HttpServletRequest request, HttpServletResponse response, Map<String, Object> model) {

		return new RequestContext(request, response, getServletContext(), model);
	}

	/**
	 * Prepare the given response for rendering. 渲染视图前准备响应对象
	 * The default implementation applies a workaround for an IE bug
	 * when sending download content via HTTPS.
	 * @param request current HTTP request
	 * @param response current HTTP response
	 */
	protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) {
        // 缺省实现实际上什么都不做
        // 因为 generatesDownloadContent() 缺省返回 false
		if (generatesDownloadContent()) {
          // 如果当前是下载场景,添加如下头部  
			response.setHeader("Pragma", "private");
			response.setHeader("Cache-Control", "private, must-revalidate");
		}
	}

	/**
	 * Return whether this view generates download content
	 * (typically binary content like PDF or Excel files).
	 * The default implementation returns false. Subclasses are
	 * encouraged to return true here if they know that they are
	 * generating download content that requires temporary caching on the
	 * client side, typically via the response OutputStream.
	 * @see #prepareResponse
	 * @see javax.servlet.http.HttpServletResponse#getOutputStream()
	 */
	protected boolean generatesDownloadContent() {
		return false;
	}

	/**
	 * Get the request handle to expose to #renderMergedOutputModel, i.e. to the view.
	 * The default implementation wraps the original request for exposure of Spring beans
	 * as request attributes (if demanded).
	 * @param originalRequest the original servlet request as provided by the engine
	 * @return the wrapped request, or the original request if no wrapping is necessary
	 * @see #setExposeContextBeansAsAttributes
	 * @see #setExposedContextBeanNames
	 * @see org.springframework.web.context.support.ContextExposingHttpServletRequest
	 */
	protected HttpServletRequest getRequestToExpose(HttpServletRequest originalRequest) {
		if (this.exposeContextBeansAsAttributes || this.exposedContextBeanNames != null) {
			WebApplicationContext wac = getWebApplicationContext();
			Assert.state(wac != null, "No WebApplicationContext");
			return new ContextExposingHttpServletRequest(originalRequest, wac, this.exposedContextBeanNames);
		}
		return originalRequest;
	}

	/**
	 * 此方法为抽象方法,必须有抽象子类提供,这里的参数 model 是经过合并静态属性,动态属性,路径变量
     * 之后最重要应用到视图的数据模型,除非开发人员有其他自定义属性要渲染,否则直接使用该数据模型
     * 渲染视图即可
     * Subclasses must implement this method to actually render the view.
	 * 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 combined output Map (never null),
	 * with dynamic values taking precedence over static attributes
	 * @param request current HTTP request
	 * @param response current HTTP response
	 * @throws Exception if rendering failed
	 */
	protected abstract void renderMergedOutputModel(
			Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception;


	/**
	 * Expose the model objects in the given map as request attributes.
	 * Names will be taken from the model Map.
     * 工具方法 : 将指定数据模型中的属性添加为请求属性
	 * This method is suitable for all resources reachable by javax.servlet.RequestDispatcher.
	 * @param model a Map of model objects to expose
	 * @param request current HTTP request
	 */
	protected void exposeModelAsRequestAttributes(Map<String, Object> model,
			HttpServletRequest request) throws Exception {

		model.forEach((name, value) -> {
			if (value != null) {
				request.setAttribute(name, value);
			}
			else {
				request.removeAttribute(name);
			}
		});
	}

	/**
	 * 工具方法 : 为当前 View 创建一个临时的   OutputStream 对象
     * Create a temporary OutputStream for this view.
	 * This is typically used as IE workaround, for setting the content length header
	 * from the temporary stream before actually writing the content to the HTTP response.
	 */
	protected ByteArrayOutputStream createTemporaryOutputStream() {
		return new ByteArrayOutputStream(OUTPUT_BYTE_ARRAY_INITIAL_SIZE);
	}

	/**
	 * Write the given temporary OutputStream to the HTTP response.
     * 工具方法 : 将指定的临时 OutputStream baos 中的数据写入到 response
	 * @param response current HTTP response
	 * @param baos the temporary OutputStream to write
	 * @throws IOException if writing/flushing failed
	 */
	protected void writeToResponse(HttpServletResponse response, ByteArrayOutputStream baos) throws IOException {
		// Write content type and also length (determined via byte array).
		response.setContentType(getContentType());
		response.setContentLength(baos.size());

		// Flush byte array to servlet output stream.
		ServletOutputStream out = response.getOutputStream();
		baos.writeTo(out);
		out.flush();
	}

	/**
	 * 工具方法 : 结合考虑请求属性 View.SELECTED_CONTENT_TYPE 和 #getContentType 返回值设置响应的 Content Type
     * Set the content type of the response to the configured
	 * #setContentType(String) content type unless the
	 * View#SELECTED_CONTENT_TYPE request attribute is present and set
	 * to a concrete media type.
	 */
	protected void setResponseContentType(HttpServletRequest request, HttpServletResponse response) {
		MediaType mediaType = (MediaType) request.getAttribute(View.SELECTED_CONTENT_TYPE);
		if (mediaType != null && mediaType.isConcrete()) {
			response.setContentType(mediaType.toString());
		}
		else {
			response.setContentType(getContentType());
		}
	}

	@Override
	public String toString() {
		return getClass().getName() + ": " + formatViewName();
	}

	protected String formatViewName() {
		return (getBeanName() != null ? "name '" + getBeanName() + "'" : "[" + getClass().getSimpleName() + "]");
	}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值