SpringBoot自定义内容协商的实现
作者:一只懒鱼a
现象演示
假设有一个需求是根据终端的不同,返回不同形式的数据,比如 PC 端需要以 HTML 格式返回数据,APP、小程序端需要以 JSON 格式返回数据。这时我们是 coding 几个相似的接口?还是在一个接口里面做复杂判断处理?两个方案貌似都不理想,一旦需求改动,维护的东西就比较多,这时候我们利用 SpringBoot 的内容协商功能,就可以很好的简化逻辑,案例演示如下:
创建实体类Dog
public class Dog { private String name; public Dog() { } public Dog(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Dog{" + "name='" + name + '\'' + '}'; } }
创建Controller
@RestController @RequestMapping("/content_negotiation") public class ContentNegotiationController { @GetMapping("/simple") public Dog getDog() { return new Dog("wangcai"); } }
开启参数形式内容协商
spring: mvc: contentnegotiation: favor-parameter: true
添加POM依赖 (让 SpringBoot 有返回相关格式数据的能力)
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.16.1</version> </dependency>
请求及响应
返回 JSON 格式的数据
返回 HTML 格式的数据
源码解析
HandlerMethodReturnValueHandlerComposite#handleReturnValue
总体分为两步:
- 选择一个 HandlerMethodReturnValueHandler 来处理当前返回值
- 处理返回值
选择 HandlerMethodReturnValueHandler
SpringBoot 会手动注册的一些 HandlerMethodReturnValueHandler ,一共有15 个 (SpringBoot 版本 2.6.13),我们主要关注 RequestResponseBodyMethodProcessor 这个handler。
RequestResponseBodyMethodProcessor#supportsReturnType
如果接口方法含有 @ResponseBody 注解,或者相关Controller上含有 @ResponseBody 注解,则RequestResponseBodyMethodProcessor 都可以处理
处理返回值
writeWithMessageConverters 方法大概有以下几个步骤:
- 获取 acceptableTypes
- 获取 producibleTypes
- 获取 mediaTypesToUse
- 给 mediaTypesToUse 排序
- 获取 selectedMediaType
- 写出数据
获取 acceptableTypes
分为两个分支:
- Response 是否指定 Content-Type,并且 Content-Type 的类型不是 */*,则直接跳转到步骤6 (写出数据)
- 获取 acceptableTypes
AbstractMessageConverterMethodProcessor#getAcceptableMediaTypes
默认情况下,只有 HeaderContentNegotiationStrategy ,因为我们在现象演示的时候,将属性 spring.mvc.contentnegotiation.favor-parameter 设置为 true,所以多出来一个 ParameterContentNegotiationStrategy 。如果 ParameterContentNegotiationStrategy 的 resolveMediaTypes 方法的返回值不为 null 且不为 MEDIA_TYPE_ALL_LIST,则以 ParameterContentNegotiationStrategy 的 resolveMediaTypes 方法返回值为准,即 ParameterContentNegotiationStrategy 的优先级高于 HeaderContentNegotiationStrategy
ParameterContentNegotiationStrategy#resolveMediaTypes
getMediaTypeKey
默认情况下,parameterName 的值为 format
修改 parameterName 默认值
spring: mvc: contentnegotiation: favor-parameter: true parameter-name: custom_format
resolveMediaTypeKey
就是以 parameterName 对应的属性值为 key, 从 URL 中获取 value,再以该 value 为 mediaTypeKey 从一个 map (mediaTypes)中获取 MediaType
mediaTypes的初始赋值
WebMvcConfigurationSupport#mvcContentNegotiationManager
因为我们在现象演示的时候添加了相关POM依赖,所有 mediaTypes 的 内容如下所示:
即默认情况下,format 的参数值含义如下 :
- json:acceptableTypes 为 [ application/json ]
- xml:acceptableTypes 为 [ application/xml ]
- 其他:acceptableTypes 为 [ */*]
也可以自定义key,配置如下所示,这时候如果参数携带 custom_format=lanyu,系统也会返回 application/xml 格式的数据
spring: mvc: contentnegotiation: favor-parameter: true parameter-name: custom_format media-types: {lanyu : application/xml}
HeaderContentNegotiationStrategy#resolveMediaTypes
HeaderContentNegotiationStrategy 的 resolveMediaTypes 方法比较简单,就是获取请求头中 Accept 的值
获取 producibleTypes
大概分为以下几种情况
Request 域的 HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE 是否为 null
属性值不为 null:返回指定的 mediaTypes
属性值为 null
- 是否存在 messageConverters 的 canWrite 方法返回 true
- 存在:相关 messageConverters 的 getSupportedMediaTypes 方法返回值的集合
- 不存在:MediaType.ALL
获取 mediaTypesToUse
主要通过 isCompatibleWith 方法判断 acceptableType 和 producibleType 是不是兼容的,主要有以下几种情况 :
producibleType 为 null:返回false
producibleType 不为null
- acceptableType 为 */* 或 producibleType为 */* :返回true
- acceptableType 和 producibleType 都不为 */*
- acceptableType 和 producibleType 的 type 一致
- acceptableType 和 producibleType 的 subtype 一致 : 返回true
- acceptableType 和 producibleType 的 subtype 不一致
- acceptableType 或 producibleType 的 subtype 的 isWildcardSubtype 方法返回true
- acceptableType 或 producibleType 的 subtype 为 *:返回true
- acceptableType 的 subtype 以 *+ 开头,并且 acceptableType 的后缀与producibleType一致:返回true
- producibleType 的 subtype 以 *+ 开头,并且 producibleType 的后缀与acceptableType 一致:返回true
- 其他情况:返回false
- acceptableType 与 producibleType 的 subtype 的 isWildcardSubtype 方法都返回false:返回false
- acceptableType 或 producibleType 的 subtype 的 isWildcardSubtype 方法返回true
- acceptableType 和 producibleType 的 type 不一致:返回false
- acceptableType 和 producibleType 的 type 一致
给 mediaTypesToUse 排序
排序规则1:
- 权重越大优先级越高
- 参数个数越多优先级越高
如果排序规则1未判断出谁的优先级高,则使用排序规则2,排序规则2如下:
- 权重越大优先级越高
- type类型不为 *
- subtype类型不为 * 或以 *+ 开头
- 参数个数越多优先级越高
获取 selectedMediaType
遍历上一步经过排序的 mediaTypes,如果存在一个 mediaType 满足以下条件,则直接返回
- type 类型不为 * ,subtype 类型不为 * 且不以 *+ 开头
MediaType 为 */* 或 application/*
写出数据
如果存在一个 HttpMessageConverter 的 canWrite 方法返回 true,则使用 HttpMessageConverter 的 write 方法写出数据
扩展:自定义HttpMessageConverter处理自定义协议
创建实体类Cat
public class Cat { private String name; public Cat() { } public Cat(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Cat{" + "name='" + name + '\'' + '}'; } }
创建自定义HttpMessageConverter
public class LanyuHttpMessageConverter implements HttpMessageConverter { @Override public boolean canRead(Class clazz, MediaType mediaType) { return false; } @Override public boolean canWrite(Class clazz, MediaType mediaType) { if (Cat.class == clazz) { return true; } return false; } @Override public List<MediaType> getSupportedMediaTypes() { return Collections.singletonList(MediaType.parseMediaType("lanyu/custom")); } @Override public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { return null; } @Override public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { try { Cat cat = (Cat) o; String data = "cat = {name : " + cat.getName() + "}"; OutputStream outputStream = outputMessage.getBody(); outputStream.write(data.getBytes()); outputStream.flush(); } catch (Exception e) { throw new RuntimeException(e); } } }
创建配置类
@Configuration public class MessageConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new LanyuHttpMessageConverter()); } }
接口及响应
@GetMapping("/custom_protocol") public Cat getCustomProtocolData() { return new Cat("tom"); }
我们也可以让我们自定义的协议支持 URL 传参形式,配置如下
spring: mvc: contentnegotiation: favor-parameter: true parameter-name: custom_format media-types: {lanyu : lanyu/custom}
到此这篇关于SpringBoot自定义内容协商的实现的文章就介绍到这了,更多相关SpringBoot 内容协商内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!