概念
Media Type
媒体类型,比如 Http 协议会在 request 请求头中带有媒体类型,如下图所示。

REST On Spring Web MVC
理解自描述消息
比如我们要获取一个人员信息,请求接口/person/{id},返回人员信息的 JSON 数据。
@RestControllerpublic 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 = 1person.name = 张三
创建自定义消息转换器 PropertiesPersonHttpMessageConverter
/*** Properties 转换器*/public class PropertiesPersonHttpMessageConverter extends AbstractHttpMessageConverter<Person> {public PropertiesPersonHttpMessageConverter() {super(MediaType.valueOf("application/properties+person"));setDefaultCharset(StandardCharsets.UTF_8);}/*** 该转换器是否支持该类** @param clazz* @return*/@Overrideprotected boolean supports(Class<?> clazz) {return clazz.isAssignableFrom(Person.class);}/*** 通过HTTP请求内容转化成对应的Bean** @param clazz* @param inputMessage* @return* @throws IOException* @throws HttpMessageNotReadableException*/@Overrideprotected 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*/@Overrideprotected 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 {@Overridepublic void extendMessageConverters(List<HttpMessageConverter<?>> converters) {converters.add(new PropertiesPersonHttpMessageConverter());}}
在 Controller 中使用自定义消息转换器实现内容的转换。
@RestControllerpublic 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-Typeproduces = "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-Typeproduces = "application/json" // 响应类型 Accept)public Person personPropertiesToJson(@RequestBody Person person) {return person;}}
@RequestMappng 中的 consumes 对应请求头 “Content-Type”
@RequestMappng 中的 produces 对应请求头 “Accept”
