踩坑记:Content type 'application/json;charset=UTF-8' not supported

1,320 阅读8分钟

踩坑记: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对象,调用对应的setNamesetEmail方法填充字段。简单、优雅、完美!

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对象时,遇到了无法解析的情况。常见原因包括:

  1. Content-Type设置错误:客户端发送的请求头Content-Type不是application/json,或者编码有问题。
  2. Jackson配置问题:Spring默认用Jackson处理JSON,但如果Jackson的依赖缺失或配置错误,就会报错。
  3. 对象结构问题:Java对象的设计导致Jackson无法正确映射JSON数据,比如字段名不匹配、缺少setter方法,或者对象中包含复杂的类型(如类作为参数的重载方法)
  4. 编码问题:请求体的字符编码(如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类中。以下是简化的过程:

  1. 解析JSON:Jackson用ObjectMapper解析JSON,生成一个JsonNode树。
  2. 查找setter:通过BeanDescription获取目标类的所有setter方法(基于JavaBean规范)。
  3. 匹配类型:对于每个JSON字段,Jackson会根据字段名查找对应的setXxx方法,并检查方法的参数类型是否与JSON值匹配。
  4. 问题来了:如果setXxx方法有多个重载版本,Jackson的BasicBeanDescription会尝试通过AnnotatedMethod查找最匹配的setter。但如果参数类型歧义(如StringAddress),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只接受AB类型,调用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方法中做好空值或类型校验,避免意外逻辑。

六、解决办法:避坑指南

  1. 避免setter方法重载:尽量不要为同一字段写多个setXxx方法。如果需要支持多种类型,用@JsonSetter注解指定:

    @JsonSetter("email")
    public void setEmail(String email) {
        this.email = email;
    }
    
  2. 明确Content-Type:确保客户端请求头明确指定Content-Type: application/json

  3. 检查Jackson依赖:确保spring-boot-starter-web包含了Jackson依赖:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
  4. 空值与未知字段处理:在setter方法中做好空值校验,使用@JsonIgnoreProperties忽略未知字段。

  5. 调试利器:启用Spring的调试日志,查看详细的异常堆栈:

    logging.level.org.springframework.web=DEBUG
    
  6. 专事专干: 我就是图省事直接用数据对象去接收前端传来的值,应该新建一个对象专门干这个事才对。

七、总结:从坑里爬出来

@RequestBody是个好帮手,但用不好也会让你“满头包”。Content type 'application/json;charset=UTF-8' not supported异常的根源,往往是JSON到Java对象的映射出了问题,尤其是重载setter方法这种“隐形杀手”。通过理解Jackson的工作原理,避免重载方法、做好空值处理、检查请求头,就能轻松避坑,这次纯粹是对我偷懒的惩罚,我错了,下次还敢😁,不然写文章没有素材啊。