踩坑记:Content type 'application/json' 报错的那些事儿
先上结论,在 Spring Boot 应用开发过程中,我们经常会使用@RequestBody注解来接收 HTTP 请求体中的数据,并将其绑定到对应的 Java 对象上,这极大地提高了开发效率。这次遇到了Content type 'application/json;charset=UTF-8' not supported这样的错误,也是出于此处。在实际排查中发现,因实体类中 set 方法重载引发的序列化异常,是导致该错误的常见隐藏原因。排除了网上100%的答案,我自己详细记录一下我的解决方案。
一、@RequestBody:从入门到“入坑”
1.1 @RequestBody是啥?
在SpringBoot的Web开发中,@RequestBody是个超级好用的注解。它主要用于处理HTTP请求的Body内容,通常是JSON或XML格式的数据。简单来说,客户端发来一坨JSON,服务端用@RequestBody把这坨JSON自动反序列化为Java对象,省去了我们手动解析的麻烦。
1.2 基本用法
假设你有个接口需要接收一个用户对象,JSON长这样:
{
"name": "张三",
"email": "zhangsan@example.com"
}
服务端代码可能是这样的:
@RestController
public class UserController {
@PostMapping("/user")
public String createUser(@RequestBody User user) {
return "收到用户: " + user.getName();
}
}
public class User {
private String name;
private String email;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
SpringBoot会利用Jackson(默认的JSON序列化/反序列化库)把JSON转成User对象,调用对应的setName和setEmail方法填充字段。简单、优雅、完美!
1.3 作用
- 自动反序列化:将请求体的JSON/XML映射到Java对象。
- 简化开发:无需手动解析请求体,代码更简洁。
- 灵活性:支持复杂的嵌套对象、集合等。
听起来是不是很美好?但别高兴太早,接下来咱们聊聊为啥会翻车。
二、异常的“罪魁祸首”:Content type 'application/json' not supported
2.1 异常场景
某天,你兴高采烈地写了个接口,信心满满地用Postman发送请求,结果日志里蹦出这么一行:
org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'application/json;charset=UTF-8' not supported
你一脸懵:JSON格式明明是对的,@RequestBody也老老实实加了,咋就“不支持”呢?这错误表面上说“Content-Type不被支持”,但实际上问题往往出在Spring对请求体的处理过程中。
2.2 异常原因
这个异常通常发生在Spring尝试将请求体的JSON反序列化为Java对象时,遇到了无法解析的情况。常见原因包括:
- Content-Type设置错误:客户端发送的请求头
Content-Type不是application/json,或者编码有问题。 - Jackson配置问题:Spring默认用Jackson处理JSON,但如果Jackson的依赖缺失或配置错误,就会报错。
- 对象结构问题:Java对象的设计导致Jackson无法正确映射JSON数据,比如字段名不匹配、缺少setter方法,或者对象中包含复杂的类型(如类作为参数的重载方法)。
- 编码问题:请求体的字符编码(如UTF-8)与服务端期望不一致。
今天咱们重点聊第三点:对象结构问题,尤其是“类作为参数的重载函数”导致的翻车现场,这个问题是突然报出来的,其他几点可能性微乎其微。
三、代码演示:翻车现场重现
来,咱们用代码复现一下这个坑。假设我们有个稍微复杂的User类,里面嵌套了一个Address类:
@RestController
public class UserController {
@PostMapping("/user")
public String createUser(@RequestBody User user) {
return "收到用户: " + user.getName();
}
}
@Data
public class User {
private String username;
private String age;
private String email;
public void setEmail(A email) {
this.email = email.toString() + "A";
}
public void setEmail(B email) {
this.email = email.toString() + "B" +b;
}
}
@Data
public class A {
private String name;
}
@Data
public class B {
private String name;
}
客户端发送的JSON如下:
{
"username": "John Doe",
"age": 30,
"email": {
"name": "123223"
}
}
你可能期待Spring调用某个setEmail方法,把"zhangsan@example.com"设置到email字段。结果却报了Content type 'application/json;charset=UTF-8' not supported!为啥?因为User类中的setEmail方法有两个重载版本,一个接受A类型,一个接受B类型,而JSON里的email字段是字符串,Jackson不知道该调用哪个方法!
四、核心原因:重载方法的“锅”
4.1 为啥会翻车?
Jackson在反序列化JSON到Java对象时,依赖JavaBean规范,主要通过setter方法设置字段值。当它看到email字段时,会寻找setEmail方法。如果setEmail有多个重载版本(比如一个接受A,一个接受B),Jackson会懵圈:我到底该调用哪个?
具体来说:
-
JSON中的email字段是字符串或对象。
-
Jackson希望找到一个setEmail方法,能接受匹配的类型或可转换的类型。
-
由于setEmail方法被重载(A和B),Jackson无法准确决定调用哪个,索性就报错:Content type not supported。
4.2 源码探秘
咱们来瞅瞅Jackson的源码,搞清楚它为啥这么“挑剔”。Jackson的核心反序列化逻辑在com.fasterxml.jackson.databind.deser.BeanDeserializer类中。以下是简化的过程:
- 解析JSON:Jackson用
ObjectMapper解析JSON,生成一个JsonNode树。 - 查找setter:通过
BeanDescription获取目标类的所有setter方法(基于JavaBean规范)。 - 匹配类型:对于每个JSON字段,Jackson会根据字段名查找对应的
setXxx方法,并检查方法的参数类型是否与JSON值匹配。 - 问题来了:如果
setXxx方法有多个重载版本,Jackson的BasicBeanDescription会尝试通过AnnotatedMethod查找最匹配的setter。但如果参数类型歧义(如String和Address),Jackson会抛出异常。
关键代码(BeanDeserializer的部分逻辑,简化版):
public void deserialize(JsonParser p, DeserializationContext ctxt, Object bean) {
// 遍历JSON字段
String propName = p.getCurrentName();
// 查找setter
SettableBeanProperty prop = _beanProperties.find(propName);
if (prop != null) {
// 调用setter
prop.deserializeAndSet(p, ctxt, bean);
} else {
// 找不到合适的setter,抛异常
throw new InvalidFormatException("No suitable setter found for property: " + propName);
}
}
当setAddress有多个重载方法时,_beanProperties.find(propName)无法确定哪个setter是正确的,最终导致HttpMediaTypeNotSupportedException。
五、注意事项:setEmail的“幽灵”调用
还有两个容易忽略的坑,堪称Jackson反序列化的“幽灵”:
5.1 setEmail的“幽灵”调用
即使JSON里没有email字段,Jackson仍然可能调用setEmail方法!比如,JSON是:
{
"username": "张三",
"age": "30"
}
Jackson在反序列化时,可能会尝试调用setEmail(null),但由于setEmail只接受A或B类型,调用setEmail(null)会导致类型不匹配,进一步加剧异常。即便你加了个setEmail(String)方法,Jackson仍会因为重载的setEmail(A)和setEmail(B)而不知所措。
5.2 setAddress的“幽灵”调用
更阴险的坑来了:如果User类里压根没有address字段,但定义了setAddress方法,Jackson依然会在JSON包含address字段时调用它!来看个例子:
@Data
public class User {
private String username;
private String age;
private String email;
public void setEmail(A email) {
this.email = email.toString() + "A";
}
public void setEmail(B email) {
this.email = email.toString() + "B";
}
public void setAddress(String address) {
// 假设我们想手动处理address
this.email = address; // 故意写入email字段
}
}
客户端发送的JSON如下:
{
"username": "张三",
"age": "30",
"address": "北京市朝阳区"
}
尽管User类没有address字段,Jackson看到JSON里有address,就会调用setAddress(String)方法!这会导致:
email字段被意外设置为"北京市朝阳区",完全违背预期。- 如果
setAddress方法逻辑复杂(比如抛出异常或调用其他服务),可能引发更大的问题。
为啥会这样? Jackson在反序列化时,会遍历JSON的所有字段,并尝试匹配目标类的setter方法(基于setXxx命名约定)。即使User类没有address字段,只要有setAddress方法,Jackson就会“热情”地调用它。这种行为完全符合JavaBean规范,但对开发者来说是个隐形炸弹!
5.3 应对“幽灵”调用的策略
- 不要定义无关的setter:确保Java类中只有与JSON字段对应的setter方法。如果不需要
address,就别写setAddress。 - 忽略未知字段:在
User类上加@JsonIgnoreProperties(ignoreUnknown = true),让Jackson忽略JSON中多余的字段:@Data @JsonIgnoreProperties(ignoreUnknown = true) public class User { private String username; private String age; private String email; } - 空值校验:在setter方法中做好空值或类型校验,避免意外逻辑。
六、解决办法:避坑指南
-
避免setter方法重载:尽量不要为同一字段写多个
setXxx方法。如果需要支持多种类型,用@JsonSetter注解指定:@JsonSetter("email") public void setEmail(String email) { this.email = email; } -
明确Content-Type:确保客户端请求头明确指定
Content-Type: application/json。 -
检查Jackson依赖:确保
spring-boot-starter-web包含了Jackson依赖:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> -
空值与未知字段处理:在setter方法中做好空值校验,使用
@JsonIgnoreProperties忽略未知字段。 -
调试利器:启用Spring的调试日志,查看详细的异常堆栈:
logging.level.org.springframework.web=DEBUG -
专事专干: 我就是图省事直接用数据对象去接收前端传来的值,应该新建一个对象专门干这个事才对。
七、总结:从坑里爬出来
@RequestBody是个好帮手,但用不好也会让你“满头包”。Content type 'application/json;charset=UTF-8' not supported异常的根源,往往是JSON到Java对象的映射出了问题,尤其是重载setter方法这种“隐形杀手”。通过理解Jackson的工作原理,避免重载方法、做好空值处理、检查请求头,就能轻松避坑,这次纯粹是对我偷懒的惩罚,我错了,下次还敢😁,不然写文章没有素材啊。