Appearance
Spring MVC 内容协商 (Content Negotiation) 深度解析 🎯
什么是内容协商?为什么需要它?
想象一下这样的场景:你开发了一个 API,既要给前端 JavaScript 返回 JSON 数据,又要给移动端返回 XML 格式,还要给第三方系统提供不同的数据格式。如果为每种格式都写一个独立的接口,代码会变得非常冗余和难以维护。
NOTE
内容协商(Content Negotiation) 就是为了解决这个问题而生的!它让同一个 API 接口能够根据客户端的需求,智能地返回不同格式的数据。
Spring MVC 内容协商的工作原理
默认行为:基于 Accept 头部
Spring MVC 默认只检查 HTTP 请求的 Accept
头部来确定客户端期望的媒体类型。
kotlin
@RestController
@RequestMapping("/api/users")
class UserController {
@GetMapping
fun getUsers(): List<User> {
// 返回用户列表,Spring会根据Accept头部自动选择格式
return listOf(
User(1, "张三", "[email protected]"),
User(2, "李四", "[email protected]")
)
}
}
data class User(
val id: Long,
val name: String,
val email: String
)
http
# 请求JSON格式
GET /api/users HTTP/1.1
Accept: application/json
# 请求XML格式
GET /api/users HTTP/1.1
Accept: application/xml
# 请求任意格式
GET /api/users HTTP/1.1
Accept: */*
TIP
同一个 Controller 方法,不同的 Accept 头部,就能得到不同格式的响应!这就是内容协商的魅力所在。
自定义内容协商配置
基础配置:添加媒体类型映射
当你需要支持更多的内容格式或者自定义映射时,可以通过实现 WebMvcConfigurer
来配置:
kotlin
@Configuration
class WebConfiguration : WebMvcConfigurer {
override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
// 配置媒体类型映射
configurer.mediaType("json", MediaType.APPLICATION_JSON)
configurer.mediaType("xml", MediaType.APPLICATION_XML)
configurer.mediaType("yaml", MediaType.parseMediaType("application/yaml"))
// 设置默认内容类型
configurer.defaultContentType(MediaType.APPLICATION_JSON)
}
}
高级配置:多种协商策略
kotlin
@Configuration
class AdvancedWebConfiguration : WebMvcConfigurer {
override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
configurer
// 1. 基于URL参数的协商 (例如: /api/users?format=json)
.favorParameter(true)
.parameterName("format")
// 2. 基于URL路径扩展名的协商 (例如: /api/users.json)
.favorPathExtension(false) // [!code warning] // 出于安全考虑,建议禁用
// 3. 忽略Accept头部
.ignoreAcceptHeader(false)
// 4. 设置默认内容类型
.defaultContentType(MediaType.APPLICATION_JSON)
// 5. 媒体类型映射
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
.mediaType("pdf", MediaType.APPLICATION_PDF)
}
}
WARNING
安全提醒:基于路径扩展名的内容协商(如 /api/users.json
)存在安全风险,Spring 官方建议使用查询参数方式替代。
实际业务场景应用
场景1:多格式数据导出API
kotlin
@RestController
@RequestMapping("/api/reports")
class ReportController {
@GetMapping("/sales")
fun getSalesReport(
@RequestParam startDate: LocalDate,
@RequestParam endDate: LocalDate
): SalesReport {
// 生成销售报告数据
return salesService.generateReport(startDate, endDate)
}
// 同一个方法,根据Accept头部返回不同格式:
// Accept: application/json -> JSON格式
// Accept: application/xml -> XML格式
// Accept: application/pdf -> PDF格式(需要自定义MessageConverter)
}
data class SalesReport(
val period: String,
val totalSales: BigDecimal,
val orders: List<OrderSummary>
)
场景2:API版本化与格式协商结合
kotlin
@RestController
@RequestMapping("/api/v1/products")
class ProductController {
@GetMapping
fun getProducts(
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "10") size: Int
): Page<Product> {
return productService.findAll(PageRequest.of(page, size))
}
@GetMapping("/{id}")
fun getProduct(@PathVariable id: Long): Product {
return productService.findById(id)
?: throw ResponseStatusException(HttpStatus.NOT_FOUND, "产品不存在")
}
}
完整的配置示例
kotlin
@Configuration
@EnableWebMvc
class WebMvcConfiguration : WebMvcConfigurer {
override fun configureContentNegotiation(configurer: ContentNegotiationConfigurer) {
configurer
// 启用基于参数的协商
.favorParameter(true)
.parameterName("format")
// 禁用基于路径扩展名的协商(安全考虑)
.favorPathExtension(false)
// 不忽略Accept头部
.ignoreAcceptHeader(false)
// 设置默认内容类型
.defaultContentType(MediaType.APPLICATION_JSON)
// 配置媒体类型映射
.mediaType("json", MediaType.APPLICATION_JSON)
.mediaType("xml", MediaType.APPLICATION_XML)
.mediaType("html", MediaType.TEXT_HTML)
.mediaType("csv", MediaType.parseMediaType("text/csv"))
}
// 配置消息转换器以支持更多格式
override fun configureMessageConverters(converters: MutableList<HttpMessageConverter<*>>) {
// 添加自定义的CSV转换器
converters.add(CsvHttpMessageConverter())
}
}
内容协商的优先级策略
Spring MVC 按照以下优先级顺序进行内容协商:
IMPORTANT
协商优先级:
- URL 参数(如果启用)
- HTTP Accept 头部
- 默认内容类型
常见问题与解决方案
问题1:406 Not Acceptable 错误
kotlin
// 错误示例:没有合适的消息转换器
@GetMapping("/data")
fun getData(): CustomObject {
return CustomObject() // [!code error] // 如果没有对应的MessageConverter会报406错误
}
// 解决方案:确保有对应的消息转换器或使用标准格式
@GetMapping("/data")
fun getData(): ResponseEntity<Map<String, Any>> {
val data = mapOf(
"message" to "success",
"data" to customService.getData()
)
return ResponseEntity.ok(data)
}
问题2:格式协商不生效
WARNING
确保在配置中正确设置了媒体类型映射,并且客户端发送了正确的 Accept 头部或参数。
kotlin
// 调试内容协商
@RestController
class DebugController {
@GetMapping("/debug")
fun debug(request: HttpServletRequest): Map<String, Any> {
return mapOf(
"acceptHeader" to request.getHeader("Accept"),
"formatParam" to request.getParameter("format"),
"contentType" to request.contentType
)
}
}
最佳实践建议 ✅
TIP
推荐做法:
- 优先使用 Accept 头部:这是 HTTP 标准的做法
- 谨慎使用路径扩展名:存在安全风险,建议禁用
- 提供查询参数作为备选:方便测试和某些特殊场景
- 设置合理的默认格式:通常是 JSON
- 充分测试各种格式:确保所有支持的格式都能正常工作
通过内容协商,你的 API 变得更加灵活和用户友好。同一个接口可以服务于不同的客户端需求,大大提高了代码的复用性和维护效率!🚀