Spring Boot 优雅处理 JSON动态属性

开发 前端
有时我们需要处理具有未知属性的动态 JSON 对象,这些对象的结构可能在运行时有所不同,超出了预定义数据结构的范围。在这种情况下,我们就需要采用一些特殊的技术或策略来灵活地处理这些动态 JSON 对象。

环境:SpringBoot3.4.0

1. 简介

使用 Jackson 处理预定义的 JSON 数据结构非常简单。Spring Boot 默认使用 Jackson 作为其 JSON 处理库,因此,在默认情况下,我们无需进行任何额外的配置即可轻松地在 Spring Boot 应用中序列化和反序列化 JSON 数据。

然而,有时我们需要处理具有未知属性的动态 JSON 对象,这些对象的结构可能在运行时有所不同,超出了预定义数据结构的范围。在这种情况下,我们就需要采用一些特殊的技术或策略来灵活地处理这些动态 JSON 对象。

1.1 什么是动态属性?

首先,我们有如下的实体类:

public class Product {
  private String name;
  private String category;
  // getters, setters
}

对应的JSON字符串如下:

{
  "name": "SpringBoot实战案例100例",
  "category": "book",
  "details": {
    "price": "70",
    "author": "XGPack"
  }
}

这里的 "details" 代表的是动态属性,它在 Product 实体对象中并没有预先定义的对应属性。

接下来,本篇文章将介绍4种方式处理这种JSON对象中包含动态属性的情况。

2. 实战案例

2.1 使用JsonNode属性

我们可以在 Product 类中添加一个类型为 com.fasterxml.jackson.databind.JsonNode 的属性,用来接收和处理上述的 details 动态属性,如下示例:

public class Product {


  // other properties
  private JsonNode details;
  // getters, setters
}

测试代码

ObjectMapper objectMapper = new ObjectMapper() ;
String json = """
    {
      "name": "SpringBoot实战案例100例",
      "category": "book",
      "details": {
        "price": "70",
        "author": "XGPack"
      }
    }
  """ ;
Product product = objectMapper.readValue(json, Product.class) ;
System.err.printf("name: %s, category: %s%n", product.getName(), product.getCategory()) ;
System.out.println("--------------------------------") ;
System.err.printf("price: %s, auther: %s%n", 
  product.getDetails().get("price").asText(),
  product.getDetails().get("author").asText()) ;

输出结果

图片图片

问题得到了解决,但这个解决方案存在一个问题;由于我们有一个 JsonNode 字段,我们的类依赖于 Jackson 库。

2.2 使用Map集合

我们还可以使用Map集合来接收这些动态属性,如下示例:

public class Product_Map {
  // other properties
  private Map<String, Object> details;
  // getters, setters
}

测试代码

ObjectMapper objectMapper = ... ;
String json = ... ;
Product product = objectMapper.readValue(json, Product.class) ;
System.err.printf("name: %s, category: %s%n", product.getName(), product.getCategory()) ;
System.out.println("--------------------------------") ;
System.err.printf("price: %s, author: %s%n", 
    product.getDetails().get("price"),
    product.getDetails().get("author")) ;

此种方式有通过JsonNode差不多,只是这种方式不依赖于jackson包。

2.3 使用@JsonAnySetter注解

当对象只包含动态属性时(details),上面的2个解决方案是很好的选择。然而,有时我们在一个 JSON 对象中混合了固定属性和动态属性。也就是如下数据格式时:

{
  "name": "SpringBoot实战案例100例",
  "category": "book",
  "price": "70",
  "author": "XGPack"
}

动态属性与固定的属性是平级混合在一起,这种情况我们可以使用 @JsonAnySetter 注解来标记一个方法,以处理额外的、未知的属性。这样的方法应该接受两个参数:属性的key和value。

public class Product {


  // other properties
  private Map<String, Object> details = new LinkedHashMap<>() ;
  @JsonAnySetter
  public void setDetail(String key, Object value) {
      details.put(key, value) ;
  }
  // getters, setters
}

这里,我们在 setter 方法上使用 @JsonAnySetter 注解,以便该方法能够处理那些动态的属性。

测试代码

ObjectMapper objectMapper = ... ;
String json = ... ;
Product product = objectMapper.readValue(json, Product.class) ;
System.err.printf("name: %s, category: %s%n", product.getName(), product.getCategory()) ;
System.out.println("--------------------------------") ;
System.err.printf("price: %s, author: %s%n", 
    product.getDetails().get("price"),
    product.getDetails().get("author")) ;

输出结果

图片图片

2.4 自定义反序列化器

在大多数情况下,这些解决方案都能很好地工作;然而,当我们需要更多的控制时,我们可以使用自定义反序列化器来处理。

public class ProductDeserializer extends StdDeserializer<Product> {
  public ProductDeserializer() {
    this(null);
  }
  public ProductDeserializer(Class<?> vc) {
    super(vc);
  }
  @Override
  public Product_CustomDeserializer deserialize(JsonParser jp, DeserializationContext ctxt)
      throws IOException, JsonProcessingException {
    JsonNode node = jp.getCodec().readTree(jp) ;
    // 获取通用字段
    String name = node.get("name").asText() ;
    String category = node.get("category").asText() ;
    // 获取动态属性
    JsonNode detailsNode = node.get("details");
    String price = detailsNode.get("price").asText() ;
    String author = detailsNode.get("author").asText() ;
    Map<String, Object> details = new HashMap<>() ;
    details.put("price", displayAspectRatio) ;
    details.put("author", audioConnector) ;
    return new Product(name, category, details) ;
  }
}

测试代码:

ObjectMapper objectMapper = new ObjectMapper() ;
// 注册反序列获器
SimpleModule module = new SimpleModule();
module.addDeserializer(Product.class, new ProductDeserializer());
objectMapper.registerModule(module) ;
String json = ... ;
// ...

这里通过编程的方式注册自定义反序列化器,其它代码都是一样的。

我们还可以通过更简单的方式进行处理,直接通过注解的方式注册自定义的反序列化器:

@JsonDeserialize(using = ProductDeserializer.class)
public class Product {
  // ...
}

在Spring Boot环境下,那么还可以通过如下方式定义和注册反序列化器。

@JsonComponent
public class PackJsonComponent {


  // 自定义反序列化
  public static class Deserializer extends JsonDeserializer<Product> {
    @Override
    public Product deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
      JsonNode node = jp.getCodec().readTree(jp) ;
      String name = node.get("name").asText() ;
      String category = node.get("category").asText() ;
      
      // ...
      return new Product(name, category, details) ;
    }
  }
}

使用@JsonComponent注解即可。

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2021-04-20 10:50:38

Spring Boot代码Java

2022-10-26 07:14:25

Spring 6Spring业务

2024-09-27 12:27:31

2014-07-22 09:01:53

SwiftJSON

2024-08-02 09:15:22

Spring捕捉格式

2020-03-16 17:20:02

异常处理Spring Boot

2025-01-13 12:46:31

SpringBootJacksonJSON

2024-12-06 09:27:28

2023-04-17 23:49:09

开发代码Java

2024-10-16 12:23:55

技巧Spring验证

2023-09-13 08:56:51

2022-04-08 16:27:48

SpringBoot异常处理

2021-03-09 13:18:53

加密解密参数

2021-01-07 14:06:30

Spring BootJUnit5Java

2023-11-01 08:58:10

2024-10-11 11:46:40

2022-06-04 12:25:10

解密加密过滤器

2022-05-18 12:04:19

Mybatis数据源Spring

2024-08-06 11:17:58

SpringJSON数据

2021-10-22 14:50:23

Spring BootJava
点赞
收藏

51CTO技术栈公众号