内容协商(Content Negotiation)是Spring MVC提供的一种机制,它允许客户端和服务器协商响应的内容类型。Spring Boot在此基础上进行了自动配置和简化。
1、内容协商基本概念
内容协商机制主要涉及以下三个方面:
- 客户端通过
Accept头指定希望接收的响应格式 - 服务器根据请求和自身能力决定返回的格式
- 服务器通过
Content-Type头告知客户端返回的内容格式
2、Spring Boot内容协商实现方式
Spring Boot支持以下几种内容协商方式:
基于HTTP Accept头
这是最标准的内容协商方式,客户端在请求头中指定Accept,如:
java
Accept: application/json
基于URL后缀
通过在URL路径中添加后缀来指定格式,如:
/api/user.json- 返回JSON格式/api/user.xml- 返回XML格式
基于请求参数
通过请求参数指定格式,默认参数名为format,如:
/api/user?format=json
3、Spring Boot内容协商配置
默认配置
Spring Boot默认启用了基于Accept头和基于后缀的内容协商,支持JSON格式(需要Jackson库)和XML格式(需要Jackson XML扩展或JAXB)。
自定义配置
可以在application.properties或application.yml中进行配置:
properties
# 启用/禁用基于后缀的内容协商
spring.mvc.contentnegotiation.favor-path-extension=true
# 启用/禁用基于参数的内容协商
spring.mvc.contentnegotiation.favor-parameter=false
# 自定义参数名称(如果启用基于参数的内容协商)
spring.mvc.contentnegotiation.parameter-name=format
# 注册的媒体类型映射
spring.mvc.contentnegotiation.media-types.pdf=application/pdf
4、代码示例
基本控制器示例:
java
@RestController
@RequestMapping("/api/users")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
return new User(id, "张三", "zhangsan@example.com");
}
@GetMapping(value = "/{id}", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public User getUserWithProduces(@PathVariable Long id) {
return new User(id, "李四", "lisi@example.com");
}
}
// User实体类
@Data
public class User {
private Long id;
private String name;
private String email;
}
自定义HttpMessageConverter:
如果你想支持自定义格式(如PDF),可以这样做:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.favorPathExtension(true)
.favorParameter(true)
.parameterName("mediaType")
.ignoreAcceptHeader(false)
.useRegisteredExtensionsOnly(false)
.defaultContentType(MediaType.APPLICATION_JSON)
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML);
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// 添加自定义的MessageConverter
converters.add(new PdfMessageConverter());
}
}
// 自定义PDF转换器
public class PdfMessageConverter extends AbstractHttpMessageConverter<User> {
public PdfMessageConverter() {
super(new MediaType("application", "pdf"));
}
@Override
protected boolean supports(Class<?> clazz) {
return User.class.isAssignableFrom(clazz);
}
@Override
protected User readInternal(Class<? extends User> clazz, HttpInputMessage inputMessage) {
throw new UnsupportedOperationException("Not implemented");
}
@Override
protected void writeInternal(User user, HttpOutputMessage outputMessage) {
// 实现将User对象转换为PDF的逻辑
// 这里只是示例,实际需要PDF生成库如iText
try {
outputMessage.getBody().write(("PDF Report for User: " + user.getName()).getBytes());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
使用不同格式请求
JSON格式请求
GET /api/users/1 HTTP/1.1
Accept: application/json
或
GET /api/users/1.json HTTP/1.1
XML格式请求
GET /api/users/1 HTTP/1.1
Accept: application/xml
或
GET /api/users/1.xml HTTP/1.1
PDF格式请求
GET /api/users/1 HTTP/1.1
Accept: application/pdf
或
GET /api/users/1.pdf HTTP/1.1
5、内容协商的工作原理
ContentNegotiationManager负责确定请求的媒体类型- 根据配置(Accept头、路径后缀、参数)确定最匹配的媒体类型
- 查找能够处理该媒体类型的
HttpMessageConverter - 使用找到的转换器进行响应内容的序列化
6、常见问题解决
1. 返回格式不符合预期
确保:
- 添加了相应的依赖(如Jackson XML或JAXB)
- 正确设置了
Accept头 - 控制器方法上使用
produces限定了支持的媒体类型
2. 添加自定义格式
需要:
- 实现自定义的
HttpMessageConverter - 注册媒体类型映射
- 确保转换器被添加到转换器列表中
7、最佳实践
- REST API优先使用基于Accept头的内容协商
- 对于浏览器友好的API,可以同时支持基于后缀的内容协商
- 明确指定控制器支持的媒体类型(使用
produces) - 为自定义格式提供合适的
HttpMessageConverter