ContentNegotiation内容协商机制(二)---Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】(上)

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: ContentNegotiation内容协商机制(二)---Spring MVC内容协商实现原理及自定义配置【享学Spring MVC】(上)

前言


上文 介绍了Http内容协商的一些概念,以及Spring MVC内置的4种协商方式使用介绍。本文主要针对Spring MVC内容协商方式:从步骤、原理层面理解,最后达到通过自己来扩展协商方式效果。


首先肯定需要介绍的,那必然就是Spring MVC的默认支持的四大协商策略的原理分析喽:


ContentNegotiationStrategy


该接口就是Spring MVC实现内容协商的策略接口:


// A strategy for resolving the requested media types for a request.
// @since 3.2
@FunctionalInterface
public interface ContentNegotiationStrategy {
  // @since 5.0.5
  List<MediaType> MEDIA_TYPE_ALL_LIST = Collections.singletonList(MediaType.ALL);
  // 将给定的请求解析为媒体类型列表
  // 返回的 List 首先按照 specificity 参数排序,其次按照 quality 参数排序
  // 如果请求的媒体类型不能被解析则抛出 HttpMediaTypeNotAcceptableException 异常
  List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException;
}


说白了,这个策略接口就是想知道客户端的请求需要什么类型(MediaType)的数据List。从 上文我们知道Spring MVC它支持了4种不同的协商机制,它都和此策略接口相关的。

它的继承树:


image.png


从实现类的名字上就能看出它和上文提到的4种方式恰好是一一对应着的(ContentNegotiationManager除外)。


Spring MVC默认加载两个该策略接口的实现类:

ServletPathExtensionContentNegotiationStrategy–>根据文件扩展名(支持RESTful)。

HeaderContentNegotiationStrategy–>根据HTTP Header里的Accept字段(支持Http)。


HeaderContentNegotiationStrategy


Accept Header解析:它根据请求头Accept来协商。

public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {
  @Override
  public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
    // 我的Chrome浏览器值是:[text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3]
    // postman的值是:[*/*]
    String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT);
    if (headerValueArray == null) {
      return MEDIA_TYPE_ALL_LIST;
    }
    List<String> headerValues = Arrays.asList(headerValueArray);
    try {
      List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
      // 排序
      MediaType.sortBySpecificityAndQuality(mediaTypes);
      // 最后Chrome浏览器的List如下:
      // 0 = {MediaType@6205} "text/html"
      // 1 = {MediaType@6206} "application/xhtml+xml"
      // 2 = {MediaType@6207} "image/webp"
      // 3 = {MediaType@6208} "image/apng"
      // 4 = {MediaType@6209} "application/signed-exchange;v=b3"
      // 5 = {MediaType@6210} "application/xml;q=0.9"
      // 6 = {MediaType@6211} "*/*;q=0.8"
      return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
    } catch (InvalidMediaTypeException ex) {
      throw new HttpMediaTypeNotAcceptableException("Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
    }
  }
}


可以看到,如果没有传递Accept,则默认使用MediaType.ALL 也就是*/*


AbstractMappingContentNegotiationStrategy


通过file extension/query param来协商的抽象实现类。在了解它之前,有必要先插队先了解MediaTypeFileExtensionResolver它的作用:


MediaTypeFileExtensionResolver:MediaType和路径扩展名解析策略的接口,例如将 .json 解析成 application/json 或者反向解析

// @since 3.2
public interface MediaTypeFileExtensionResolver {
  // 根据指定的mediaType返回一组文件扩展名
  List<String> resolveFileExtensions(MediaType mediaType);
  // 返回该接口注册进来的所有的扩展名
  List<String> getAllFileExtensions();
}


继承树如下:


image.png


显然,本处只需要讲解它的直接实现子类MappingMediaTypeFileExtensionResolver即可:


MappingMediaTypeFileExtensionResolver


public class MappingMediaTypeFileExtensionResolver implements MediaTypeFileExtensionResolver {
  // key是lowerCaseExtension,value是对应的mediaType
  private final ConcurrentMap<String, MediaType> mediaTypes = new ConcurrentHashMap<>(64);
  // 和上面相反,key是mediaType,value是lowerCaseExtension(显然用的是多值map)
  private final MultiValueMap<MediaType, String> fileExtensions = new LinkedMultiValueMap<>();
  // 所有的扩展名(List非set哦~)
  private final List<String> allFileExtensions = new ArrayList<>();
  ...
  public Map<String, MediaType> getMediaTypes() {
    return this.mediaTypes;
  }
  // protected 方法
  protected List<MediaType> getAllMediaTypes() {
    return new ArrayList<>(this.mediaTypes.values());
  }
  // 给extension添加一个对应的mediaType
  // 采用ConcurrentMap是为了避免出现并发情况下导致的一致性问题
  protected void addMapping(String extension, MediaType mediaType) {
    MediaType previous = this.mediaTypes.putIfAbsent(extension, mediaType);
    if (previous == null) {
      this.fileExtensions.add(mediaType, extension);
      this.allFileExtensions.add(extension);
    }
  }
  // 接口方法:拿到指定的mediaType对应的扩展名们~
  @Override
  public List<String> resolveFileExtensions(MediaType mediaType) {
    List<String> fileExtensions = this.fileExtensions.get(mediaType);
    return (fileExtensions != null ? fileExtensions : Collections.emptyList());
  }
  @Override
  public List<String> getAllFileExtensions() {
    return Collections.unmodifiableList(this.allFileExtensions);
  }
  // protected 方法:根据扩展名找到一个MediaType~(当然可能是找不到的)
  @Nullable
  protected MediaType lookupMediaType(String extension) {
    return this.mediaTypes.get(extension.toLowerCase(Locale.ENGLISH));
  }
}


此抽象类维护一些Map以及提供操作的方法,它维护了一个文件扩展名和MediaType的双向查找表。扩展名和MediaType的对应关系:


  1. 一个MediaType对应N个扩展名
  2. 一个扩展名最多只会属于一个MediaType~


继续回到AbstractMappingContentNegotiationStrategy。


// @since 3.2 它是个协商策略抽象实现,同时也有了扩展名+MediaType对应关系的能力
public abstract class AbstractMappingContentNegotiationStrategy extends MappingMediaTypeFileExtensionResolver implements ContentNegotiationStrategy {
  // Whether to only use the registered mappings to look up file extensions,
  // or also to use dynamic resolution (e.g. via {@link MediaTypeFactory}.
  // org.springframework.http.MediaTypeFactory是Spring5.0提供的一个工厂类
  // 它会读取/org/springframework/http/mime.types这个文件,里面有记录着对应关系
  private boolean useRegisteredExtensionsOnly = false;
  // Whether to ignore requests with unknown file extension. Setting this to
  // 默认false:若认识不认识的扩展名,抛出异常:HttpMediaTypeNotAcceptableException
  private boolean ignoreUnknownExtensions = false;
  // 唯一构造函数
  public AbstractMappingContentNegotiationStrategy(@Nullable Map<String, MediaType> mediaTypes) {
    super(mediaTypes);
  }
  // 实现策略接口方法
  @Override
  public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
    // getMediaTypeKey:抽象方法(让子类把扩展名这个key提供出来)
    return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));
  }
  public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key) throws HttpMediaTypeNotAcceptableException {
    if (StringUtils.hasText(key)) {
      // 调用父类方法:根据key去查找出一个MediaType出来
      MediaType mediaType = lookupMediaType(key); 
      // 找到了就return就成(handleMatch是protected的空方法~~~  子类目前没有实现的)
      if (mediaType != null) {
        handleMatch(key, mediaType); // 回调
        return Collections.singletonList(mediaType);
      }
      // 若没有对应的MediaType,交给handleNoMatch处理(默认是抛出异常,见下面)
      // 注意:handleNoMatch如果通过工厂找到了,那就addMapping()保存起来(相当于注册上去)
      mediaType = handleNoMatch(webRequest, key);
      if (mediaType != null) {
        addMapping(key, mediaType);
        return Collections.singletonList(mediaType);
      }
    }
    return MEDIA_TYPE_ALL_LIST; // 默认值:所有
  }
  // 此方法子类ServletPathExtensionContentNegotiationStrategy有复写
  @Nullable
  protected MediaType handleNoMatch(NativeWebRequest request, String key) throws HttpMediaTypeNotAcceptableException {
    // 若不是仅仅从注册里的拿,那就再去MediaTypeFactory里看看~~~  找到了就返回
    if (!isUseRegisteredExtensionsOnly()) {
      Optional<MediaType> mediaType = MediaTypeFactory.getMediaType("file." + key);
      if (mediaType.isPresent()) {
        return mediaType.get();
      }
    }
    // 忽略找不到,返回null吧  否则抛出异常:HttpMediaTypeNotAcceptableException
    if (isIgnoreUnknownExtensions()) {
      return null;
    }
    throw new HttpMediaTypeNotAcceptableException(getAllMediaTypes());
  }
}


该抽象类实现了模版处理流程。

由子类去决定:你的扩展名是来自于URL的参数还是来自于path…




相关文章
|
8天前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
28 2
|
9天前
|
Java 开发者 微服务
手写模拟Spring Boot自动配置功能
【11月更文挑战第19天】随着微服务架构的兴起,Spring Boot作为一种快速开发框架,因其简化了Spring应用的初始搭建和开发过程,受到了广大开发者的青睐。自动配置作为Spring Boot的核心特性之一,大大减少了手动配置的工作量,提高了开发效率。
28 0
|
26天前
|
Java API Spring
在 Spring 配置文件中配置 Filter 的步骤
【10月更文挑战第21天】在 Spring 配置文件中配置 Filter 是实现请求过滤的重要手段。通过合理的配置,可以灵活地对请求进行处理,满足各种应用需求。还可以根据具体的项目要求和实际情况,进一步深入研究和优化 Filter 的配置,以提高应用的性能和安全性。
|
18天前
|
Java Spring
[Spring]aop的配置与使用
本文介绍了AOP(面向切面编程)的基本概念和核心思想。AOP是Spring框架的核心功能之一,通过动态代理在不修改原代码的情况下注入新功能。文章详细解释了连接点、切入点、通知、切面等关键概念,并列举了前置通知、后置通知、最终通知、异常通知和环绕通知五种通知类型。
29 1
|
3月前
|
开发框架 前端开发 .NET
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
49 0
|
6月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
194 0
|
6月前
|
开发框架 前端开发 JavaScript
JavaScript云LIS系统源码ASP.NET CORE 3.1 MVC + SQLserver + Redis医院实验室信息系统源码 医院云LIS系统源码
实验室信息系统(Laboratory Information System,缩写LIS)是一类用来处理实验室过程信息的软件,云LIS系统围绕临床,云LIS系统将与云HIS系统建立起高度的业务整合,以体现“以病人为中心”的设计理念,优化就诊流程,方便患者就医。
81 0
|
存储 开发框架 前端开发
[回馈]ASP.NET Core MVC开发实战之商城系统(五)
经过一段时间的准备,新的一期【ASP.NET Core MVC开发实战之商城系统】已经开始,在之前的文章中,讲解了商城系统的整体功能设计,页面布局设计,环境搭建,系统配置,及首页【商品类型,banner条,友情链接,降价促销,新品爆款】,商品列表页面,商品详情等功能的开发,今天继续讲解购物车功能开发,仅供学习分享使用,如有不足之处,还请指正。
167 0
|
开发框架 前端开发 .NET
[回馈]ASP.NET Core MVC开发实战之商城系统(三)
[回馈]ASP.NET Core MVC开发实战之商城系统(三)
103 0
|
开发框架 前端开发 .NET
[回馈]ASP.NET Core MVC开发实战之商城系统(一)
[回馈]ASP.NET Core MVC开发实战之商城系统(一)
177 0
下一篇
无影云桌面