概念

Media Type

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

image.png

REST On Spring Web MVC

理解自描述消息

比如我们要获取一个人员信息,请求接口/person/{id},返回人员信息的 JSON 数据。

  1. @RestController
  2. public class PersonController {
  3. @GetMapping("/person/{id}")
  4. public Person person(@PathVariable("id") Long id, @RequestParam(required = false) String name) {
  5. Person person = new Person();
  6. person.setId(id);
  7. person.setName(name);
  8. return person;
  9. }
  10. }
  1. {
  2. "id": 1,
  3. "name": "张三"
  4. }

请求头中 Accept 参数内容如下所示。

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,/;q=0.8

Spring MVC 会根据 Accept 参数来处理接口的返回数据类型,我们使用 Postman 来模拟浏览器请求。

image.png

设置 Accept 的值是 application/xml,没有数据返回,后台日志中报错没有找到对应的转换器。

通过如下步骤翻看 Spring MVC 源码。

EnableWebMvc -> DelegatingWebMvcConfiguration -> WebMvcConfigurationSupport#addDefaultHttpMessageConverters

image.png

上面的代码通过判断指定类是否存在来给各转换器的属性赋值

如果没有指定转换器,Spring MVC 调默认方法(addDefaultHttpMessageConverters)添加一些默认的转换器。

通过一路往上找调用的地方,可以找到 WebMvcConfigurationSupport#requestMappingHandlerAdapter。

image.png

在 adapter 中封装了 ContentNegotiationManager, resolveMediaTypes 方法返回 request 请求的 mediaTypes。

pom.xml 文件中引入 XML 转换器,设置 Accept 的值为 application/xml,返回 Person 对象的 XML 文件

  1. <dependency>
  2. <groupId>com.fasterxml.jackson.dataformat</groupId>
  3. <artifactId>jackson-dataformat-xml</artifactId>
  4. </dependency>
  1. <Person>
  2. <id>1</id>
  3. <name>张三</name>
  4. </Person>

结论

所有的 HTTP 自描述消息处理器均在 messageConverters(类型:HttpMessageConverter),这个集合会传递到 RequestMappingHandlerAdapter,最终控制写出。messageConverters 其中包含很多自描述消息类型的处理,比如 JSON、XML、TEXT等等。

以 application/json 为例,Spring Boot 中默认使用 Jackson2 序列化方式,其中媒体类型:application/json,它的处理类 MappingJackson2HttpMessageConverter,提供两类方法:

  1. 读 read* :通过 HTTP 请求内容转化成对应的 Bean
  2. 写 write*: 通过 Bean 序列化成对应文本内容作为响应内容

疑问

问题:当未设置 Accept 请求头时,为什么还是返回 JSON 数据
回答:这个依赖于 messageConverters 的插入顺序。通过在 AbstractJackson2HttpMessageConverter#canWrite 方法上 debug,进入到 AbstractMessageConverterMethodProcessor 类,可以看到 messageConverters 的内容。
image.png
代码中是采用遍历的方式去逐一尝试是否可以 canWrite,如果返回 true,说明可以序列化该对象,Jackson2 排在 Jackson2Xml 前面,刚好 Jackson2 能序列化该对象,所以返回了 JSON 数据。

问题:如何修改默认的转换器
回答:通过 WebMvcConfigurer 类的 configureMessageConverters、extendMessageConverters 方法调整。

扩展自描述消息

Properties 格式(待扩展,MediaType:application/properties+person)

  1. person.id = 1
  2. person.name = 张三

创建自定义消息转换器 PropertiesPersonHttpMessageConverter

  1. /**
  2. * Properties 转换器
  3. */
  4. public class PropertiesPersonHttpMessageConverter extends AbstractHttpMessageConverter<Person> {
  5. public PropertiesPersonHttpMessageConverter() {
  6. super(MediaType.valueOf("application/properties+person"));
  7. setDefaultCharset(StandardCharsets.UTF_8);
  8. }
  9. /**
  10. * 该转换器是否支持该类
  11. *
  12. * @param clazz
  13. * @return
  14. */
  15. @Override
  16. protected boolean supports(Class<?> clazz) {
  17. return clazz.isAssignableFrom(Person.class);
  18. }
  19. /**
  20. * 通过HTTP请求内容转化成对应的Bean
  21. *
  22. * @param clazz
  23. * @param inputMessage
  24. * @return
  25. * @throws IOException
  26. * @throws HttpMessageNotReadableException
  27. */
  28. @Override
  29. protected Person readInternal(Class<? extends Person> clazz, HttpInputMessage inputMessage)
  30. throws IOException, HttpMessageNotReadableException {
  31. InputStream in = inputMessage.getBody();
  32. Properties properties = new Properties();
  33. properties.load(new InputStreamReader(in, getDefaultCharset()));
  34. Person person = new Person();
  35. person.setId(Long.valueOf(properties.getProperty("person.id")));
  36. person.setName(properties.getProperty("person.name"));
  37. return person;
  38. }
  39. /**
  40. * 通过Bean序列化成对应文本内容作为响应内容
  41. *
  42. * @param person
  43. * @param outputMessage
  44. * @throws IOException
  45. * @throws HttpMessageNotWritableException
  46. */
  47. @Override
  48. protected void writeInternal(Person person, HttpOutputMessage outputMessage)
  49. throws IOException, HttpMessageNotWritableException {
  50. Properties properties = new Properties();
  51. properties.setProperty("person.id", String.valueOf(person.getId()));
  52. properties.setProperty("person.name", person.getName());
  53. OutputStream out = outputMessage.getBody();
  54. properties.store(new OutputStreamWriter(out, getDefaultCharset()),
  55. "Written by web server");
  56. }
  57. }

实现 AbstractHttpMessageConverter 抽象类

  • supports 方法:是否支持当前 POJO 类型
  • readInternal 方法:读取 HTTP 请求中的内容,并且转化成相应的 POJO 对象(通过 Properties 内容转化成 JSON)
  • writeInternal 方法:将 POJO 的内容序列化成文本内容(Properties 格式),最终输出到 HTTP 响应中(通过 JSON 内容转化成 Properties )

配置自定义消息转换器。

  1. /**
  2. * Web Mvc 配置类
  3. */
  4. public class WebMvcConfig implements WebMvcConfigurer {
  5. @Override
  6. public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
  7. converters.add(new PropertiesPersonHttpMessageConverter());
  8. }
  9. }

在 Controller 中使用自定义消息转换器实现内容的转换。

  1. @RestController
  2. public class PersonController {
  3. @GetMapping("/person/{id}")
  4. public Person person(@PathVariable("id") Long id,
  5. @RequestParam(required = false) String name) {
  6. Person person = new Person();
  7. person.setId(id);
  8. person.setName(name);
  9. return person;
  10. }
  11. /**
  12. * JSON转properties
  13. *
  14. * @param person
  15. * @return
  16. */
  17. @PostMapping(value = "/person/json/to/properties",
  18. consumes = "application/json", // 请求类型 Content-Type
  19. produces = "application/properties+person" // 响应类型 Accept
  20. )
  21. public Person personJsonToProperties(@RequestBody Person person) {
  22. return person;
  23. }
  24. /**
  25. * properties转JSON
  26. *
  27. * @param person
  28. * @return
  29. */
  30. @PostMapping(value = "/person/properties/to/json",
  31. consumes = "application/properties+person", // 请求类型 Content-Type
  32. produces = "application/json" // 响应类型 Accept
  33. )
  34. public Person personPropertiesToJson(@RequestBody Person person) {
  35. return person;
  36. }
  37. }

@RequestMappng 中的 consumes 对应请求头 “Content-Type”

@RequestMappng 中的 produces 对应请求头 “Accept”