前言
上篇文章已经重点讲解过了:ViewResolver视图解析器
【小家Spring】Spring MVC容器的web九大组件之—ViewResolver源码详解—视图解析器ViewResolver详解
SpringMVC用于处理视图最重要的两个接口是ViewResolver和View。ViewResolver的主要作用 是把一个逻辑上的视图名称解析为一个真正的视图,SpringMVC中用于把View对象呈现给客户端的 是View对象本身,而ViewResolver只是把逻辑视图名称解析为对象的View对象。View接口的主要 作用是用于处理视图,然后返回给客户端。
View
View是用于MVC交互的Web视图。实现负责呈现内容,并公开模型。单个视图可显示多个模型属性
视图实现可能差异很大,比如我们最基础的实现:JSP就是一种视图展示方式。当然还有后面的Jstl以及FreeMarker等。此接口旨在避免限制可能的实现范围
视图应该是bean(但不一定需要放进容器)。它们很可能被viewresolver实例化为bean。由于这个接口是无状态的,视图实现应该是线程安全的。
public interface View { // @since 3.0 // HttpStatus的key,可议根据此key去获取。备注:并不是每个视图都需要实现的。目前只有`RedirectView`有处理 String RESPONSE_STATUS_ATTRIBUTE = View.class.getName() + ".responseStatus"; // @since 3.1 也会这样去拿:request.getAttribute(View.PATH_VARIABLES) String PATH_VARIABLES = View.class.getName() + ".pathVariables"; // The {@link org.springframework.http.MediaType} selected during content negotiation // @since 3.2 // MediaType mediaType = (MediaType) request.getAttribute(View.SELECTED_CONTENT_TYPE) String SELECTED_CONTENT_TYPE = View.class.getName() + ".selectedContentType"; // Return the content type of the view, if predetermined(预定的) @Nullable default String getContentType() { return null; } // 这是最重要的 根据model里面的数据,request等 把渲染好的数据写进response里~ void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception; }
看看它的继承树:
可以看出来它只有两个分支:AbstractView和SmartView,而SmartView的唯一实现为:RedirectView并且它也继承自AbstractView。
我们可以粗略的认为:Spring MVC内置的所有的View都是AbstractView的子类
AbstractView
AbstractView实现了render方法,主要做的操作是将model中的参数和request中的参数全部都放到Request中,然后就转发Request就可以了
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; @Nullable private String requestContextAttribute; // "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. private final Map<String, Object> staticAttributes = new LinkedHashMap<>(); private boolean exposePathVariables = true; private boolean exposeContextBeansAsAttributes = false; @Nullable private Set<String> exposedContextBeanNames; @Nullable private String beanName; // 把你传进俩的Properties 都合并进来~~~ public void setAttributes(Properties attributes) { CollectionUtils.mergePropertiesIntoMap(attributes, this.staticAttributes); } ... @Override public void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { // 合并staticAttributes、pathVars、model数据到一个Map里来 // 其中:后者覆盖前者的值(若有相同key的话~~)也就是所谓的model的值优先级最高~~~~ // 最终还会暴露RequestContext对象到Model里,因此model里可以直接访问RequestContext对象哦~~~~ Map<String, Object> mergedModel = createMergedOutputModel(model, request, response); // 默认实现为设置几个响应头~~~ // 备注:默认情况下pdf的view、xstl的view会触发下载~~~ prepareResponse(request, response); // getRequestToExpose表示吧request暴露成:ContextExposingHttpServletRequest(和容器相关,以及容器内的BeanNames) // renderMergedOutputModel是个抽象方法 由子类去实现~~~~ renderMergedOutputModel(mergedModel, getRequestToExpose(request), response); } //================下面是一些方法,父类提供 子类可以直接使用的方法============== // 一个temp输出流,缓冲区大小为4096 字节流 protected ByteArrayOutputStream createTemporaryOutputStream() { return new ByteArrayOutputStream(OUTPUT_BYTE_ARRAY_INITIAL_SIZE); } // 把字节流写进response里面~~~ 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(); } // 相当于如果request.getAttribute(View.SELECTED_CONTENT_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()); } } ... }
该抽象类主要是提供了对render方法的模版实现,以及提供一些基础方法供给子类来使用,比如createTemporaryOutputStream()等等
AbstractJackson2View
这个是一个比较新的Viw(@since 4.1),它是基于Jackson渲染的视图。
//@since 4.1 // Compatible with Jackson 2.6 and higher, as of Spring 4.3. public abstract class AbstractJackson2View extends AbstractView { private ObjectMapper objectMapper; private JsonEncoding encoding = JsonEncoding.UTF8; @Nullable private Boolean prettyPrint; private boolean disableCaching = true; protected boolean updateContentLength = false; // 唯一构造函数,并且还是protected的~~ protected AbstractJackson2View(ObjectMapper objectMapper, String contentType) { this.objectMapper = objectMapper; configurePrettyPrint(); setContentType(contentType); setExposePathVariables(false); } ... // get/set方法 // 复写了父类的此方法~~~ setResponseContentType是父类的哟~~~~ @Override protected void prepareResponse(HttpServletRequest request, HttpServletResponse response) { setResponseContentType(request, response); // 设置编码格式,默认是UTF-8 response.setCharacterEncoding(this.encoding.getJavaName()); if (this.disableCaching) { response.addHeader("Cache-Control", "no-store"); } } // 实现了父类的渲染方法~~~~ @Override protected void renderMergedOutputModel(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception { ByteArrayOutputStream temporaryStream = null; OutputStream stream; // 注意此处:updateContentLength默认值是false 所以会直接从response里面吧输出流拿出来 而不用temp流 if (this.updateContentLength) { temporaryStream = createTemporaryOutputStream(); stream = temporaryStream; } else { stream = response.getOutputStream(); } Object value = filterAndWrapModel(model, request); // value是最终的从model中出来的~~~~这里就是把value值写进去~~~~ // 先通过stream得到一个JsonGenerator,然后先writePrefix(generator, object) // 然后objectMapper.writerWithView // 最后writeSuffix(generator, object); 然后flush即可~ writeContent(stream, value); if (temporaryStream != null) { writeToResponse(response, temporaryStream); } } // 筛选Model并可选地将其包装在@link mappingjacksonvalue容器中 protected Object filterAndWrapModel(Map<String, Object> model, HttpServletRequest request) { // filterModel抽象方法,从指定的model中筛选出不需要的属性值~~~~~ Object value = filterModel(model); // 把这两个属性值,选择性的放进container容器里面 最终返回~~~~ Class<?> serializationView = (Class<?>) model.get(JsonView.class.getName()); FilterProvider filters = (FilterProvider) model.get(FilterProvider.class.getName()); if (serializationView != null || filters != null) { MappingJacksonValue container = new MappingJacksonValue(value); if (serializationView != null) { container.setSerializationView(serializationView); } if (filters != null) { container.setFilters(filters); } value = container; } return value; } }
MappingJackson2JsonView
它是用于返回Json视图的(下面会介绍Spring MVC返回json的三种方式)
// @since 3.1.2 可议看到它出现得还是比较早的~ public class MappingJackson2JsonView extends AbstractJackson2View { public static final String DEFAULT_CONTENT_TYPE = "application/json"; @Nullable private String jsonPrefix; @Nullable private Set<String> modelKeys; private boolean extractValueFromSingleKeyModel = false; @Override protected Object filterModel(Map<String, Object> model) { Map<String, Object> result = new HashMap<>(model.size()); Set<String> modelKeys = (!CollectionUtils.isEmpty(this.modelKeys) ? this.modelKeys : model.keySet()); // 遍历model所有内容~ model.forEach((clazz, value) -> { // 符合下列条件的会给排除掉~~~ // 不是BindingResult类型 并且 modelKeys包含此key 并且此key不是JsonView和FilterProvider 这种key就排除掉~~~ if (!(value instanceof BindingResult) && modelKeys.contains(clazz) && !clazz.equals(JsonView.class.getName()) && !clazz.equals(FilterProvider.class.getName())) { result.put(clazz, value); } }); // 如果只需要排除singleKey,那就返回第一个即可,否则result全部返回 return (this.extractValueFromSingleKeyModel && result.size() == 1 ? result.values().iterator().next() : result); } // 如果配置了前缀,把前缀写进去~~~ @Override protected void writePrefix(JsonGenerator generator, Object object) throws IOException { if (this.jsonPrefix != null) { generator.writeRaw(this.jsonPrefix); } } }
此视图是专门来处理作为一个json视图格式进行返回的。那么接下里有必要举例说明一下,Spring MVC返回Json格式数据的多种方式:
Spring MVC返回json的三种方式
- 使用MappingJackson2JsonView,其实它是相对来说比较新的一种返回json数据的放置,主要是用到了这个视图的能力。
直接使用它相对来说还是比较麻烦点的,一般都需要结合内容协商视图解析器来使用(比如把它设置默认处理json的视图),但是本文就做一个Demo,所以还是简单的处理一下吧:使用BeanNameViewResolver执行我们定义的这个视图去即可:
@RequestMapping(value = "/json") public String testView(Model model) { // 注意Model不添加数据,将会是一个空的JSON串 model.addAttribute("name", "fsx"); model.addAttribute("age", 18); return "mappingJackson2JsonView"; } // 配置视图: @Configuration @EnableWebMvc public class WebMvcConfig extends WebMvcConfigurerAdapter { // 此处为配置了一个前缀,发现前缀可以解决jsonp的问题~~~ @Bean public MappingJackson2JsonView mappingJackson2JsonView() { MappingJackson2JsonView mappingJackson2JsonView = new MappingJackson2JsonView(); mappingJackson2JsonView.setJsonPrefix("prefix"); return mappingJackson2JsonView; } @Override public void configureViewResolvers(ViewResolverRegistry registry) { BeanNameViewResolver viewResolver = new BeanNameViewResolver(); viewResolver.setOrder(10); // 这样能保证在InternalResourceViewResolver之前执行 registry.viewResolver(viewResolver); } }
浏览器访问:http://localhost:8080/demo_war_war/json可看到如下:
它提供的前缀能力,在某些特殊的场景会有用
- 利用
HttpServletResponse
,然后获取response.getOutputStream()
或response.getWriter()
自己写json串
@RequestMapping(value = "/json") public void testView(PrintWriter printWriter) { printWriter.write("{\"name\":\"fsx\",\"age\":18}"); }
显然这种方式最为原始的方式,一般情况下我是不推荐这么使用的~
插一句:我曾经看到过有项目在使用Spring MVC框架的时候,还大量的使用到了Servlet规范的东西,其实这个真的是非常非常不好的做法~~
3.@ResponseBody这种方式是当下平时我们书写使用最多的方式
相信这种方式我说一个字:“略”,应该没有人有意见吧~~~