首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >炸锅了!HttpMediaTypeNotAcceptableException 血案背后,竟藏着 Spring MVC 最阴险的潜规则!

炸锅了!HttpMediaTypeNotAcceptableException 血案背后,竟藏着 Spring MVC 最阴险的潜规则!

原创
作者头像
疯狂的KK
发布2025-07-14 14:48:17
发布2025-07-14 14:48:17
3860
举报
文章被收录于专栏:Java项目实战Java项目实战

凌晨三点,运维小哥的电话像炸雷一样劈开我的美梦:“王架构!线上订单接口崩了!几千单卡着支付,用户快把客服电话打爆了!”

我猛地坐起,电脑开机的 30 秒里,手心全是汗。监控面板上,刺眼的红色错误刷屏 ——org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation。这个报错像个幽灵,上周测试环境刚出现过,当时以为是小问题,改了改就没在意,没想到在线上掀起了血案!

如果你是 Java 开发者,没见过这个报错算我输;但如果你敢说 “我完全懂它的套路”,那我劝你先别急着吹牛。今天这篇文章,我会扒开这个报错的层层伪装,带你看透它背后 Spring MVC 最阴险的潜规则,再送你一套 “防坑核武器”,让你从此跟 406 报错说永别!

一、从一场惊心动魄的线上故障说起:这个报错到底有多 “坑”?

先给大家看一段惊心动魄的日志(已脱敏):

代码语言:javascript
复制
2025-07-10 02:37:21.456 ERROR 12345 --- [nio-8080-exec-5] o.s.web.servlet.handler.AbstractHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation]2025-07-10 02:37:22.103 WARN 12345 --- [nio-8080-exec-7] o.s.web.servlet.PageNotFound : No mapping for GET /error

就是这两行破日志,让我们付出了直接经济损失 23 万 +,紧急发版 3 次,团队通宵排查的代价。更气人的是,本地测试、QA 环境全是好的,一到生产就炸锅,简直像个故意找茬的 “幽灵 bug”。

1.1 报错现场还原:它总在 “不经意间” 给你致命一击

这个报错的诡异之处在于:

  • 同一个接口,用 Postman 测Accept: application/json没问题,换成Accept: */*就报错
  • 本地调试时返回正常,部署到生产就随机出现 406 状态码
  • 加了@ResponseBody注解,返回的明明是 JSON 对象,却提示 “找不到可接受的表示”

如果你以为这只是个简单的 “格式不匹配”,那可太天真了。这背后藏着 Spring MVC 消息转换机制的 “潜规则”,90% 的开发者踩坑后都只会瞎改配置,根本没摸到门道。

二、庖丁解牛:HttpMediaTypeNotAcceptableException 的本质是什么?

在开始排坑前,我们必须先搞懂:这个报错到底在说什么?

2.1 官方解释的 “翻译官” 版本

Spring 官方文档对这个异常的描述是:“服务器无法根据请求头中的 Accept 字段,找到合适的响应表示形式”。

用人话翻译一下就是:

  • 客户端(比如浏览器、APP)在请求时,通过Accept头告诉服务器:“我只看得懂这些格式哦(比如 application/json、text/html)”
  • 服务器处理完请求后,想返回数据,却发现自己能生成的格式(通过Content-Type标识),没有一个在客户端的 “可接受列表” 里
  • 服务器急了:“我能给的你不要,你要的我没有,这活儿没法干!” 于是就抛出了这个异常,通常还会附带 406 Not Acceptable 的状态码

2.2 举个栗子:超市购物的 “不匹配” 场景

你去超市买饮料,对收银员说:“我只喝可乐(Accept: 可乐)”。

收银员翻了半天,货架上只有雪碧、果汁(服务器能生成的 Content-Type)。

这时候收银员只能对你说:“抱歉,没有你要的(HttpMediaTypeNotAcceptableException)”。

这个逻辑很简单,但在 Spring MVC 中,因为加入了 “消息转换器”“返回值处理器” 等中间角色,问题就变得异常复杂。

三、深挖根源:为什么会出现 “找不到可接受的表示”?

经过我们团队 3 次通宵排查,结合 5 个生产环境案例,总结出这个报错的 4 大 “罪魁祸首”,每个都藏得极深!

3.1 罪魁祸首一:Accept 头与返回类型的 “鸡同鸭讲”

最常见的原因是:客户端的Accept头太 “挑”,而服务器返回的数据格式不在其列表中。

场景再现:

前端开发小王为了 “规范”,在请求头里硬编码了Accept: application/xml,但后端接口返回的是@RestController默认的 JSON 格式(Content-Type: application/json)。

代码证据:
代码语言:javascript
复制
// 前端请求(错误示例)fetch('/api/order', {  headers: {    'Accept': 'application/xml' // 只接受XML  }})// 后端接口@RestControllerpublic class OrderController {  @GetMapping("/api/order")  public OrderDTO getOrder() { // 返回JSON格式    return new OrderDTO(1L, "iPhone 15");  }}
为什么会报错?
  • @RestController默认依赖MappingJackson2HttpMessageConverter,只能生成 JSON 格式(application/json)
  • 客户端明确说只接受application/xml,两者不匹配,直接触发 406
隐藏陷阱:

就算客户端设置Accept: */*(表示接受所有格式),也可能报错!因为服务器可能 “啥都给不了”(比如返回 null 时没有合适的转换器)。

3.2 罪魁祸首二:消息转换器 “罢工” 或 “配置错乱”

Spring MVC 靠HttpMessageConverter(消息转换器)把 Java 对象转换成客户端能看懂的格式(比如 JSON、XML)。如果转换器出问题,服务器就 “不会说话” 了。

常见坑点:
  1. 缺少必要的转换器依赖:比如用了@ResponseBody返回 JSON,但没加 Jackson 依赖,导致MappingJackson2HttpMessageConverter没生效。
代码语言:javascript
复制
<!-- 漏了这个依赖,JSON转换器就没法工作 --><dependency>  <groupId>com.fasterxml.jackson.core</groupId>  <artifactId>jackson-databind</artifactId>  <version>2.15.2</version></dependency>
  1. 转换器顺序配置错误:Spring 会按转换器的注册顺序尝试转换,如果前面的转换器 “抢活干” 却干不了,就会导致后面的转换器没机会处理。

比如,StringHttpMessageConverter排在MappingJackson2HttpMessageConverter前面,当返回String类型时,它会优先处理,但如果客户端要的是 JSON,就会报错。

  1. 自定义转换器覆盖了默认配置:有些开发者为了支持特殊格式(比如 protobuf),自定义了转换器,却忘了保留默认的 JSON 转换器,导致普通接口无法返回 JSON。
代码语言:javascript
复制
// 错误示例:自定义转换器覆盖了默认配置@Configurationpublic class WebConfig implements WebMvcConfigurer {  @Override  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {    // 只加了自定义转换器,没加默认的Jackson转换器    converters.add(new ProtobufHttpMessageConverter());  }}

这会导致所有需要返回 JSON 的接口都找不到合适的转换器,直接报错。

3.3 罪魁祸首三:返回值处理 “卡壳”(比如返回 null)

你以为返回null很简单?大错特错!如果接口返回null,而此时没有能处理null的转换器,就会触发 406。

场景还原:

后端接口查询订单,没找到数据时返回null,而客户端请求头是Accept: application/json。

代码语言:javascript
复制
@GetMapping("/api/order/{id}")public OrderDTO getOrder(@PathVariable Long id) {  OrderDO order = orderMapper.selectById(id);  return order == null ? null : convert(order); // 可能返回null}
为什么会报错?
  • 默认的MappingJackson2HttpMessageConverter在处理null时,会根据配置决定是否返回application/json(空 JSON)
  • 如果配置了spring.jackson.serialization-inclusion=NON_NULL,同时返回null,转换器可能认为 “没有可序列化的内容”,拒绝处理
  • 此时没有其他转换器能处理null,就会抛出异常
隐藏细节:

Spring 的RequestResponseBodyMethodProcessor在处理返回值时,如果所有转换器都拒绝处理(canWrite返回 false),就会触发 406。

3.4 罪魁祸首四:全局异常处理器 “好心办坏事”

全局异常处理器(@ControllerAdvice)本应处理异常,但如果配置不当,会 “雪上加霜”。

典型错误:

异常处理器返回的格式与客户端Accept头不匹配,比如客户端要 JSON,处理器却返回了 String。

代码语言:javascript
复制
@ControllerAdvicepublic class GlobalExceptionHandler {  @ExceptionHandler(BusinessException.class)  public String handleBusinessException(BusinessException e) {    // 错误:返回String(默认Content-Type: text/plain)    return e.getMessage();  }}

如果客户端请求头是Accept: application/json,这个处理器返回的text/plain就会导致 406,而且会覆盖原始异常,增加排查难度。

四、终极解决方案:4 大场景的 “排坑指南”(附实战代码)

知道了根源,解决起来就有章法了。下面是我们团队在生产环境验证过的 “排坑指南”,每个方案都附带可直接复制的代码。

4.1 解决 “Accept 头与返回类型不匹配”

方案 1:明确指定接口支持的格式(produces 属性)

在@RequestMapping或@GetMapping中用produces指定接口能返回的格式,Spring 会自动校验与Accept头是否匹配。

代码语言:javascript
复制
// 正确示例:明确支持JSON格式@GetMapping(value = "/api/order", produces = MediaType.APPLICATION_JSON_VALUE)public OrderDTO getOrder() {  return new OrderDTO(1L, "iPhone 15");}
  • 如果客户端Accept头不包含application/json,Spring 会直接返回 406,避免走到业务逻辑后才报错
  • 建议在所有接口上显式指定produces,增加可读性和约束性
方案 2:让客户端放宽 “要求”(适合前端可控的场景)

如果前端可以修改,将Accept头设置为application/json, */*;q=0.9(优先 JSON,也接受其他格式)。

代码语言:javascript
复制
// 前端请求优化fetch('/api/order', {  headers: {    'Accept': 'application/json, */*;q=0.9' // 放宽要求  }})

q=0.9表示权重,数字越大优先级越高,这样既优先接受 JSON,又能兼容其他格式。

4.2 解决 “消息转换器配置问题”

方案 1:确保必要的转换器依赖(Maven/Gradle)

不管用不用 JSON,都建议在 pom.xml 中加入 Jackson 依赖(Spring Boot 默认已包含,但以防万一):

代码语言:javascript
复制
<!-- JSON转换器依赖 --><dependency>  <groupId>com.fasterxml.jackson.core</groupId>  <artifactId>jackson-databind</artifactId></dependency><!-- 如果需要XML支持,再加这个 --><dependency>  <groupId>com.fasterxml.jackson.dataformat</groupId>  <artifactId>jackson-dataformat-xml</artifactId></dependency>
方案 2:正确配置消息转换器(保留默认 + 添加自定义)

自定义转换器时,不要覆盖默认转换器,而是在默认基础上添加,避免 “顾此失彼”。

代码语言:javascript
复制
@Configurationpublic class WebConfig implements WebMvcConfigurer {  @Override  public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {    // 关键:用extend而不是set,保留默认转换器    // 添加自定义转换器(比如protobuf)    converters.add(new ProtobufHttpMessageConverter());        // 可选:调整Jackson转换器的顺序(放到前面优先处理JSON)    for (int i = 0; i < converters.size(); i++) {      if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {        MappingJackson2HttpMessageConverter jacksonConverter = (MappingJackson2HttpMessageConverter) converters.remove(i);        converters.add(0, jacksonConverter); // 放到第一个        break;      }    }  }}

注意:用extendMessageConverters而不是configureMessageConverters,前者会保留默认转换器,后者会覆盖。

4.3 解决 “返回值为 null 导致的转换失败”

方案 1:返回统一响应体(避免 null)

定义一个通用的ApiResponse类,无论成功失败都返回这个对象,避免直接返回null。

代码语言:javascript
复制
// 通用响应体@Datapublic class ApiResponse<T> {  private int code;  private String message;  private T data;  public static <T> ApiResponse<T> success(T data) {    ApiResponse<T> response = new ApiResponse<>();    response.code = 200;    response.message = "success";    response.data = data; // 允许data为null,但外层对象非null    return response;  }}// 接口改造@GetMapping("/api/order/{id}")public ApiResponse<OrderDTO> getOrder(@PathVariable Long id) {  OrderDO order = orderMapper.selectById(id);  return ApiResponse.success(order == null ? null : convert(order));}

这样即使data是null,外层的ApiResponse对象也能被 Jackson 转换器序列化为 JSON({"code":200,"message":"success","data":null}),避免转换失败。

方案 2:配置 Jackson 处理 null 的策略

在application.yml中配置 Jackson 序列化时包含 null 字段:

代码语言:javascript
复制
spring:  jackson:    serialization-inclusion: ALWAYS # 总是包含null字段(默认就是ALWAYS)    default-property-inclusion: ALWAYS

如果用 Java 配置:

代码语言:javascript
复制
@Beanpublic MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {  MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();  ObjectMapper objectMapper = new ObjectMapper();  // 包含所有字段(包括null)  objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);  converter.setObjectMapper(objectMapper);  return converter;}

这确保 Jackson 转换器会处理包含 null 的对象,不会因为有 null 字段而拒绝转换。

4.4 解决 “全局异常处理器格式不匹配”

方案:异常处理器返回 JSON 格式(与接口保持一致)

让全局异常处理器返回ApiResponse(和正常接口格式一致),并指定produces为 JSON。

代码语言:javascript
复制
@ControllerAdvicepublic class GlobalExceptionHandler {  // 明确返回JSON格式  @ExceptionHandler(BusinessException.class)  @ResponseBody  @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)  public ApiResponse<Void> handleBusinessException(BusinessException e) {    ApiResponse<Void> response = new ApiResponse<>();    response.code = e.getCode();    response.message = e.getMessage();    return response;  }  // 处理其他异常(比如404、500)  @ExceptionHandler(Exception.class)  @ResponseBody  @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE)  public ApiResponse<Void> handleException(Exception e) {    ApiResponse<Void> response = new ApiResponse<>();    response.code = 500;    response.message = "服务器内部错误";    return response;  }}

这样无论发生什么异常,返回的都是客户端预期的 JSON 格式,避免因异常处理导致的 406。

五、防患于未然:3 大措施让你永远避开这个坑

解决问题不如避免问题。结合我们团队的实践,这 3 个措施能从根源上杜绝HttpMediaTypeNotAcceptableException。

5.1 制定接口开发规范(附规范文档模板)

强制规范

  1. 所有接口必须用@RestController,默认返回 JSON 格式
  2. 所有接口必须显式指定produces = MediaType.APPLICATION_JSON_VALUE
  3. 统一返回ApiResponse对象,禁止直接返回 POJO 或 null
  4. 全局异常处理器必须返回ApiResponse,且produces为 JSON
  5. 前端请求默认Accept: application/json, */*;q=0.9,不允许指定特殊格式(除非特殊需求)

规范文档模板(可直接套用):

代码语言:javascript
复制
# 接口开发规范V1.0## 1. 响应格式- 统一使用ApiResponse<T>作为返回体- 成功:{"code":200,"message":"success","data":...}- 失败:{"code":xxx,"message":"错误信息","data":null}## 2. 注解要求- 类上必须加@RestController- 方法上必须加@GetMapping/@PostMapping等,并指定produces  示例:@GetMapping(value = "/api/xxx", produces = MediaType.APPLICATION_JSON_VALUE)## 3. 异常处理- 业务异常必须抛BusinessException,由全局处理器统一处理- 禁止在接口中捕获异常后返回String或其他非ApiResponse格式

5.2 加入单元测试和接口测试(覆盖 Accept 头场景)

用 JUnit 和 Spring Test 模拟不同的Accept头,验证接口是否正常响应。

代码语言:javascript
复制
@SpringBootTest@AutoConfigureMockMvcpublic class OrderControllerTest {  @Autowired  private MockMvc mockMvc;  // 测试Accept: application/json(正常场景)  @Test  public void testGetOrderWithJsonAccept() throws Exception {    mockMvc.perform(get("/api/order/1")            .header("Accept", "application/json"))            .andExpect(status().isOk())            .andExpect(content().contentType(MediaType.APPLICATION_JSON));  }  // 测试Accept: application/xml(应该返回406)  @Test  public void testGetOrderWithXmlAccept() throws Exception {    mockMvc.perform(get("/api/order/1")            .header("Accept", "application/xml"))            .andExpect(status().isNotAcceptable()); // 预期406  }  // 测试返回null的场景(应该返回JSON)  @Test  public void testGetNonExistOrder() throws Exception {    mockMvc.perform(get("/api/order/999")            .header("Accept", "application/json"))            .andExpect(status().isOk())            .andExpect(jsonPath("$.code").value(200))            .andExpect(jsonPath("$.data").doesNotExist()); // 假设不存在时data为null  }}

这些测试能在开发阶段就发现格式不匹配的问题,避免带到生产环境。

5.3 线上监控告警(406 状态码实时预警)

在监控系统中(比如 Prometheus + Grafana)添加 406 状态码的监控指标,一旦出现就立即告警。

Prometheus 配置示例

代码语言:javascript
复制
groups:- name: http_status_rules  rules:  - alert: Http406ErrorIncreased    expr: sum(rate(http_server_requests_seconds_count{status="406"}[5m])) > 0    for: 1m    labels:      severity: critical    annotations:      summary: "406错误数量增加"      description: "过去5分钟内出现{{ $value }}次406错误,可能是HttpMediaTypeNotAcceptableException导致"

同时,在日志收集系统(比如 ELK)中添加 406 状态码和异常关键字的检索,方便快速定位问题接口。

六、总结:从 “踩坑” 到 “避坑” 的核心思维

HttpMediaTypeNotAcceptableException看似是个小问题,实则暴露了开发者对 Spring MVC 消息转换机制的理解不足。解决它的核心思维是:

  1. 明确 “供需关系”:客户端的Accept是 “需求”,服务器的Content-Type是 “供给”,必须匹配
  2. 掌控 “转换器”:理解消息转换器的工作原理,确保 JSON 等常用转换器正常配置且顺序正确
  3. 统一 “响应格式”:用通用响应体(如ApiResponse)避免 null 导致的转换问题
  4. 规范 “异常处理”:全局异常处理器的返回格式必须与正常接口一致

记住:在 Spring MVC 中,“隐式约定” 越多的地方,越容易踩坑。把隐式的规则显式化(比如显式指定produces),是避免大多数问题的关键。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、从一场惊心动魄的线上故障说起:这个报错到底有多 “坑”?
    • 1.1 报错现场还原:它总在 “不经意间” 给你致命一击
  • 二、庖丁解牛:HttpMediaTypeNotAcceptableException 的本质是什么?
    • 2.1 官方解释的 “翻译官” 版本
    • 2.2 举个栗子:超市购物的 “不匹配” 场景
  • 三、深挖根源:为什么会出现 “找不到可接受的表示”?
    • 3.1 罪魁祸首一:Accept 头与返回类型的 “鸡同鸭讲”
      • 场景再现:
      • 代码证据:
      • 为什么会报错?
      • 隐藏陷阱:
    • 3.2 罪魁祸首二:消息转换器 “罢工” 或 “配置错乱”
      • 常见坑点:
    • 3.3 罪魁祸首三:返回值处理 “卡壳”(比如返回 null)
      • 场景还原:
      • 为什么会报错?
      • 隐藏细节:
    • 3.4 罪魁祸首四:全局异常处理器 “好心办坏事”
      • 典型错误:
  • 四、终极解决方案:4 大场景的 “排坑指南”(附实战代码)
    • 4.1 解决 “Accept 头与返回类型不匹配”
      • 方案 1:明确指定接口支持的格式(produces 属性)
      • 方案 2:让客户端放宽 “要求”(适合前端可控的场景)
    • 4.2 解决 “消息转换器配置问题”
      • 方案 1:确保必要的转换器依赖(Maven/Gradle)
      • 方案 2:正确配置消息转换器(保留默认 + 添加自定义)
    • 4.3 解决 “返回值为 null 导致的转换失败”
      • 方案 1:返回统一响应体(避免 null)
      • 方案 2:配置 Jackson 处理 null 的策略
    • 4.4 解决 “全局异常处理器格式不匹配”
      • 方案:异常处理器返回 JSON 格式(与接口保持一致)
  • 五、防患于未然:3 大措施让你永远避开这个坑
    • 5.1 制定接口开发规范(附规范文档模板)
    • 5.2 加入单元测试和接口测试(覆盖 Accept 头场景)
    • 5.3 线上监控告警(406 状态码实时预警)
  • 六、总结:从 “踩坑” 到 “避坑” 的核心思维
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档