Note 09. 深入 HttpMessageConverter:内容协商机制
Note 09. 深入 HttpMessageConverter:内容协商机制
Prorise第九章. 揭秘 Spring MVC 核心:HttpMessageConverter 与内容协商
摘要:在前几章中,我们已经熟练地使用 @RequestBody 和 @ResponseBody 来处理 JSON 数据,但这一切是如何自动发生的?本章,我们将深入 Spring MVC 的“引擎室”,揭开这个自动化魔法背后的核心功臣——HttpMessageConverter。我们将系统学习“内容协商”(Content Negotiation)机制,理解 Spring MVC 如何根据 Content-Type 和 Accept 请求头智能地选择数据格式,并亲手实践,让我们的同一个 API 接口具备同时支持 JSON 和 XML 两种响应格式的强大能力。
本章学习路径
- 原理揭秘:我们将从一个问题出发:“Spring MVC 凭什么能自动转换 JSON?”,从而引出
HttpMessageConverter的核心职责——Java 对象与 HTTP 报文体之间的翻译官。 - 决策大脑:内容协商:我们将深入学习内容协商机制,彻底厘清
Content-Type(我给你的是什么)和Accept(我想要的是什么)这两个关键请求头的区别与作用。 - 决策流程详解:我们将通过清晰的流程图,一步步解析 Spring MVC 在处理
@RequestBody和@ResponseBody时,是如何遍历转换器列表并做出最终选择的。 - 实战:赋能 API 支持 XML:我们将通过一个实战案例,仅需添加一个依赖,就能让之前只返回 JSON 的接口“瞬间”学会返回 XML,直观感受内容协商的强大之处。
- 概念辨析:在章节最后,我们将明确区分
HttpMessageConverter与Converter<S, T>这两个容易混淆的概念,巩固知识边界。
9.1. 回顾与思考:自动化转换的背后是什么?
在之前的章节中,我们已经理所当然地接受了一个“事实”:
- 在 Controller 方法的参数前加上
@RequestBody,Spring MVC 就能自动将请求体中的 JSON 字符串,转换成一个UserDTOJava 对象。 - 在
@RestController注解的类中,一个方法只要返回一个UserVOJava 对象,Spring MVC 就能自动将其序列化为 JSON 字符串,并写入响应体。
这一切看起来如此流畅自然,但我们作为追求进阶的开发者,必须提出一个深刻的问题:这背后究竟是谁在工作? Spring MVC 内部必然存在一个或一套组件,专门负责在 Java 对象和 HTTP 请求/响应的原始报文(通常是字符串或字节流)之间进行双向的“翻译”。
这个核心组件,就是 HttpMessageConverter (HTTP 消息转换器)。
你可以把它理解为一个高度专业的“翻译团队”。团队里有多个“翻译官”,每个翻译官都精通一种特定的“语言”(数据格式):
MappingJackson2HttpMessageConverter:精通 JSON 语言,是我们最常用、最重要的翻译官。MappingJackson2XmlHttpMessageConverter:精通 XML 语言。StringHttpMessageConverter:精通纯文本语言。ByteArrayHttpMessageConverter:精通二进制字节流。- 等等…
当 Spring Boot 应用启动时,它会根据当前项目 classpath 下的依赖,自动地将这些“翻译官”注册并组成一个有序的列表。spring-boot-starter-web 默认引入了 jackson-databind 依赖,因此 MappingJackson2HttpMessageConverter 会被自动注册并排在靠前的位置。
9.2. 内容协商:客户端与服务器的“对话”
现在我们知道了有一群“翻译官”待命,那么当一个 HTTP 请求到来时,Spring MVC 是如何决定该派哪一位翻译官上场呢?这个决策过程,就叫做 内容协商 (Content Negotiation)。
内容协商本质上是客户端与服务器之间,通过 HTTP Header 进行的一场关于数据格式的“对话”。这场对话依赖于两个核心的 HTTP 请求头,它们经常被混淆,我们必须彻底搞清楚。
9.2.1. Content-Type:我发送给你的是什么
- 方向:客户端 -> 服务器 (请求)
- 作用:用于 读取请求体,即配合
@RequestBody注解。 - 含义:
Content-TypeHeader 由客户端设置,用来明确告知服务器:“我这次请求的 Body 里,装的是什么格式的数据。” - 示例:
Content-Type: application/json-> “你好服务器,我发给你的是一段 JSON,请找你的 JSON 专家来解析。”Content-Type: application/xml-> “你好服务器,我发给你的是一段 XML,请找你的 XML 专家来解析。”Content-Type: application/x-www-form-urlencoded-> “你好服务器,我发给你的是普通的表单数据。”
如果客户端发送了 JSON 数据,但没有设置这个 Header,或者设置错了(例如写成了 text/plain),服务器就会因为找不到合适的“翻译官”而感到困惑,最终可能会拒绝处理,并返回一个 415 Unsupported Media Type 的错误。
9.2.2. Accept:我期望你返回什么
- 方向:服务器 -> 客户端 (响应)
- 作用:用于 写入响应体,即配合
@ResponseBody或@RestController。 - 含义:
AcceptHeader 同样由客户端设置,用来告知服务器:“关于这次请求的响应,我希望你返回什么格式的数据给我。我能看懂这些格式。” - 示例:
Accept: application/json-> “你好服务器,请务必返回 JSON 格式的数据给我,我只认这个。”Accept: application/xml-> “你好服务器,请返回 XML 给我。”Accept: application/json, application/xml-> “你好服务器,JSON 或者 XML 格式的响应我都能接受,你自己看着办吧。”
如果服务器发现客户端期望的格式(如 application/xml),自己手下的所有“翻译官”都不会,那么它就会很礼貌地拒绝,并返回一个 406 Not Acceptable 的错误。
9.3. 源码视角:Spring MVC 的决策流程
了解了两个核心 Header 后,我们来深入 Spring MVC 内部,看看它具体是如何使用这些信息来决策的。
9.3.1. 处理 @RequestBody (读取请求)
当一个请求到达带有 @RequestBody 注解的方法时,Spring MVC 的处理流程如下:
- 获取
Content-Type:首先,从当前 HTTP 请求中解析出Content-TypeHeader 的值,比如是application/json。 - 获取目标类型:查看
@RequestBody注解标注的参数类型,比如是UserCreateDTO.class。 - 遍历转换器列表:开始从头到尾遍历内部维护的
HttpMessageConverter列表。 - 调用
canRead()决策:对于列表中的每一个converter,调用其canRead(Class<?> clazz, @Nullable MediaType mediaType)方法,并传入UserCreateDTO.class和application/json。 - 寻找“天选之子”:
StringHttpMessageConverter拿到后一看,类型是UserCreateDTO,不是String,返回false。- 轮到
MappingJackson2HttpMessageConverter,它一看,媒体类型是application/json(我擅长!),目标类型是一个 POJO(我能处理!),于是返回true。
- 执行读取:一旦找到第一个返回
true的converter,Spring MVC 就会立即调用它的read()方法,将请求的输入流(InputStream)交给它处理。MappingJackson2HttpMessageConverter随后会将输入流中的 JSON 文本反序列化成一个UserCreateDTO对象实例。 - 中断并返回:后续的转换器不再遍历,决策过程结束。
9.3.2. 处理 @ResponseBody (写入响应)
当一个方法执行完毕,需要将返回的 Java 对象写入响应体时,流程类似但依据不同:
- 获取
AcceptHeader:从请求中解析出客户端期望的媒体类型列表,比如是application/json。 - 获取源对象类型:查看方法的返回值类型,比如是
UserVO.class。 - 遍历转换器列表:同样,从头到尾遍历
HttpMessageConverter列表。 - 调用
canWrite()决策:对于列表中的每一个converter,调用其canWrite(Class<?> clazz, @Nullable MediaType mediaType)方法,并传入UserVO.class和application/json。 - 寻找“天选之子”:
MappingJackson2HttpMessageConverter发现自己既能处理UserVO这个 POJO,又能生成客户端想要的application/json格式,于是返回true。 - 执行写入:Spring MVC 立即调用这个
converter的write()方法,MappingJackson2HttpMessageConverter负责将UserVO对象序列化成 JSON 字符串,并写入响应的输出流(OutputStream)。 - 中断并返回:决策过程结束。
9.4. 实战:让 API 同时支持 JSON 与 XML
理论讲了这么多,让我们通过一个激动人心的实战来感受内容协商的威力。我们将改造在第八章创建的 JsonTestController,让 /user/profile 这个接口在不修改任何 Java 代码的情况下,既能返回 JSON,也能返回 XML。
9.4.1. 步骤一:添加 XML “翻译官”依赖
Spring Boot 的自动配置是基于“按需加载”的。默认情况下,它没有 XML 相关的依赖,所以 MappingJackson2XmlHttpMessageConverter 这个“翻译官”并未被注册。我们需要做的,仅仅是把它“请”进我们的项目。
打开 pom.xml 文件,添加 jackson-dataformat-xml 依赖:
1 | <dependency> |
9.4.2. 步骤二:重启与验证
添加完依赖后,无需任何其他配置,直接重启你的 Spring Boot 应用。在启动过程中,Spring Boot 的自动配置机制会检测到 jackson-dataformat-xml 的存在,并自动将 MappingJackson2XmlHttpMessageConverter 注册到 HttpMessageConverter 列表中。
现在,让我们用 cURL 工具来分别测试两种情况:
测试 1:请求 JSON 格式 (和以前一样)
1 | curl -H "Accept: application/json" http://localhost:8080/user/profile |
响应 (JSON):
1 | { |
这个行为和之前完全一样,因为当 Accept 头是 application/json 时,内容协商机制依然会优先选择 MappingJackson2HttpMessageConverter。
测试 2:请求 XML 格式 (见证奇迹的时刻)
1 | curl -H "Accept: application/xml" http://localhost:8080/user/profile |
响应 (XML):
1 | <UserVO> |
我们成功了!仅仅通过改变客户端的 Accept 请求头,服务器就智能地切换了“翻译官”,返回了完全不同的数据格式。这就是内容协商机制最直观、最强大的体现。
9.5. 本章总结与概念辨析
摘要回顾
本章,我们深入到了 Spring MVC 数据转换的底层。我们揭示了 @RequestBody 和 @ResponseBody 背后真正的功臣——HttpMessageConverter 接口。通过学习内容协商机制,我们彻底理解了服务器是如何根据客户端的 Content-Type 和 Accept 头,智能地选择合适的转换器来处理请求和响应的。最后,通过一个极具说服力的 XML 支持实战,我们亲眼见证了内容协商在构建灵活、多格式 API 中的强大威力。
9.5.1. 核心概念辨析:HttpMessageConverter vs Converter<S, T>
在学习 Spring MVC 时,这两个 Converter 极易混淆,我们必须在此做出清晰的界定。
| 特性 | HttpMessageConverter | Converter<S, T> |
|---|---|---|
| 核心职责 | 在 Java 对象 与 HTTP 请求/响应体 之间进行转换 | 在一种 Java 类型 (S) 到另一种 Java 类型 (T) 之间进行转换 |
| 处理对象 | 整个 HTTP Body (如 JSON, XML 字符串) | 单个值 (如 String, Integer) |
| 触发场景 | @RequestBody, @ResponseBody, RestTemplate | @RequestParam, @PathVariable, @CookieValue 等参数绑定 |
| 工作机制 | 内容协商 (基于 Content-Type, Accept 头) | Spring 类型转换服务 (ConversionService) |
| 典型实现 | MappingJackson2HttpMessageConverter | StringToIntegerConverter, 自定义的 StringToEnumConverter |
| 解决问题 | “我的 UserDTO 对象如何与请求体中的 JSON 字符串 互相转换?” | “URL 中的字符串 '1' 如何转换成我方法参数中的 Integer 类型?” |
简单来说,HttpMessageConverter 是“宏观”的,负责整个报文体的序列化/反序列化;而 Converter<S, T> 是“微观”的,负责单个参数值的类型转换。







