Spring MVC 通过 内容协商 (Content Negotiation) 来根据客户端请求的 Accept
头决定返回 JSON、XML 还是其他格式的数据。
以下是核心机制和步骤:
-
客户端请求中的
Accept
头:- 客户端(如浏览器、curl、Postman等)在发起HTTP请求时,会通过
Accept
请求头告知服务器它期望接收的数据格式(MIME类型)。 - 例如:
Accept: application/json
(客户端期望JSON)Accept: application/xml
(客户端期望XML)Accept: application/json, application/xml;q=0.9, */*;q=0.8
(客户端优先期望JSON,如果不行则XML,q是权重因子)
- 客户端(如浏览器、curl、Postman等)在发起HTTP请求时,会通过
-
Spring MVC 的
ContentNegotiationManager
:- Spring MVC 内部使用
ContentNegotiationManager
(或其策略实现) 来确定请求的目标媒体类型。 ContentNegotiationManager
会按照一定的优先级顺序尝试解析客户端的期望格式。默认情况下(尤其是在Spring Boot中),这个顺序通常是:- URL 路径扩展名 (Path Extension): 检查URL末尾是否有如
.json
或.xml
的扩展名。例如:GET /api/users/1.json
。 (可以通过配置禁用,因为有些人认为这不符合纯粹的RESTful风格,URL应标识资源而非其表示。) - URL 参数 (Parameter): 检查URL中是否有名为
format
(或其他可配置名称) 的参数。例如:GET /api/users/1?format=json
。 (默认通常是关闭的,但可以配置启用。) Accept
请求头: 这是最常用且最符合HTTP规范的方式。ContentNegotiationManager
会解析Accept
头来确定客户端期望的媒体类型。- 默认内容类型 (Default Content Type): 如果以上方法都无法确定,或者客户端没有发送明确的
Accept
头,Spring MVC 可能会使用预设的默认内容类型(例如,JSON)。
- URL 路径扩展名 (Path Extension): 检查URL末尾是否有如
- Spring MVC 内部使用
-
@ResponseBody
和HttpMessageConverter
:- 当Controller方法被
@ResponseBody
注解标记(或者Controller类被@RestController
注解标记,它包含了@ResponseBody
),Spring MVC 知道这个方法的返回值应该直接写入HTTP响应体中,而不是被视图解析器解析为视图名。 - Spring MVC 维护了一个
HttpMessageConverter
列表。这些转换器负责将Java对象序列化为特定的HTTP响应体格式(如JSON、XML)以及将HTTP请求体反序列化为Java对象。 - 常见的转换器有:
MappingJackson2HttpMessageConverter
: 用于处理JSON,依赖Jackson库。Jaxb2RootElementHttpMessageConverter
: 用于处理XML,依赖JAXB API和实现。MappingJackson2XmlHttpMessageConverter
: 也是用于处理XML,但使用Jackson的XML模块。
- 当Controller方法被
-
工作流程总结:
- 客户端发送请求,包含
Accept
头 (例如,Accept: application/xml
)。 DispatcherServlet
将请求路由到相应的Controller方法。ContentNegotiationManager
解析请求(主要关注Accept
头),确定目标媒体类型为application/xml
。- Controller方法执行完毕,返回一个Java对象 (例如,一个
User
对象)。 - 由于方法有
@ResponseBody
,Spring MVC会查找一个能够将User
对象转换为application/xml
格式的HttpMessageConverter
。 - 如果找到了合适的转换器(例如
Jaxb2RootElementHttpMessageConverter
或MappingJackson2XmlHttpMessageConverter
),该转换器就会将User
对象序列化为XML字符串。 - Spring MVC将序列化后的XML字符串写入HTTP响应体,并设置响应头
Content-Type: application/xml
。
- 客户端发送请求,包含
示例代码 (Spring Boot):
假设你有一个POJO:
// src/main/java/com/example/demo/User.java
package com.example.demo;
// 如果使用JAXB,需要添加 @XmlRootElement
// import javax.xml.bind.annotation.XmlRootElement;
// @XmlRootElement
public class User {
private Long id;
private String name;
private String email;
// 构造函数、Getters 和 Setters
public User(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
Controller:
// src/main/java/com/example/demo/UserController.java
package com.example.demo;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping(value = "/users/{id}", produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE })
public User getUser(@PathVariable Long id) {
// 实际应用中会从服务层或数据库获取用户
return new User(id, "John Doe " + id, "john.doe" + id + "@example.com");
}
}
依赖:
- 对于JSON: Spring Boot 的
spring-boot-starter-web
默认包含了 Jackson,所以通常不需要额外添加。 - 对于XML: 你需要添加相应的XML处理库。
- 使用Jackson XML:
如果使用Jackson XML,它会自动注册<!-- pom.xml --> <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>
MappingJackson2XmlHttpMessageConverter
。 - 使用JAXB:
从Java 9开始,JAXB不再是JDK的一部分,需要手动添加依赖:
并且在你的POJO上使用<!-- pom.xml --> <dependency> <groupId>jakarta.xml.bind</groupId> <artifactId>jakarta.xml.bind-api</artifactId> <!-- Version may vary --> </dependency> <dependency> <groupId>org.glassfish.jaxb</groupId> <artifactId>jaxb-runtime</artifactId> <!-- Version may vary --> </dependency>
@XmlRootElement
(以及其他JAXB注解,如@XmlElement
)。
- 使用Jackson XML:
如何测试:
使用 curl
或 Postman:
-
请求JSON:
curl -H "Accept: application/json" http://localhost:8080/users/1
响应头会包含
Content-Type: application/json
,响应体是JSON格式的用户数据。 -
请求XML:
curl -H "Accept: application/xml" http://localhost:8080/users/1
响应头会包含
Content-Type: application/xml
,响应体是XML格式的用户数据。
produces
属性:
在 @RequestMapping
(或 @GetMapping
等) 中使用 produces
属性,例如 produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE }
,是一个好习惯。它明确声明了这个端点能够生成哪些媒体类型。如果客户端请求的 Accept
类型与 produces
中声明的任何一种都不匹配,Spring MVC 通常会返回 406 Not Acceptable
状态码。
自定义内容协商策略 (可选):
如果想更精细地控制内容协商的行为(例如,禁用路径扩展名策略,或设置默认的内容类型),可以通过实现 WebMvcConfigurer
接口并重写 configureContentNegotiation
方法来进行配置。
// import org.springframework.context.annotation.Configuration;
// import org.springframework.http.MediaType;
// import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
// import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// @Configuration
// public class WebConfig implements WebMvcConfigurer {
//
// @Override
// public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
// configurer.favorParameter(false) // 不使用URL参数 (?format=json)
// .favorPathExtension(false) // 不使用路径扩展 (.json)
// .ignoreAcceptHeader(false) // 优先使用Accept头 (默认)
// .defaultContentType(MediaType.APPLICATION_JSON) // 默认返回JSON
// .mediaType("json", MediaType.APPLICATION_JSON)
// .mediaType("xml", MediaType.APPLICATION_XML);
// }
// }
通过这种方式,Spring MVC 提供了一个强大且灵活的机制,使RESTful API能够根据客户端的需求返回不同格式的数据,而Controller层的代码保持简洁,不需要关心具体的序列化细节。