想象这样一个场景:你正在构建一个 API 接口,但用户不断向同一个接口端点发送结构不同的 JSON 数据 —— 有时是实体商品订单,有时是数字下载订单,还有时是订阅订单。

这三类数据都属于 “订单” 范畴,但 JSON 请求中的字段却各不相同。在 Spring Boot 中,该如何优雅地处理这种情况?

本文将围绕这一核心问题展开,带你理解动态请求体的处理思路,让你的 Spring Boot 应用能够灵活应对这类场景。

什么是请求体?

当你向 REST API 发送 POST 或 PUT 请求时,通常会在请求体中携带一些数据。

比如新增用户时,你可能会发送这样的 JSON:

{
  "name": "Ujjawal",
  "email": "ujjawal@example.com"
}

在 Spring Boot 中,你可以通过如下方式在控制器中接收这个 JSON:

运行

@PostMapping("/users")
public String createUser(@RequestBody User user) {
  return "用户 " + user.getName() + " 添加成功!";
}

背后的工作原理很简单:

  • @RequestBody注解告诉 Spring:“把 HTTP 请求体中的 JSON 数据转换成 Java 对象”;
  • Spring 会自动使用 Jackson(一款主流的 JSON 解析库)完成这个转换过程。
  • 注意本文全篇使用Jackson.

到这里一切都很顺利,但如果接收到的 JSON 结构并非固定不变呢?

问题所在 —— 当 JSON 结构不可预测

假设你正在构建电商 API,用户可以提交三种不同类型的订单:

1. 实体商品订单(PhysicalProductOrder)

{   
  "orderType": "PHYSICAL",   
  "productName": "无线鼠标",   
  "quantity": 2,
  "shippingAddress": "新德里" 
}

2. 数字商品订单(DigitalProductOrder)

{
  "orderType": "DIGITAL",
  "productName": "电子书:Spring Boot 实战",
  "downloadLink": "https://example.com/ebook.pdf" 
}

3. 订阅订单(SubscriptionOrder)

{  
  "orderType": "SUBSCRIPTION", 
  "serviceName": "MusicStream 高级会员", 
  "durationMonths": 12 
}

每种订单都有一些公共字段(比如productName),但也包含各自独有的字段。你肯定不想为每种订单都单独创建一个接口:

/order/physical
/order/digital
/order/subscription

你更希望有一个简洁统一的接口:

/orders

但如果简单地编写如下代码:

运行

@PostMapping("/orders")
public void createOrder(@RequestBody Order order) { ... }

Spring 并不会自动识别出该把 JSON 转换成哪种Order类型的对象 —— 我们需要主动告诉它该如何判断。

核心概念 —— 多态反序列化

我们需要让 Jackson 具备这样的能力:

当检测到orderType: PHYSICAL时,创建PhysicalProductOrder对象;当检测到orderType: DIGITAL时,创建DigitalProductOrder对象;…… 以此类推。

这就是多态反序列化—— 根据一个 “类型标识” 字段,将 JSON 数据转换成不同的 Java 类实例。

实战实现

在编写代码之前,我们先梳理一下实现思路:

我们需要一个/orders接口来接收三种不同结构的订单 JSON,核心是让后端自动识别订单类型并映射到对应的 Java 类。这看似复杂,实则是继承机制与 Jackson 配置的巧妙结合。

实现思路拆解

  1. 提取所有订单的公共特征比如所有订单都包含productName字段,且都需要processOrder()方法来处理订单。因此我们先定义一个抽象基类Order,用于存放公共字段和方法。

  2. 为每种订单类型扩展专属特征

    • 实体订单:新增quantity(数量)和shippingAddress(收货地址);
    • 数字订单:新增downloadLink(下载链接);
    • 订阅订单:新增durationMonths(订阅时长)和serviceName(服务名称)。这些订单类都作为Order的子类实现。
  3. 为 Jackson 指定类型判断依据这里需要一个鉴别器字段(比如orderType),JSON 请求中携带该字段,Jackson 通过它判断具体的订单类型:

    { 
      "orderType": "PHYSICAL", 
      "productName": "键盘", 
      "quantity": 3 
    }
    
  4. 配置 Jackson 的类型解析规则Jackson 不会自动识别类型,我们需要显式配置:

    • 基类:Order
    • 所有子类:PhysicalProductOrderDigitalProductOrderSubscriptionOrder
    • 类型判断字段:orderType

    这需要用到两个核心注解:

    • @JsonTypeInfo:告诉 Jackson 存在子类,并指定类型信息的获取方式;
    • @JsonSubTypes:列出所有子类,并关联对应的类型标识。
  5. Spring Boot 自动处理转换当请求到达控制器时,Spring 会读取 JSON 中的orderType字段,自动将请求体转换成对应的子类实例,无需手动解析。

代码实现

1. 基类:Order
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;

// 配置多态反序列化规则
@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME,          // 使用名称作为类型标识
  include = JsonTypeInfo.As.PROPERTY, // 类型标识以属性形式存在于JSON中
  property = "orderType"              // 用于判断类型的字段名
)
@JsonSubTypes({
  // 映射类型标识与对应的子类
  @JsonSubTypes.Type(value = PhysicalProductOrder.class, name = "PHYSICAL"),
  @JsonSubTypes.Type(value = DigitalProductOrder.class, name = "DIGITAL"),
  @JsonSubTypes.Type(value = SubscriptionOrder.class, name = "SUBSCRIPTION")
})
public abstract class Order {
  private String productName;

  // Getter & Setter
  public String getProductName() { 
    return productName; 
  }

  public void setProductName(String productName) { 
    this.productName = productName; 
  }

  // 抽象方法,由子类实现具体的订单处理逻辑
  public abstract void processOrder();
}
2. 各订单类型的子类实现

PhysicalProductOrder.java(实体商品订单)

public class PhysicalProductOrder extends Order {
  private int quantity;          // 商品数量
  private String shippingAddress;// 收货地址

  // Getter & Setter
  public int getQuantity() {
    return quantity; 
  }
  
  public void setQuantity(int quantity) { 
    this.quantity = quantity; 
  }
  
  public String getShippingAddress() { 
    return shippingAddress; 
  }
  
  public void setShippingAddress(String shippingAddress) { 
    this.shippingAddress = shippingAddress; 
  }
  
  // 实现订单处理逻辑
  @Override
  public void processOrder() {
    System.out.println("正在向" + shippingAddress + "发货,数量:" + getQuantity() + "件");
  }
}

DigitalProductOrder.java(数字商品订单)

public class DigitalProductOrder extends Order {
    private String downloadLink; // 下载链接

  // Getter & Setter
  public String getDownloadLink() { 
    return downloadLink; 
  }
  
  public void setDownloadLink(String downloadLink) { 
    this.downloadLink = downloadLink; 
  }
  
  // 实现订单处理逻辑
  @Override
  public void processOrder() {
    System.out.println("正在发送下载链接:" + downloadLink);
  }
}

SubscriptionOrder.java(订阅订单)

public class SubscriptionOrder extends Order {
  private String serviceName;    // 服务名称
  private int durationMonths;    // 订阅时长(月)

  // Getter & Setter
  public String getServiceName() { 
    return serviceName; 
  }
  
  public void setServiceName(String serviceName) { 
    this.serviceName = serviceName; 
  }
 
  public int getDurationMonths() { 
    return durationMonths; 
  }
  
  public void setDurationMonths(int durationMonths) { 
    this.durationMonths = durationMonths; 
  }
  
  // 实现订单处理逻辑
  @Override
  public void processOrder() {
    System.out.println("正在激活订阅服务:" + serviceName + ",时长:" + durationMonths + "个月");
  }
}
3. 控制器:统一的订单接口
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/orders")
public class OrderController {
  @PostMapping
  public String createOrder(@RequestBody Order order) {
    // 调用对应子类的processOrder方法
    order.processOrder();
    return "商品「" + order.getProductName() + "」的订单处理成功!";
  }
}

测试验证

现在我们向/orders接口发送不同类型的订单请求,验证效果:

测试实体商品订单

POST /orders
Content-Type: application/json
Body:
{
  "orderType": "PHYSICAL",
  "productName": "键盘",
  "quantity": 3,
  "shippingAddress": "浦那"
}

响应结果

商品「键盘」的订单处理成功!

后端日志

正在向浦那发货,数量:3件

异常处理:未知订单类型

如果用户发送了未定义的订单类型:

{
  "orderType": "UNKNOWN",
  "productName": "神秘礼盒"
}

Jackson 会抛出如下异常:

Could not resolve type id 'UNKNOWN' into a subtype of [simple type, class com.example.Order]

为了提高接口的健壮性,我们可以配置默认类型:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME,
  include = JsonTypeInfo.As.PROPERTY,
  property = "orderType",
  defaultImpl = PhysicalProductOrder.class // 未知类型时默认使用实体订单类
)

这样当检测到未知的orderType时,会自动使用PhysicalProductOrder作为默认类型。

适用场景

推荐使用多态请求体的场景

  1. 单个接口需要接收多个关联的对象类型;
  2. 各对象类型共享部分公共属性和行为;
  3. 可以通过一个字段(如 type、category)明确区分对象类型。

不推荐使用的场景

  1. 各数据结构完全无关;
  2. 使用独立接口更易维护和理解。

掌握这种方式后,你可以编写更简洁、更具适应性的 API,无需因客户端新增字段或数据类型而频繁修改接口结构。


总结

  1. Spring Boot 处理动态请求体的核心是利用 Jackson 的多态反序列化能力,通过@JsonTypeInfo@JsonSubTypes配置类型映射规则;
  2. 实现思路是先定义抽象基类封装公共属性 / 方法,再为不同数据结构实现子类,通过鉴别器字段(如orderType)关联类型与子类;
  3. 该方案适用于 “同一接口接收关联但不同结构数据” 的场景,能显著提升 API 的简洁性和扩展性。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐