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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 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…




相关文章
|
5月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
167 2
|
2月前
|
XML Java Maven
Spring 手动实现Spring底层机制
Spring 第六节 手动实现Spring底层机制 万字详解!
95 31
|
4月前
|
NoSQL Java Redis
Spring Boot 自动配置机制:从原理到自定义
Spring Boot 的自动配置机制通过 `spring.factories` 文件和 `@EnableAutoConfiguration` 注解,根据类路径中的依赖和条件注解自动配置所需的 Bean,大大简化了开发过程。本文深入探讨了自动配置的原理、条件化配置、自定义自动配置以及实际应用案例,帮助开发者更好地理解和利用这一强大特性。
352 14
|
4月前
|
缓存 Java 数据库连接
深入探讨:Spring与MyBatis中的连接池与缓存机制
Spring 与 MyBatis 提供了强大的连接池和缓存机制,通过合理配置和使用这些机制,可以显著提升应用的性能和可扩展性。连接池通过复用数据库连接减少了连接创建和销毁的开销,而 MyBatis 的一级缓存和二级缓存则通过缓存查询结果减少了数据库访问次数。在实际应用中,结合具体的业务需求和系统架构,优化连接池和缓存的配置,是提升系统性能的重要手段。
227 4
|
5月前
|
Java 开发者 Spring
深入解析:Spring AOP的底层实现机制
在现代软件开发中,Spring框架的AOP(面向切面编程)功能因其能够有效分离横切关注点(如日志记录、事务管理等)而备受青睐。本文将深入探讨Spring AOP的底层原理,揭示其如何通过动态代理技术实现方法的增强。
189 8
|
7月前
|
前端开发 Java Spring
关于spring mvc 的 addPathPatterns 拦截配置常见问题
关于spring mvc 的 addPathPatterns 拦截配置常见问题
408 2
|
6月前
|
前端开发 Java 数据库
springBoot:template engine&自定义一个mvc&后端给前端传数据&增删改查 (三)
本文介绍了如何自定义一个 MVC 框架,包括后端向前端传递数据、前后端代理配置、实现增删改查功能以及分页查询。详细展示了代码示例,从配置文件到控制器、服务层和数据访问层的实现,帮助开发者快速理解和应用。
|
11月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
275 0
|
8月前
|
开发框架 前端开发 .NET
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
ASP.NET MVC WebApi 接口返回 JOSN 日期格式化 date format
104 0
|
存储 开发框架 前端开发
[回馈]ASP.NET Core MVC开发实战之商城系统(五)
经过一段时间的准备,新的一期【ASP.NET Core MVC开发实战之商城系统】已经开始,在之前的文章中,讲解了商城系统的整体功能设计,页面布局设计,环境搭建,系统配置,及首页【商品类型,banner条,友情链接,降价促销,新品爆款】,商品列表页面,商品详情等功能的开发,今天继续讲解购物车功能开发,仅供学习分享使用,如有不足之处,还请指正。
204 0
下一篇
oss创建bucket