
凌晨三点,运维小哥的电话像炸雷一样劈开我的美梦:“王架构!线上订单接口崩了!几千单卡着支付,用户快把客服电话打爆了!”
我猛地坐起,电脑开机的 30 秒里,手心全是汗。监控面板上,刺眼的红色错误刷屏 ——org.springframework.web.HttpMediaTypeNotAcceptableException: Could not find acceptable representation。这个报错像个幽灵,上周测试环境刚出现过,当时以为是小问题,改了改就没在意,没想到在线上掀起了血案!
如果你是 Java 开发者,没见过这个报错算我输;但如果你敢说 “我完全懂它的套路”,那我劝你先别急着吹牛。今天这篇文章,我会扒开这个报错的层层伪装,带你看透它背后 Spring MVC 最阴险的潜规则,再送你一套 “防坑核武器”,让你从此跟 406 报错说永别!
先给大家看一段惊心动魄的日志(已脱敏):
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”。
这个报错的诡异之处在于:
如果你以为这只是个简单的 “格式不匹配”,那可太天真了。这背后藏着 Spring MVC 消息转换机制的 “潜规则”,90% 的开发者踩坑后都只会瞎改配置,根本没摸到门道。
在开始排坑前,我们必须先搞懂:这个报错到底在说什么?
Spring 官方文档对这个异常的描述是:“服务器无法根据请求头中的 Accept 字段,找到合适的响应表示形式”。
用人话翻译一下就是:
你去超市买饮料,对收银员说:“我只喝可乐(Accept: 可乐)”。
收银员翻了半天,货架上只有雪碧、果汁(服务器能生成的 Content-Type)。
这时候收银员只能对你说:“抱歉,没有你要的(HttpMediaTypeNotAcceptableException)”。
这个逻辑很简单,但在 Spring MVC 中,因为加入了 “消息转换器”“返回值处理器” 等中间角色,问题就变得异常复杂。
经过我们团队 3 次通宵排查,结合 5 个生产环境案例,总结出这个报错的 4 大 “罪魁祸首”,每个都藏得极深!
最常见的原因是:客户端的Accept头太 “挑”,而服务器返回的数据格式不在其列表中。
前端开发小王为了 “规范”,在请求头里硬编码了Accept: application/xml,但后端接口返回的是@RestController默认的 JSON 格式(Content-Type: application/json)。
// 前端请求(错误示例)fetch('/api/order', { headers: { 'Accept': 'application/xml' // 只接受XML }})// 后端接口@RestControllerpublic class OrderController { @GetMapping("/api/order") public OrderDTO getOrder() { // 返回JSON格式 return new OrderDTO(1L, "iPhone 15"); }}就算客户端设置Accept: */*(表示接受所有格式),也可能报错!因为服务器可能 “啥都给不了”(比如返回 null 时没有合适的转换器)。
Spring MVC 靠HttpMessageConverter(消息转换器)把 Java 对象转换成客户端能看懂的格式(比如 JSON、XML)。如果转换器出问题,服务器就 “不会说话” 了。
<!-- 漏了这个依赖,JSON转换器就没法工作 --><dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version></dependency>比如,StringHttpMessageConverter排在MappingJackson2HttpMessageConverter前面,当返回String类型时,它会优先处理,但如果客户端要的是 JSON,就会报错。
// 错误示例:自定义转换器覆盖了默认配置@Configurationpublic class WebConfig implements WebMvcConfigurer { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { // 只加了自定义转换器,没加默认的Jackson转换器 converters.add(new ProtobufHttpMessageConverter()); }}这会导致所有需要返回 JSON 的接口都找不到合适的转换器,直接报错。
你以为返回null很简单?大错特错!如果接口返回null,而此时没有能处理null的转换器,就会触发 406。
后端接口查询订单,没找到数据时返回null,而客户端请求头是Accept: application/json。
@GetMapping("/api/order/{id}")public OrderDTO getOrder(@PathVariable Long id) { OrderDO order = orderMapper.selectById(id); return order == null ? null : convert(order); // 可能返回null}Spring 的RequestResponseBodyMethodProcessor在处理返回值时,如果所有转换器都拒绝处理(canWrite返回 false),就会触发 406。
全局异常处理器(@ControllerAdvice)本应处理异常,但如果配置不当,会 “雪上加霜”。
异常处理器返回的格式与客户端Accept头不匹配,比如客户端要 JSON,处理器却返回了 String。
@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,而且会覆盖原始异常,增加排查难度。
知道了根源,解决起来就有章法了。下面是我们团队在生产环境验证过的 “排坑指南”,每个方案都附带可直接复制的代码。
在@RequestMapping或@GetMapping中用produces指定接口能返回的格式,Spring 会自动校验与Accept头是否匹配。
// 正确示例:明确支持JSON格式@GetMapping(value = "/api/order", produces = MediaType.APPLICATION_JSON_VALUE)public OrderDTO getOrder() { return new OrderDTO(1L, "iPhone 15");}如果前端可以修改,将Accept头设置为application/json, */*;q=0.9(优先 JSON,也接受其他格式)。
// 前端请求优化fetch('/api/order', { headers: { 'Accept': 'application/json, */*;q=0.9' // 放宽要求 }})q=0.9表示权重,数字越大优先级越高,这样既优先接受 JSON,又能兼容其他格式。
不管用不用 JSON,都建议在 pom.xml 中加入 Jackson 依赖(Spring Boot 默认已包含,但以防万一):
<!-- 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>自定义转换器时,不要覆盖默认转换器,而是在默认基础上添加,避免 “顾此失彼”。
@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,前者会保留默认转换器,后者会覆盖。
定义一个通用的ApiResponse类,无论成功失败都返回这个对象,避免直接返回null。
// 通用响应体@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}),避免转换失败。
在application.yml中配置 Jackson 序列化时包含 null 字段:
spring: jackson: serialization-inclusion: ALWAYS # 总是包含null字段(默认就是ALWAYS) default-property-inclusion: ALWAYS如果用 Java 配置:
@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 字段而拒绝转换。
让全局异常处理器返回ApiResponse(和正常接口格式一致),并指定produces为 JSON。
@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 个措施能从根源上杜绝HttpMediaTypeNotAcceptableException。
强制规范:
规范文档模板(可直接套用):
# 接口开发规范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格式用 JUnit 和 Spring Test 模拟不同的Accept头,验证接口是否正常响应。
@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 }}这些测试能在开发阶段就发现格式不匹配的问题,避免带到生产环境。
在监控系统中(比如 Prometheus + Grafana)添加 406 状态码的监控指标,一旦出现就立即告警。
Prometheus 配置示例:
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 消息转换机制的理解不足。解决它的核心思维是:
记住:在 Spring MVC 中,“隐式约定” 越多的地方,越容易踩坑。把隐式的规则显式化(比如显式指定produces),是避免大多数问题的关键。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。