现象演示
假设有一个需求是根据终端的不同,返回不同形式的数据,比如 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;}@Overridepublic 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: trueparameter-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: trueparameter-name: custom_formatmedia-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
- 是否存在 messageConverters 的 canWrite 方法返回 true
获取 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;}@Overridepublic String toString() {return "Cat{" +"name='" + name + '\'' +'}';}
}
创建自定义HttpMessageConverter
public class LanyuHttpMessageConverter implements HttpMessageConverter {@Overridepublic boolean canRead(Class clazz, MediaType mediaType) {return false;}@Overridepublic boolean canWrite(Class clazz, MediaType mediaType) {if (Cat.class == clazz) {return true;}return false;}@Overridepublic List<MediaType> getSupportedMediaTypes() {return Collections.singletonList(MediaType.parseMediaType("lanyu/custom"));}@Overridepublic Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {return null;}@Overridepublic 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 {@Overridepublic 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: trueparameter-name: custom_formatmedia-types: {lanyu : lanyu/custom}