bigywater.tech
Spring · 46

内容协商与 Accept

Accept 头到底怎么决定返回 JSON 还是 XML?为什么返回 String 时 Content-Type 说是 json 内容却不是?.json 后缀还能用吗?406 和 415 有什么区别?这篇在 Spring Boot 4.0.8 的 Spring MVC 7.0.9 上逐个请求验证。

406 / 415
Accept 不满足是 406,Content-Type 不满足是 415
404
路径后缀 /n/user.json 在本版本下不被识别(404)
参数 > Accept
开启 favor-parameter 后 ?format=json 压过 Accept: application/xml

Accept 与权重

实测 · 本机执行输出
  GET /n/user (无 Accept) → 200 [application/json] {"name":"Tom","age":20}
  GET /n/user Accept: */* → 200 [application/json] {"name":"Tom","age":20}
  GET /n/user Accept: application/json → 200 [application/json] {"name":"Tom","age":20}
  GET /n/user Accept: application/xml → 200 [application/xml;charset=UTF-8] <User><name>Tom</name><age>20</age></User>
  GET /n/user Accept: application/json;q=0.5, application/xml;q=0.9 → 200 [application/xml;charset=UTF-8] <User><name>Tom</name><age>20</age></User>
  GET /n/user Accept: text/html → 406 [text/html;charset=UTF-8] <html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are s
  GET /n/user Accept: text/plain → 406 [-] 
  GET /n/user Accept: application/xml, application/json → 200 [application/xml;charset=UTF-8] <User><name>Tom</name><age>20</age></User>

q 值大的赢,没有 Accept 或 */* 取第一个能写出该对象的转换器。

String 的陷阱

实测 · 本机执行输出
  GET /n/str (无 Accept) → 200 [text/plain;charset=UTF-8] plain-string
  GET /n/str Accept: application/json → 200 [application/json] plain-string
  GET /n/str Accept: text/plain → 200 [text/plain;charset=UTF-8] plain-string
  GET /n/str Accept: application/xml → 200 [application/xml;charset=UTF-8] plain-string
  GET /n/str Accept: text/html → 200 [text/html;charset=UTF-8] plain-string

String 返回值原样写出,只是按 Accept 贴 Content-Type。

produces 与 ResponseEntity

实测 · 本机执行输出
  GET /n/json-only Accept: application/xml → 406 [application/xml;charset=UTF-8] <Map><timestamp>2026-10-19T03:38:12.497Z</timestamp><status>406</status><error>Not Acceptable</error><path>/n/
  GET /n/json-only Accept: application/json → 200 [application/json] {"name":"Tom","age":20}
  GET /n/json-only (无 Accept) → 200 [application/json] {"name":"Tom","age":20}
  GET /n/two Accept: application/json → 200 [application/json] {"from":"json-method"}
  GET /n/two Accept: application/xml → 200 [application/xml;charset=UTF-8] <from>xml-method</from>
  GET /n/two Accept: text/plain → 406 [-] 
  GET /n/two (无 Accept) → 200 [application/json] {"from":"json-method"}
  GET /n/entity Accept: application/json → 200 [application/xml] <User><name>Tom</name><age>20</age></User>
  GET /n/entity Accept: application/xml → 200 [application/xml] <User><name>Tom</name><age>20</age></User>

produces 参与映射;ResponseEntity 显式指定则不再协商。

后缀与参数

实测 · 本机执行输出
  GET /n/user.json (无 Accept) → 404 [application/json] {"status":404,"error":"Not Found","path":"/n/user.json"}
  GET /n/user.xml (无 Accept) → 404 [application/json] {"status":404,"error":"Not Found","path":"/n/user.xml"}
  GET /n/user?format=xml (无 Accept) → 200 [application/json] {"name":"Tom","age":20}
  GET /n/user?format=json Accept: application/xml → 200 [application/xml;charset=UTF-8] <User><name>Tom</name><age>20</age></User>

路径后缀不再识别;?format= 默认也不识别。

打开参数策略

实测 · 本机执行输出
  GET /n/user?format=xml (无 Accept) → 200 [application/xml;charset=UTF-8] <User><name>Tom</name><age>20</age></User>
  GET /n/user?format=json Accept: application/xml → 200 [application/json] {"name":"Tom","age":20}
  GET /n/user?format=csv (无 Accept) → 406 [-] 

favor-parameter 打开后参数优先于 Accept。

请求体

实测 · 本机执行输出
  POST /n/in (无 Accept) Content-Type: application/json → 200 [text/plain;charset=UTF-8] json-in User[name=Tom, age=20]
  POST /n/in (无 Accept) Content-Type: application/xml → 415 [application/json] {"status":415,"error":"Unsupported Media Type","path":"/n/in"}
  POST /n/in (无 Accept) Content-Type: text/plain → 415 [application/json] {"status":415,"error":"Unsupported Media Type","path":"/n/in"}
  POST /n/in (无 Accept) → 415 [application/json] {"status":415,"error":"Unsupported Media Type","path":"/n/in"}
  POST /n/any (无 Accept) Content-Type: application/xml → 200 [text/plain;charset=UTF-8] any-in User[name=Tom, age=20]
  POST /n/any (无 Accept) Content-Type: text/plain → 415 [application/json] {"status":415,"error":"Unsupported Media Type","path":"/n/any"}

Content-Type 不满足是 415。

分步演示

内容协商
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用 Spring Boot 4.0.8 的 SpringApplication(spring-boot-starter-webmvc + tools.jackson.dataformat:jackson-dataformat-xml,内嵌 Tomcat 11);每个请求由 Python 的 http.client 发出,响应的 Content-Type 取自真实响应头;响应里的 timestamp 在输出时被略去
    • 没有验证:自定义 HttpMessageConverter 与自定义媒体类型;通过 WebMvcConfigurer.configureContentNegotiation 编程配置;Accept-Language 的协商;错误页(/error)的 html 与 json 选择细节;WebFlux 的内容协商;gzip 等 Content-Encoding 的协商
    京ICP备2026067502号-1京公网安备11010502063487号