概念
Media Type
媒体类型,比如 Http 协议会在 request 请求头中带有媒体类型,如下图所示。
REST On Spring Web MVC
理解自描述消息
比如我们要获取一个人员信息,请求接口/person/{id},返回人员信息的 JSON 数据。
@RestController
public class PersonController {
@GetMapping("/person/{id}")
public Person person(@PathVariable("id") Long id, @RequestParam(required = false) String name) {
Person person = new Person();
person.setId(id);
person.setName(name);
return person;
}
}
{
"id": 1,
"name": "张三"
}
请求头中 Accept 参数内容如下所示。
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8
Spring MVC 会根据 Accept 参数来处理接口的返回数据类型,我们使用 Postman 来模拟浏览器请求。
设置 Accept 的值是 application/xml,没有数据返回,后台日志中报错没有找到对应的转换器。
通过如下步骤翻看 Spring MVC 源码。
EnableWebMvc -> DelegatingWebMvcConfiguration -> WebMvcConfigurationSupport#addDefaultHttpMessageConverters
上面的代码通过判断指定类是否存在来给各转换器的属性赋值
如果没有指定转换器,Spring MVC 调默认方法(addDefaultHttpMessageConverters)添加一些默认的转换器。
通过一路往上找调用的地方,可以找到 WebMvcConfigurationSupport#requestMappingHandlerAdapter。
在 adapter 中封装了 ContentNegotiationManager, resolveMediaTypes 方法返回 request 请求的 mediaTypes。
pom.xml 文件中引入 XML 转换器,设置 Accept 的值为 application/xml,返回 Person 对象的 XML 文件
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<Person>
<id>1</id>
<name>张三</name>
</Person>
结论
所有的 HTTP 自描述消息处理器均在 messageConverters(类型:HttpMessageConverter),这个集合会传递到 RequestMappingHandlerAdapter,最终控制写出。messageConverters 其中包含很多自描述消息类型的处理,比如 JSON、XML、TEXT等等。
以 application/json 为例,Spring Boot 中默认使用 Jackson2 序列化方式,其中媒体类型:application/json,它的处理类 MappingJackson2HttpMessageConverter,提供两类方法:
- 读 read* :通过 HTTP 请求内容转化成对应的 Bean
- 写 write*: 通过 Bean 序列化成对应文本内容作为响应内容
疑问
问题:当未设置 Accept 请求头时,为什么还是返回 JSON 数据
回答:这个依赖于 messageConverters 的插入顺序。通过在 AbstractJackson2HttpMessageConverter#canWrite 方法上 debug,进入到 AbstractMessageConverterMethodProcessor 类,可以看到 messageConverters 的内容。
代码中是采用遍历的方式去逐一尝试是否可以 canWrite,如果返回 true,说明可以序列化该对象,Jackson2 排在 Jackson2Xml 前面,刚好 Jackson2 能序列化该对象,所以返回了 JSON 数据。
问题:如何修改默认的转换器
回答:通过 WebMvcConfigurer 类的 configureMessageConverters、extendMessageConverters 方法调整。
扩展自描述消息
Properties 格式(待扩展,MediaType:application/properties+person)
person.id = 1
person.name = 张三
创建自定义消息转换器 PropertiesPersonHttpMessageConverter
/**
* Properties 转换器
*/
public class PropertiesPersonHttpMessageConverter extends AbstractHttpMessageConverter<Person> {
public PropertiesPersonHttpMessageConverter() {
super(MediaType.valueOf("application/properties+person"));
setDefaultCharset(StandardCharsets.UTF_8);
}
/**
* 该转换器是否支持该类
*
* @param clazz
* @return
*/
@Override
protected boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(Person.class);
}
/**
* 通过HTTP请求内容转化成对应的Bean
*
* @param clazz
* @param inputMessage
* @return
* @throws IOException
* @throws HttpMessageNotReadableException
*/
@Override
protected Person readInternal(Class<? extends Person> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
InputStream in = inputMessage.getBody();
Properties properties = new Properties();
properties.load(new InputStreamReader(in, getDefaultCharset()));
Person person = new Person();
person.setId(Long.valueOf(properties.getProperty("person.id")));
person.setName(properties.getProperty("person.name"));
return person;
}
/**
* 通过Bean序列化成对应文本内容作为响应内容
*
* @param person
* @param outputMessage
* @throws IOException
* @throws HttpMessageNotWritableException
*/
@Override
protected void writeInternal(Person person, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
Properties properties = new Properties();
properties.setProperty("person.id", String.valueOf(person.getId()));
properties.setProperty("person.name", person.getName());
OutputStream out = outputMessage.getBody();
properties.store(new OutputStreamWriter(out, getDefaultCharset()),
"Written by web server");
}
}
实现 AbstractHttpMessageConverter 抽象类
- supports 方法:是否支持当前 POJO 类型
- readInternal 方法:读取 HTTP 请求中的内容,并且转化成相应的 POJO 对象(通过 Properties 内容转化成 JSON)
- writeInternal 方法:将 POJO 的内容序列化成文本内容(Properties 格式),最终输出到 HTTP 响应中(通过 JSON 内容转化成 Properties )
配置自定义消息转换器。
/**
* Web Mvc 配置类
*/
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new PropertiesPersonHttpMessageConverter());
}
}
在 Controller 中使用自定义消息转换器实现内容的转换。
@RestController
public class PersonController {
@GetMapping("/person/{id}")
public Person person(@PathVariable("id") Long id,
@RequestParam(required = false) String name) {
Person person = new Person();
person.setId(id);
person.setName(name);
return person;
}
/**
* JSON转properties
*
* @param person
* @return
*/
@PostMapping(value = "/person/json/to/properties",
consumes = "application/json", // 请求类型 Content-Type
produces = "application/properties+person" // 响应类型 Accept
)
public Person personJsonToProperties(@RequestBody Person person) {
return person;
}
/**
* properties转JSON
*
* @param person
* @return
*/
@PostMapping(value = "/person/properties/to/json",
consumes = "application/properties+person", // 请求类型 Content-Type
produces = "application/json" // 响应类型 Accept
)
public Person personPropertiesToJson(@RequestBody Person person) {
return person;
}
}
@RequestMappng 中的 consumes 对应请求头 “Content-Type”
@RequestMappng 中的 produces 对应请求头 “Accept”