【小家Spring】Spring MVC容器的web九大组件之---ViewResolver源码详解---视图View详解(上)

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家Spring】Spring MVC容器的web九大组件之---ViewResolver源码详解---视图View详解(上)

前言


上篇文章已经重点讲解过了: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;
}


看看它的继承树:


image.png


可以看出来它只有两个分支: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的三种方式


  1. 使用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可看到如下:


image.png


它提供的前缀能力,在某些特殊的场景会有用


  1. 利用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这种方式是当下平时我们书写使用最多的方式

相信这种方式我说一个字:“略”,应该没有人有意见吧~~~

相关文章
|
1月前
|
存储 设计模式 前端开发
请解释 Web 应用程序的 MVC(模型-视图-控制器)架构。
【2月更文挑战第26天】【2月更文挑战第89篇】请解释 Web 应用程序的 MVC(模型-视图-控制器)架构。
|
1月前
|
Java 容器 Spring
【spring(一)】核心容器总结
【spring(一)】核心容器总结
|
1月前
|
Java 开发者 容器
【Java】深入了解Spring容器的两个关键组件
【Java】深入了解Spring容器的两个关键组件
10 0
|
1月前
|
XML Java 数据格式
Spring 的奇幻起源:从 IoC 容器到 Bean 的魔法世界 (下)
Spring 的奇幻起源:从 IoC 容器到 Bean 的魔法世界
|
1月前
|
XML Java 数据格式
Spring 的奇幻起源:从 IoC 容器到 Bean 的魔法世界 (上)
Spring 的奇幻起源:从 IoC 容器到 Bean 的魔法世界 (上)
|
2月前
|
前端开发 Java 数据格式
10个知识点让你读懂spring MVC容器
随着 Spring Boot 逐步全面覆盖到我们的项目之中,我们已经基本忘却当年经典的 Servlet + Spring MVC 的组合,那让人熟悉的 web.xml 配置。而本文,我们想先抛开 Spring Boot 到一旁,回到从前,一起来看看 Servlet 是怎么和 Spring MVC 集成,怎么来初始化 Spring 容器的。
20 1
|
6月前
|
XML 缓存 前端开发
Spring MVC视图解析器
Spring MVC视图解析器
50 1
|
8月前
|
前端开发 Java Maven
Spring Boot入门(十二) 之 前端静态资源的引入 (webjars 以及 thymeleaf 视图解析器)
Spring Boot入门(十二) 之 前端静态资源的引入 (webjars 以及 thymeleaf 视图解析器)
135 0
|
前端开发 Java API
Spring MVC框架:第二章:视图解析器和@RequestMapping注解使用在类级别及获取原生Servlet API对象
Spring MVC框架:第二章:视图解析器和@RequestMapping注解使用在类级别及获取原生Servlet API对象
239 0
|
前端开发 Java Spring
【小家Spring】Spring MVC容器的web九大组件之---ViewResolver源码详解---视图解析器ViewResolver详解(下)
【小家Spring】Spring MVC容器的web九大组件之---ViewResolver源码详解---视图解析器ViewResolver详解(下)
【小家Spring】Spring MVC容器的web九大组件之---ViewResolver源码详解---视图解析器ViewResolver详解(下)