@JsonAnyGetter 动态表格渲染的“神”

一、jsonAnyGetter 是什么?

在 Java 后端开发中,我们常常会使用各种 JSON 处理库来处理 JSON 数据,其中 Jackson 是一款非常流行的 JSON 处理库。@jsonAnyGetter就是 Jackson 库中的一个注解 ,它在 Java 对象与 JSON 数据相互转换的过程中扮演着重要的角色。

简单来说,@jsonAnyGetter注解用于将 Java 对象中的一个Map类型的属性,在序列化为 JSON 时,将Map中的键值对展开为 JSON 对象的顶级属性。这使得我们可以灵活地处理那些在 Java 类中没有预先定义具体属性,但在实际 JSON 数据中可能会动态出现的字段。例如,当我们与第三方接口进行交互时,对方返回的 JSON 数据结构可能不是完全固定的,使用@jsonAnyGetter注解就能方便地应对这种情况。

在我的实践当中,用来返回一些动态字段数据 给前端 渲染一些复杂的动态表格列 非常好用。

二、使用前的准备工作

在使用@jsonAnyGetter之前,首先要确保项目中引入了 Jackson 库的相关依赖。如果你使用的是 Maven 项目管理工具 ,可以在pom.xml文件中添加以下依赖:

<dependency>
    
<groupId>com.fasterxml.jackson.core</groupId>

<artifactId>jackson-databind</artifactId>

<version>2.15.2</version>

</dependency>

上述代码中,<groupId>指定了依赖的组 ID,这里是 Jackson 库的核心组 ID;<artifactId>指定了依赖的工件 ID,jackson-databind是 Jackson 库中用于数据绑定的核心模块;<version>指定了依赖的版本号,这里使用的是 2.15.2 版本,你可以根据实际情况选择合适的版本 。

如果你使用的是 Gradle 构建工具,在build.gradle文件中添加如下依赖:

implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'

添加完依赖后,Maven 或 Gradle 会自动下载并管理相关的库文件。

在 Spring Boot 项目中,如果你引入了spring-boot-starter-web依赖,由于spring-boot-starter-web中已经包含了 Jackson 相关依赖,所以不需要额外添加上述依赖。Spring Boot 会自动配置 Jackson 的相关功能,使得我们可以方便地使用@jsonAnyGetter等注解 。

此外,为了使用@jsonAnyGetter注解,我们还需要在 Java 类中导入相关的包:

import com.fasterxml.jackson.annotation.JsonAnyGetter;

这样,我们就完成了使用@jsonAnyGetter注解的前期准备工作,可以开始在项目中使用它来处理动态 JSON 数据了。

三、jsonAnyGetter 的常见用法

(一)动态属性序列化

在实际开发中,我们经常会遇到需要将对象中的动态属性序列化为 JSON 的情况。例如,在一个电商系统中,商品对象除了有固定的属性如名称、价格、描述等,还可能有一些动态属性,比如不同地区的库存数量、促销活动信息等 。这些动态属性在 Java 类中可能没有预先定义为具体的字段,而是存储在一个Map类型的属性中。使用@jsonAnyGetter注解,就可以轻松地将这些动态属性序列化到 JSON 中 。

下面通过一个示例代码来展示具体的实现方式:

import com.fasterxml.jackson.annotation.JsonAnyGetter;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;

import java.util.Map;

class Product {

    private String name;

    private double price;

    private Map < String, Object > additionalInfo = new HashMap < > ();

    public Product(String name, double price) {

        this.name = name;

        this.price = price;

    }

    public String getName() {

        return name;

    }

    public double getPrice() {

        return price;

    }

    // 向动态属性Map中添加属性

    public void addAdditionalInfo(String key, Object value) {

        additionalInfo.put(key, value);

    }

    @JsonAnyGetter

    public Map < String, Object > getAdditionalInfo() {

        return additionalInfo;

    }

}

public class JsonAnyGetterExample {

    public static void main(String[] args) throws Exception {

        Product product = new Product("手机", 3999.99);

        product.addAdditionalInfo("stock", 100);

        product.addAdditionalInfo("promotion", "满减500");

        ObjectMapper objectMapper = new ObjectMapper();

        String json = objectMapper.writeValueAsString(product);

        System.out.println(json);

    }

}

在上述代码中,Product类包含了name和price两个固定属性,以及一个Map类型的additionalInfo属性用于存储动态属性 。@JsonAnyGetter注解标注在getAdditionalInfo方法上,该方法返回additionalInfo这个Map对象 。在main方法中,我们创建了一个Product对象,并向additionalInfo中添加了两个动态属性:stock和promotion 。然后使用ObjectMapper将Product对象序列化为 JSON 字符串 。运行上述代码,输出的 JSON 字符串如下:

{"name":"手机","price":3999.99,"stock":100,"promotion":"满减500"}

可以看到,additionalInfo中的键值对被展开为 JSON 对象的顶级属性,与name和price属性处于同一层级,实现了动态属性的序列化。

(二)灵活的 JSON 结构构建

在构建复杂或灵活的 JSON 结构时,jsonAnyGetter也能发挥重要作用。比如,我们需要将不同类型的数据整合到一个 JSON 对象中,这些数据可能来自不同的数据源或具有不同的业务含义 。通过@jsonAnyGetter,我们可以将这些数据以键值对的形式添加到一个Map中,然后在序列化时将其转换为符合要求的 JSON 结构 。

假设我们正在开发一个内容管理系统,需要将一篇文章的基本信息(标题、作者、发布时间)和文章的扩展信息(阅读量、点赞数、评论数)整合到一个 JSON 对象中返回给前端 。文章的扩展信息可能会随着用户的操作实时变化,因此适合用动态属性来处理 。示例代码如下:

import com.fasterxml.jackson.annotation.JsonAnyGetter;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.Date;

import java.util.HashMap;

import java.util.Map;

class Article {

    private String title;

    private String author;

    private Date publishTime;

    private Map < String, Object > extendedInfo = new HashMap < > ();

    public Article(String title, String author, Date publishTime) {

        this.title = title;

        this.author = author;

        this.publishTime = publishTime;

    }

    public String getTitle() {

        return title;

    }

    public String getAuthor() {

        return author;

    }

    public Date getPublishTime() {

        return publishTime;

    }

    // 向扩展信息Map中添加属性

    public void addExtendedInfo(String key, Object value) {

        extendedInfo.put(key, value);

    }

    @JsonAnyGetter

    public Map < String, Object > getExtendedInfo() {

        return extendedInfo;

    }

}

public class JsonAnyGetterFlexibleExample {

    public static void main(String[] args) throws Exception {

        Date now = new Date();

        Article article = new Article("Java后端开发技巧", "张三", now);

        article.addExtendedInfo("views", 1000);

        article.addExtendedInfo("likes", 200);

        article.addExtendedInfo("comments", 50);

        ObjectMapper objectMapper = new ObjectMapper();

        String json = objectMapper.writeValueAsString(article);

        System.out.println(json);

    }

}

在上述代码中,Article类包含了文章的基本信息属性,以及一个用于存储扩展信息的Map类型属性extendedInfo 。@JsonAnyGetter注解标注在getExtendedInfo方法上 。在main方法中,我们创建了一个Article对象,并向extendedInfo中添加了阅读量、点赞数和评论数等扩展信息 。最后将Article对象序列化为 JSON 字符串 。输出的 JSON 字符串如下:


{"title":"Java后端开发技巧","author":"张三","publishTime":1699263724597,"views":1000,"likes":200,"comments":50}

通过这种方式,我们成功地将不同类型的数据整合到了一个 JSON 对象中,并且可以灵活地添加或修改动态属性,满足了复杂 JSON 结构构建的需求。

(三)处理未知属性

在反序列化时,jsonAnyGetter通常需要配合@jsonAnySetter使用,来处理 JSON 中那些在 Java 类中没有对应属性的字段。比如,当我们从第三方接口接收 JSON 数据时,对方返回的 JSON 结构可能会包含一些我们事先不知道的字段 。使用@jsonAnySetter注解,可以将这些未知字段存储到一个Map中,而@jsonAnyGetter则用于在序列化时将这个Map中的数据正确地输出 。

以下是一个示例代码:

import com.fasterxml.jackson.annotation.JsonAnyGetter;

import com.fasterxml.jackson.annotation.JsonAnySetter;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;

import java.util.Map;

class DataObject {

    private String knownProperty;

    private Map < String, Object > unknownProperties = new HashMap < > ();

    public String getKnownProperty() {

        return knownProperty;

    }

    public void setKnownProperty(String knownProperty) {

        this.knownProperty = knownProperty;

    }

    @JsonAnySetter

    public void setUnknownProperty(String key, Object value) {

        unknownProperties.put(key, value);

    }

    @JsonAnyGetter

    public Map < String, Object > getUnknownProperties() {

        return unknownProperties;

    }

}

public class JsonAnyGetterSetterExample {

    public static void main(String[] args) throws Exception {

        String json = "{\"knownProperty\":\"value\",\"unknownField1\":\"unknownValue1\",\"unknownField2\":123}";

        ObjectMapper objectMapper = new ObjectMapper();

        DataObject dataObject = objectMapper.readValue(json, DataObject.class);

        System.out.println("Known Property: " + dataObject.getKnownProperty());

        System.out.println("Unknown Properties: " + dataObject.getUnknownProperties());

        // 再次序列化

        String serializedJson = objectMapper.writeValueAsString(dataObject);

        System.out.println("Serialized Json: " + serializedJson);

    }

}

在上述代码中,DataObject类包含一个已知属性knownProperty和一个用于存储未知属性的Map类型属性unknownProperties 。@JsonAnySetter注解标注在setUnknownProperty方法上,用于在反序列化时将 JSON 中的未知字段存储到unknownProperties中 。@JsonAnyGetter注解标注在getUnknownProperties方法上,用于在序列化时将unknownProperties中的数据正确地输出 。在main方法中,我们首先定义了一个包含未知字段的 JSON 字符串,然后使用ObjectMapper将其反序列化为DataObject对象 。接着,我们输出了已知属性和未知属性的值 。最后,我们再次将DataObject对象序列化为 JSON 字符串并输出 。运行上述代码,输出结果如下:

Known Property: value

Unknown Properties: {unknownField1=unknownValue1, unknownField2=123}

Serialized Json: {"knownProperty":"value","unknownField1":"unknownValue1","unknownField2":123}

可以看到,通过@jsonAnySetter和@jsonAnyGetter的配合使用,我们成功地处理了 JSON 中的未知属性,实现了数据的正确反序列化和序列化 。

四、实战案例分析

(一)业务场景引入

在一个电商系统中,商品的属性管理是非常重要的一部分。不同类型的商品可能具有不同的属性,比如电子产品有品牌、型号、内存大小、屏幕尺寸等属性;服装有品牌、尺码、颜色、材质等属性 。如果我们使用传统的方式,为每种商品类型都定义一个固定的 Java 类来存储这些属性,会导致类的数量过多,代码冗余且难以维护 。而且,当商品的属性发生变化时,还需要频繁地修改 Java 类的定义 。为了解决这个问题,我们可以利用@jsonAnyGetter注解,将商品的动态属性存储在一个Map中,这样可以灵活地处理不同商品的各种属性 。

(二)代码实现与解析

下面是使用@jsonAnyGetter解决上述业务问题的完整代码:

import com.fasterxml.jackson.annotation.JsonAnyGetter;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;

import java.util.Map;

class Product {

    private String id;

    private String name;

    private double price;

    private Map < String, Object > attributes = new HashMap < > ();

    public Product(String id, String name, double price) {

        this.id = id;

        this.name = name;

        this.price = price;

    }

    public String getId() {

        return id;

    }

    public String getName() {

        return name;

    }

    public double getPrice() {

        return price;

    }

    // 向属性Map中添加属性

    public void addAttribute(String key, Object value) {

        attributes.put(key, value);

    }

    @JsonAnyGetter

    public Map < String, Object > getAttributes() {

        return attributes;

    }

}

public class JsonAnyGetterProductExample {

    public static void main(String[] args) throws Exception {

        Product phone = new Product("P001", "华为手机", 4999.99);

        phone.addAttribute("brand", "华为");

        phone.addAttribute("model", "Mate 60 Pro");

        phone.addAttribute("memory", "128GB");

        phone.addAttribute("screenSize", "6.82英寸");

        Product shirt = new Product("P002", "纯棉衬衫", 199.0);

        shirt.addAttribute("brand", "海澜之家");

        shirt.addAttribute("size", "L");

        shirt.addAttribute("color", "白色");

        shirt.addAttribute("material", "纯棉");

        ObjectMapper objectMapper = new ObjectMapper();

        String phoneJson = objectMapper.writeValueAsString(phone);

        String shirtJson = objectMapper.writeValueAsString(shirt);

        System.out.println("手机商品JSON: " + phoneJson);

        System.out.println("衬衫商品JSON: " + shirtJson);

    }

}

代码解析如下:

  1. Product类定义
    • id、name和price是所有商品都具有的基本属性 。
    • attributes是一个Map类型的属性,用于存储商品的动态属性 ,键为属性名,值为属性值 。
  1. 构造函数
    • Product(String id, String name, double price)构造函数用于初始化商品的基本属性 。
  1. addAttribute方法
    • 该方法用于向attributes中添加动态属性,接收属性名和属性值作为参数 ,并将其存储到attributes这个Map中 。
  1. getAttributes方法
    • 该方法返回attributes这个Map对象 ,并且使用@JsonAnyGetter注解进行标注 。这告诉 Jackson 在将Product对象序列化为 JSON 时,将attributes中的键值对展开为 JSON 对象的顶级属性 。
  1. main方法
    • 创建了两个Product对象,分别代表手机和衬衫 。
    • 为每个Product对象添加了不同的动态属性 。
    • 使用ObjectMapper将两个Product对象分别序列化为 JSON 字符串 ,并输出到控制台 。

运行上述代码,输出结果如下:


手机商品JSON: {"id":"P001","name":"华为手机","price":4999.99,"brand":"华为","model":"Mate 60 Pro","memory":"128GB","screenSize":"6.82英寸"}

衬衫商品JSON: {"id":"P002","name":"纯棉衬衫","price":199.0,"brand":"海澜之家","size":"L","color":"白色","material":"纯棉"}

可以看到,通过@jsonAnyGetter注解,我们成功地将商品的动态属性灵活地序列化为 JSON,避免了为每种商品类型定义大量固定属性的 Java 类,大大简化了开发过程,提高了代码的可维护性和扩展性 。

五、注意事项与常见问题

(一)性能考虑

在大量数据处理时,jsonAnyGetter可能会带来一定的性能影响。由于@jsonAnyGetter通常是将Map类型的属性展开,当Map中的数据量非常大时,序列化和反序列化的过程会涉及到大量的键值对处理,这可能会导致性能下降 。例如,在一个日志系统中,如果需要处理海量的日志数据,且每条日志都包含大量的动态属性(通过@jsonAnyGetter处理),那么在将日志数据转换为 JSON 格式存储或传输时,可能会消耗较多的 CPU 和内存资源 。

为了应对这种性能问题,可以采取以下策略:

  1. 减少不必要的动态属性:仔细评估业务需求,尽量避免在Map中存储过多不必要的动态属性。只保留真正需要的属性,这样可以减少@jsonAnyGetter处理的数据量 。
  1. 使用更高效的 JSON 处理库:虽然 Jackson 是一款优秀的 JSON 处理库,但在某些特定场景下,其他库可能具有更好的性能表现。例如,Gson 库在一些简单场景下的性能也不错,且具有简洁易用的特点 。可以根据实际业务情况进行测试和选择 。
  1. 优化数据结构:如果可能,将一些固定的动态属性提取出来,定义为 Java 类的常规属性,而不是全部放在Map中通过@jsonAnyGetter处理。这样可以利用 Jackson 对常规属性的优化机制,提高处理效率 。

(二)与其他注解的冲突

当jsonAnyGetter与 Jackson 其他注解(如@JsonProperty等)同时使用时,可能会出现冲突。例如,当一个 Java 类中既有@JsonProperty注解标注的属性,又有通过@jsonAnyGetter处理的Map类型属性时,如果Map中的键与@JsonProperty指定的属性名相同,就会产生冲突 。

假设我们有如下 Java 类:

import com.fasterxml.jackson.annotation.JsonAnyGetter;

import com.fasterxml.jackson.annotation.JsonProperty;

import com.fasterxml.jackson.databind.ObjectMapper;

import java.util.HashMap;

import java.util.Map;

class ConflictExample {

    @JsonProperty("name")

    private String realName;

    private Map < String, Object > additionalInfo = new HashMap < > ();

    public ConflictExample(String realName) {

        this.realName = realName;

    }

    @JsonAnyGetter

    public Map < String, Object > getAdditionalInfo() {

        return additionalInfo;

    }

}

public class AnnotationConflictExample {

    public static void main(String[] args) throws Exception {

        ConflictExample example = new ConflictExample("张三");

        example.getAdditionalInfo().put("name", "李四");

        ObjectMapper objectMapper = new ObjectMapper();

        String json = objectMapper.writeValueAsString(example);

        System.out.println(json);

    }

}

在上述代码中,ConflictExample类既有@JsonProperty("name")标注的realName属性,又在additionalInfo这个Map中添加了键为name的属性 。运行上述代码,会发现输出的 JSON 中name属性的值是Map中对应的值(李四),而不是realName的值(张三) ,这就是因为@jsonAnyGetter和@JsonProperty产生了冲突 。

解决这种冲突的办法是:

  1. 确保属性名唯一:在设计 Java 类和动态属性的键时,要保证@JsonProperty指定的属性名与@jsonAnyGetter处理的Map中的键不重复 。这样可以避免冲突的发生 。
  1. 使用别名:如果确实需要使用相同的逻辑名,可以为其中一个属性使用别名 。例如,可以将@JsonProperty的属性名改为其他值,或者在获取Map中的值时使用不同的键名 。
  1. 自定义序列化和反序列化逻辑:在冲突无法避免的复杂情况下,可以通过自定义序列化器和反序列化器来实现对属性的精确控制 。例如,继承StdSerializer类来实现自定义的序列化逻辑,在其中根据业务需求决定如何处理冲突的属性 。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小矩灵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值