概述
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
FreeMarkerView
–FreeMarker
视图渲染TilesView
–Tiles
视图渲染InternalResourceView
JstlView
–JSP
视图渲染
- (+
SmartView
==> )RedirectView
– 重定向 XsltView
GroovyMarkupView
AbstractPdfStamperView
ScriptTemplateView
MarshallingView
AbstractJackson2View
MappingJackson2JsonView
AbstractFeedView
AbstractRssFeedView
AbstractAtomFeedView
AbstractXlsView
AbstractXlsxView
AbstractXlsxStreamingView
AbstractPdfView
–PDF
文档生成
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() + "]");
}
}