现象演示
假设有一个需求是根据终端的不同,返回不同形式的数据,比如 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
- 是否存在 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;
}
@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}