同一个接口,浏览器返回 XML、Apifox 返回 JSON?一次 Spring Boot 内容协商排查记录
问题现象
项目里有一个再普通不过的登录接口:
@RestController
@RequestMapping("/user")
public class UserController {
@GetMapping("/login")
public Result<User> login(@RequestParam(defaultValue = "guest") String username,
@RequestParam(defaultValue = "123456") String password) {
if (username == null || username.trim().isEmpty()) {
return Result.error(ResultCode.BAD_REQUEST, "用户名不能为空");
}
return Result.success(new User(username, password));
}
}
同一个接口、同一份代码,却出现了诡异的现象:
- 用 Apifox / Postman 请求,返回的是 JSON
- 用 浏览器地址栏直接访问,返回的却是 XML
代码里既没写 produces,也没做任何格式相关处理,为什么会这样?
根本原因:Spring MVC 的内容协商机制
答案在于 Spring MVC 的 内容协商(Content Negotiation)。
Spring 决定用什么格式序列化响应,默认会参考请求头里的 Accept。而不同客户端发送的 Accept 是不一样的:
| Apifox / Postman | Accept: */* | 无明确偏好,走默认 JSON |
| 浏览器 | Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 | 明确带 application/xml,优先级高于 */* |
关键就在浏览器这一行。它带的 application/xml;q=0.9 权重(0.9)高于 */*;q=0.8(0.8)。当 classpath 上恰好存在能处理 XML 的 HttpMessageConverter 时,Spring 就"投其所好",返回了 XML。
Apifox 只发 */*,没有明确偏好,Spring 就用它默认的 Jackson JSON 转换器,所以一直是 JSON。
同样的接口,因为请求头不同,走了不同的消息转换器。
定位:XML 转换器是从哪冒出来的?
那么问题变成了:我并没有主动引入 XML 支持,classpath 上怎么会有 XML 转换器?
Spring Boot 有个自动配置逻辑——只要检测到 jackson-dataformat-xml 在 classpath 上,就会自动注册 MappingJackson2XmlHttpMessageConverter。
于是用 dependency:tree 排查依赖树:
./mvnw dependency:tree | grep -E "dataformat-xml|woodstox|stax2"
结果发现了它们:
com.fasterxml.jackson.dataformat:jackson-dataformat-xml:jar:2.21.4:compile
├─ org.codehaus.woodstox:stax2-api:jar:4.2.2:compile
└─ com.fasterxml.woodstox:woodstox-core:jar:7.1.1:compile
继续追它的来源:
./mvnw dependency:tree | grep -E "com.aliyun|hutool|dataformat-xml"
+- com.aliyun:alibabacloud-oss-v2:jar:0.5.0:compile
| +- com.fasterxml.jackson.dataformat:jackson-dataformat-xml:jar:2.21.4:compile
\\- cn.hutool:hutool-all:jar:5.8.47:compile
真相大白:jackson-dataformat-xml 是被阿里云 OSS SDK com.aliyun:alibabacloud-oss-v2 传递引入的,我自己从来没直接依赖过它。就是这个"隐形"的传递依赖,触发了 Spring 的 XML 自动配置。
解决方案
针对这个问题,有三种思路,从"锁单个接口"到"全局兜底"再到"彻底根治":
方案一:给单个接口写死 produces(临时)
@GetMapping(value = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
只影响这一个接口,其它接口浏览器访问依旧返回 XML。适合临时救急,不适合全局。
方案二:配置内容协商,强制默认 JSON(全局兜底)
新建一个 WebConfig,把默认响应类型设为 JSON,并关闭基于 URL 参数的协商:
package com.example.springboot4.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer
.defaultContentType(MediaType.APPLICATION_JSON)
.favorParameter(false);
}
}
这个配置的核心是设置了 默认类型,把 JSON 在协商链里提到更优先的位置,浏览器那种 application/xml;q=0.9 就不再优先命中 XML。
如果想在配置文件里做,也可以:
spring:
mvc:
contentnegotiation:
favor-parameter: false
方案三:排除多余的 XML 依赖(治本)
既然 XML 转换器是因为 jackson-dataformat-xml 存在才被注册的,那把这个传递依赖排除掉,classpath 上没有 XML converter,Spring 就压根没法返回 XML:
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>alibabacloud-oss-v2</artifactId>
<version>0.5.0</version>
<scope>compile</scope>
<exclusions>
<!– 排除会触发 Spring 内容协商返回 XML 的传递依赖 –>
<exclusion>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</exclusion>
</exclusions>
</dependency>
排除后再验证一次依赖树,确认干净了:
./mvnw dependency:tree | grep -E "dataformat-xml|woodstox|stax2"
# 无任何输出,说明已彻底移除
注意:阿里云 OSS v2 SDK 内部解析 XML 响应用的是它自己的逻辑,通常不依赖这个 Jackson XML 模块。排除后建议跑一下实际的 OSS 上传/下载功能验证。如果报 ClassNotFoundException 之类的错,就去掉 <exclusions>,只保留方案二的 WebConfig 兜底即可。
最终采用的方案
我采用了 方案二 + 方案三双保险:
两层防护到位后,编译通过,浏览器访问 /user/login 稳定返回 JSON,问题解决。


