ContentNegotiation内容协商机制(三)---在视图View上的应用:ContentNegotiatingViewResolver深度解析【享学Spring MVC】(上)

简介: ContentNegotiation内容协商机制(三)---在视图View上的应用:ContentNegotiatingViewResolver深度解析【享学Spring MVC】(上)

前言


在经过 前两篇 文章了解了Spring MVC的内容协商机制之后,相信你已经能够熟练的运用Spring MVC提供的这项能力,配合RESTful发挥它的功效了。这其实也就达到了我们目的的80%,也达到了我书写这块知识点的目的。


为何说是80%呢?因为我认为在前后端完全分离的今天,绝大部分使用场景都是这种情况,完成了覆盖。

为何还有20%呢?因为内容协商不仅仅可以使用在HttpMessage上,还可以使用在View视图上,这也就是本文想重点补充的内容。


内容协商在HttpMessage上的应用


前两篇文章的示例都是基于此。在讲解原理的时候提到:处理的入口在AbstractMessageConverterMethodProcessor.writeWithMessageConverters()方法上,看此抽象类的子类也能看出端倪:


image.png


从子类实现中你也能够明白:它和HttpMessage是强相关的,都是经过了HttpMessageConverter处理的消息来做内容协商。


这两个实现类处理的也就是我们当下最为常用的注解:@ResponseBody。或者返回值直接是HttpEntity/ResponseEntity类型(也就是不能是RequestEntity就成)


毫无疑问,基于@ResponseBody的Rest接口方式在前后端完全分离的今天已然是主流方式,因此我说前两篇文章覆盖了80%的场景应该不为过吧~


我搜索到ContentNegotiationManager.resolveMediaTypes()方法在ContentNegotiatingViewResolver里也使用到了,因此我自然而然的联想到了内容协商也能结合视图解析器一起使用~


内容协商在视图View上的应用


由于前面我给的示例都是基于Http消息的,没有视图可言。本文此处需要讲解的是内容协商在视图解析方面的应用:同一个URL,以不同的视图作为展示方式。


我们已经知道了:RequestMappingInfoHandlerMapping(@RequestMapping)它在对带有后缀的http请求进行匹配的时候,如果找不到精确的pattern, 那么就会pattern+.*后再匹配 url,它会处理多个不同形式是 url,但是返回的是同一个View。本文就教你用一个@RequestMapping也能返回多个View~


注意:我这里指的是返回的是View视图,对于消息体的这种返回方式,不是本处讨论的范畴,它属于case 1。


视图解析器ViewResolver


关于视图的内容,可参见这里:View

关于视图解析器的内容,可参见这里:ViewResolver


本文简单的再“复习”一下Spring MVC对视图解析器的使用流程:


使用处:DispatcherServlet.resolveViewName()

得到逻辑视图后,通过已经注册好的视图解析器ViewResolver把逻辑视图解析为真正的视图View


DispatcherServlet:
  @Nullable
  protected View resolveViewName(String viewName, @Nullable Map<String, Object> model, Locale locale, HttpServletRequest request) throws Exception {
    if (this.viewResolvers != null) {
      // 按照顺序:一个一个执行。第一个最先解析到不返回null的  就是最终返回的view视图
      for (ViewResolver viewResolver : this.viewResolvers) {
        View view = viewResolver.resolveViewName(viewName, locale);
        if (view != null) {
          return view;
        }
      }
    }
    return null;
  }


加载处:DispatcherServlet.initViewResolvers()

这个在讲解Spring MVC九大组件加载时详细说过


DispatcherServlet:
  private void initViewResolvers(ApplicationContext context) {
    // 1、若detectAllViewResolvers=true,去容器中找到所有的ViewResolver Bean们。排序后返回
    // 2、若不是探测全部。就只找BeanName=viewResolver它的这一个Bean
    // 2、若一个都没有找到,就走默认策略:从DispatcherServlet.properties里配置的读取默认的配置
  }


这个查找策略,对我们合理注册、管理视图解析器都是很有用的,可以稍加留意


声明处:WebMvcConfigurationSupport.mvcViewResolver()


WebMvcConfigurationSupport:
  // @since 4.1 向容器注册一个ViewResolver Bean  
  // 使用的是容器管理方式:ViewResolverComposite 
  @Bean
  public ViewResolver mvcViewResolver() {
    // mvcContentNegotiationManager:内容协商管理器(本文重点之一)
    ViewResolverRegistry registry = new ViewResolverRegistry(mvcContentNegotiationManager(), this.applicationContext);
    // protected方法,回调给我们调用者,允许自定义ViewResolverRegistry 
    configureViewResolvers(registry);
    // 它的意思是:如果你没有自定义(或者自定义了但一个解析器都木有)
    // 那就主动去容器里找。如果仅仅仅仅只知道一个:那它就是InternalResourceViewResolver(注意此处是new的)
    // 注意此处的处理方式哦~~~~
    if (registry.getViewResolvers().isEmpty() && this.applicationContext != null) {
      String[] names = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.applicationContext, ViewResolver.class, true, false);
      if (names.length == 1) {
        registry.getViewResolvers().add(new InternalResourceViewResolver());
      }
    }
    // 最终使用ViewResolverComposite把这些(多个)装起来,便于管理~
    ViewResolverComposite composite = new ViewResolverComposite();
    composite.setOrder(registry.getOrder());
    composite.setViewResolvers(registry.getViewResolvers());
    if (this.applicationContext != null) {
      composite.setApplicationContext(this.applicationContext);
    }
    if (this.servletContext != null) {
      composite.setServletContext(this.servletContext);
    }
    return composite;
  }


这里我们能发现,它默认情况下使用的是我们上文说的默认的ContentNegotiationManager来处理内容协商的。因此下面重点要来到今天的主角ContentNegotiatingViewResolver身上


ContentNegotiatingViewResolver:内容协商视图解析器

ContentNagotiatingViewResolver自己并不解析视图,而是委派给其他的视图处理器。


为了使这个解析器正常工作,order序号需要设置成比其他的视图处理器高的优先级(默认就是最高的)


// @since 3.0
public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport implements ViewResolver, Ordered, InitializingBean {
  // 用于内容协商的管理器
  @Nullable
  private ContentNegotiationManager contentNegotiationManager;
  private final ContentNegotiationManagerFactoryBean cnmFactoryBean = new ContentNegotiationManagerFactoryBean();
  // 如果没有合适的view的时候,是否使用406这个状态码(HttpServletResponse#SC_NOT_ACCEPTABLE)
  // 默认值是false:表示没有找到就返回null,而不是406
  private boolean useNotAcceptableStatusCode = false;
  // 当无法获取到具体的视图时,会走defaultViews
  @Nullable
  private List<View> defaultViews;
  @Nullable
  private List<ViewResolver> viewResolvers;
  private int order = Ordered.HIGHEST_PRECEDENCE; // 默认,优先级就是最高的
  // 复写:WebApplicationObjectSupport的方法
  // 它在setServletContext和initApplicationContext会调用(也就是容器启动时候会调用)
  @Override
  protected void initServletContext(ServletContext servletContext) {
    Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
    //容器内找到了  就以容器内所有已经配置好的视图解析器都拿出来(包含父容器)
    if (this.viewResolvers == null) {
      this.viewResolvers = new ArrayList<>(matchingBeans.size());
      for (ViewResolver viewResolver : matchingBeans) {
        if (this != viewResolver) { // 排除自己
          this.viewResolvers.add(viewResolver);
        }
      }
    } else { // 进入这里证明是调用者自己set进来的
      for (int i = 0; i < this.viewResolvers.size(); i++) {
        ViewResolver vr = this.viewResolvers.get(i);
        if (matchingBeans.contains(vr)) {
          continue;
        }
        String name = vr.getClass().getName() + i;
        // 对视图解析器完成初始化工作~~~~~
        // 关于AutowireCapableBeanFactory的使用,参见:https://blog.csdn.net/f641385712/article/details/88651128
        obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
      }
    }
    // 找到所有的ViewResolvers排序后,放进ContentNegotiationManagerFactoryBean里
    AnnotationAwareOrderComparator.sort(this.viewResolvers);
    this.cnmFactoryBean.setServletContext(servletContext);
  }
  // 从这一步骤可以知道:contentNegotiationManager 可以自己set
  // 也可以通过工厂来生成  两种方式均可
  @Override
  public void afterPropertiesSet() {
    if (this.contentNegotiationManager == null) {
      this.contentNegotiationManager = this.cnmFactoryBean.build();
    }
    if (this.viewResolvers == null || this.viewResolvers.isEmpty()) {
      logger.warn("No ViewResolvers configured");
    }
  }
  // 处理逻辑视图到View 在此处会进行内容协商
  @Override
  @Nullable
  public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    // getMediaTypes()这个方法完成了
    // 1、通过contentNegotiationManager.resolveMediaTypes(webRequest)得到请求的MediaTypes
    // 2、拿到服务端能够提供的MediaTypes  producibleMediaTypes
    // (请注意因为没有消息转换器,所以它的值的唯一来源是:request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE))
    // (若没有指定producers的值,那就是ALL)
    // 3、按照优先级,协商出`selectedMediaTypes`(是个List)
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    // 进入此处:说明协商出了有可用的MediaTypes(至少有一个嘛)
    if (requestedMediaTypes != null) {
      // getCandidateViews()这个很重要的方法,见下文
      List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
      // 上面一步骤解析出了多个符合条件的views,这里就是通过MediaType、attrs等等一起决定出一个,一个,一个最佳的
      // getBestView()方法描述如下:
      // 第一大步:遍历所有的candidateViews,只要是smartView.isRedirectView(),就直接return
      // 第二大步:遍历所有的requestedMediaTypes,针对每一种MediaType下再遍历所有的candidateViews
      // 1、针对每一种MediaType,拿出View.getContentType(),只会看这个值不为null的
      // 2、view的contentType!=null,继续看看mediaType.isCompatibleWith(candidateContentType) 若不匹配这个视图就略过
      // 3、若匹配:attrs.setAttribute(View.SELECTED_CONTENT_TYPE, mediaType, RequestAttributes.SCOPE_REQUEST)  然后return掉此视图作为best最佳的
      View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
      if (bestView != null) { // 很显然,找到了最佳的就返回渲染吧
        return bestView;
      }
    }
    ... 
    // useNotAcceptableStatusCode=true没找到视图就返回406
    // NOT_ACCEPTABLE_VIEW是个private内部静态类View,它的render方法只有一句话:
    // response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
    if (this.useNotAcceptableStatusCode) {
      return NOT_ACCEPTABLE_VIEW;
    } else {
      return null;
    }
  }
  // 根据viewName、requestedMediaTypes等等去得到所有的备选的Views~~
  // 这这里会调用所有的viewResolvers.resolveViewName()来分别处理~~~所以可能生成多多个viewo ~
  private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes) throws Exception {
    List<View> candidateViews = new ArrayList<>();
    if (this.viewResolvers != null) {
      Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
      // 遍历所有的viewResolvers,多逻辑视图一个一个的处理
      for (ViewResolver viewResolver : this.viewResolvers) {
        View view = viewResolver.resolveViewName(viewName, locale);
        if (view != null) {
          candidateViews.add(view); // 处理好的就装进来
        }
        // 另外还没有完:遍历所有支持的MediaType,拿到它对应的扩展名们(一个MediaType可以对应多个扩展名)
        // 如果viewName + '.' + extension能被处理成一个视图,也是ok的
        // 也就是说index和index.jsp都能被解析成view视图~~~
        for (MediaType requestedMediaType : requestedMediaTypes) {
          // resolveFileExtensions()方法可以说这里是唯一调用的地方
          List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
          for (String extension : extensions) {
            String viewNameWithExtension = viewName + '.' + extension;
            view = viewResolver.resolveViewName(viewNameWithExtension, locale);
            if (view != null) {
              candidateViews.add(view); // 带上后缀名也能够处理的  这种视图也ok
            }
          }
        }
      }
    }
    // 若指定了默认视图,把视图也得加上(在最后面哦~)
    if (!CollectionUtils.isEmpty(this.defaultViews)) {
      candidateViews.addAll(this.defaultViews);
    }
    return candidateViews;
  }
}


关于ContentNegotiatingViewResolver我总结出如下细节要点:


  1. ContentNegotiationManager用于内容协商的策略可以手动set指定,也可以通过FactoryBean自动生成
  2. viewResolvers默认是去容器内找到所有的,当然你也可以手动set进来的~
  3. 使用request的媒体类型,根据扩展名选择不同的view输出不同的格式
  4. 不是自己处理view,而是代理给不同的ViewResolver来处理不同的view;
  5. 默认是支持Accept和后缀的协商方式的。并且还支持 逻辑视图名.后缀的视图解析方式~
  6. 依据View.getContentType匹配MediaType来完成的最佳匹配
相关文章
|
3月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
133 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
3月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
168 0
|
3月前
|
前端开发 Java 微服务
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@PathVariable
`@PathVariable` 是 Spring Boot 中用于从 URL 中提取参数的注解,支持 RESTful 风格接口开发。例如,通过 `@GetMapping(&quot;/user/{id}&quot;)` 可以将 URL 中的 `{id}` 参数自动映射到方法参数中。若参数名不一致,可通过 `@PathVariable(&quot;自定义名&quot;)` 指定绑定关系。此外,还支持多参数占位符,如 `/user/{id}/{name}`,分别映射到方法中的多个参数。运行项目后,访问指定 URL 即可验证参数是否正确接收。
106 0
|
3月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestMapping
@RequestMapping 是 Spring MVC 中用于请求地址映射的注解,可作用于类或方法上。类级别定义控制器父路径,方法级别进一步指定处理逻辑。常用属性包括 value(请求地址)、method(请求类型,如 GET/POST 等,默认 GET)和 produces(返回内容类型)。例如:`@RequestMapping(value = &quot;/test&quot;, produces = &quot;application/json; charset=UTF-8&quot;)`。此外,针对不同请求方式还有简化注解,如 @GetMapping、@PostMapping 等。
133 0
|
3月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RestController
本文主要介绍 Spring Boot 中 MVC 开发常用的几个注解及其使用方式,包括 `@RestController`、`@RequestMapping`、`@PathVariable`、`@RequestParam` 和 `@RequestBody`。其中重点讲解了 `@RestController` 注解的构成与特点:它是 `@Controller` 和 `@ResponseBody` 的结合体,适用于返回 JSON 数据的场景。文章还指出,在需要模板渲染(如 Thymeleaf)而非前后端分离的情况下,应使用 `@Controller` 而非 `@RestController`
122 0
|
4月前
|
XML Java Maven
Spring 手动实现Spring底层机制
Spring 第六节 手动实现Spring底层机制 万字详解!
130 31
|
5月前
|
SQL Java 数据库连接
对Spring、SpringMVC、MyBatis框架的介绍与解释
Spring 框架提供了全面的基础设施支持,Spring MVC 专注于 Web 层的开发,而 MyBatis 则是一个高效的持久层框架。这三个框架结合使用,可以显著提升 Java 企业级应用的开发效率和质量。通过理解它们的核心特性和使用方法,开发者可以更好地构建和维护复杂的应用程序。
256 29
|
3月前
|
算法 测试技术 C语言
深入理解HTTP/2:nghttp2库源码解析及客户端实现示例
通过解析nghttp2库的源码和实现一个简单的HTTP/2客户端示例,本文详细介绍了HTTP/2的关键特性和nghttp2的核心实现。了解这些内容可以帮助开发者更好地理解HTTP/2协议,提高Web应用的性能和用户体验。对于实际开发中的应用,可以根据需要进一步优化和扩展代码,以满足具体需求。
353 29
|
3月前
|
前端开发 数据安全/隐私保护 CDN
二次元聚合短视频解析去水印系统源码
二次元聚合短视频解析去水印系统源码
107 4
|
3月前
|
JavaScript 算法 前端开发
JS数组操作方法全景图,全网最全构建完整知识网络!js数组操作方法全集(实现筛选转换、随机排序洗牌算法、复杂数据处理统计等情景详解,附大量源码和易错点解析)
这些方法提供了对数组的全面操作,包括搜索、遍历、转换和聚合等。通过分为原地操作方法、非原地操作方法和其他方法便于您理解和记忆,并熟悉他们各自的使用方法与使用范围。详细的案例与进阶使用,方便您理解数组操作的底层原理。链式调用的几个案例,让您玩转数组操作。 只有锻炼思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~

推荐镜像

更多
  • DNS