乐趣区

关于jackson:7-Jackson用树模型处理JSON是必备技能不信你看

每棵大树,都曾只是一粒种子。本文已被 https://www.yourbatman.cn 收录,外面一并有 Spring 技术栈、MyBatis、JVM、中间件等小而美的 专栏 供以收费学习。关注公众号【BAT 的乌托邦】一一击破,深刻把握,回绝浅尝辄止。

✍前言

你好,我是 YourBatman。

上篇文章 体验了一把 ObjectMapper 在 数据绑定 方面的利用,用起来还是蛮不便的有木有,为啥不少人说它难用呢,着实费解。我群里问了问,次要起因是它不是静态方法调用,并且办法名获得不那么见名之意 ……

尽管 ObjectMapper 在数据绑定上既能够解决简略类型(如 Integer、List、Map 等),也能解决齐全类型(如 POJO),看似无所不能。然而,若有如下场景它仍旧 不太好实现

  1. 硕大的 JSON 串中我只想要 某一个(某几个)属性的值而已
  2. 长期应用,我并不想创立一个 POJO 与之对应,只想间接应用 即可(类型转换什么的我本人来就好)
  3. 数据结构高度 动态化

为了解决这些问题,Jackson 提供了弱小的 树模型 API 供以应用,这也就是本文的次要的内容。

小贴士:树模型尽管是 jackson-core 模块里定义的,然而是由 jackson-databind 高级模块提供的实现

版本约定

  • Jackson 版本:2.11.0
  • Spring Framework 版本:5.2.6.RELEASE
  • Spring Boot 版本:2.3.0.RELEASE

✍注释

树模型可能比数据绑定 更不便,更灵便 。特地是在构造高度 动静 或者不能很好地映射到 Java 类的状况下,它就显得更有价值了。

树模型

树模型是 JSON 数据内存树的示意模式,这是最灵便的办法,它就相似于 XML 的 DOM 解析器。Jackson 提供了树模型 API 来 生成和解析 JSON 串,次要用到如下三个外围类:

  • JsonNodeFactory:顾名思义,用来结构各种 JsonNode 节点的工厂。例如对象节点 ObjectNode、数组节点 ArrayNode 等等
  • JsonNode:示意 json 节点。能够往里面塞值,从而最终结构出一颗 json 树
  • ObjectMapper:实现 JsonNode 和 JSON 字符串的互转

这里有个萌新的概念:JsonNode。它贯通于整个树模型中,所以有必要先来意识它。

JsonNode

JSON 节点,可类比 XML 的 DOM 树节点构造来辅助了解。JsonNode 是所有 JSON 节点的基类,它是一个抽象类,它有一个较大的特点:绝大多数的 get 办法均放在了此抽象类里(即便它没有实现),目标是:在不进行类型强制转换的状况下遍历构造 。然而,大多数的 批改办法 都必须通过特定的子类类型去调用,这其实是正当的。因为在构建 / 批改某个 Node 节点时,类型类型信息个别是明确的,而在读取 Node 节点时大多数时候并不 太关怀节点类型。

多个 JsonNode 节点形成 Jackson 实现的 JSON 树模型的根底,它是流式 API 中 com.fasterxml.jackson.core.TreeNode 接口的实现,同时它还实现了 Iterable 迭代器接口。

public abstract class JsonNode extends JsonSerializable.Base 
    implements TreeNode, Iterable<JsonNode> {...}

JsonNode 的继承图谱如下(局部):

高深莫测了吧,基本上每个数据类型都会有一个 JsonNode 的实现类型对应。譬如数组节点 ArrayNode、数字节点NumericNode 等等。

个别状况下,咱们并不需要通过 new 关键字去构建一个 JsonNode 实例,而是借助 JsonNodeFactory 工厂来做。

JsonNodeFactory

构建 JsonNode 工厂类。话不多说,用几个例子跑一跑。

值类型节点(ValueNode)

此类节点均为 ValueNode 的子类,特点是:一个节点示意一个值。

@Test
public void test1() {
    JsonNodeFactory factory = JsonNodeFactory.instance;

    System.out.println("------ValueNode 值节点示例 ------");
    // 数字节点
    JsonNode node = factory.numberNode(1);
    System.out.println(node.isNumber() + ":" + node.intValue());

    // null 节点
    node = factory.nullNode();
    System.out.println(node.isNull() + ":" + node.asText());

    // missing 节点
    node = factory.missingNode();
    System.out.println(node.isMissingNode() + "_" + node.asText());

    // POJONode 节点
    node = factory.pojoNode(new Person("YourBatman", 18));
    System.out.println(node.isPojo() + ":" + node.asText());

    System.out.println("---" + node.isValueNode() + "---");
}

运行程序,输入:

------ValueNode 值节点示例 ------
true:1
true:null
true_
true:Person(name=YourBatman, age=18)
---true---

容器类型节点(ContainerNode)

此类节点均为 ContainerNode 的子类,特点是:本节点代表一个容器,外面能够装任何其它节点。

Java 中容器有两种:Map 和 Collection。对应的 Jackson 也提供了两种容器节点用于表述此类数据结构:

  • ObjectNode:类比 Map,采纳 K - V 构造存储。比方一个 JSON 构造,根节点 就是一个 ObjectNode
  • ArrayNode:类比 Collection、数组。外面能够搁置任何节点

上面用示例感受一下它们的应用:

@Test
public void test2() {
    JsonNodeFactory factory = JsonNodeFactory.instance;

    System.out.println("------ 构建一个 JSON 构造数据 ------");
    ObjectNode rootNode = factory.objectNode();

    // 增加一般值节点
    rootNode.put("zhName", "A 哥"); // 成果齐全同:rootNode.set("zhName", factory.textNode("A 哥"))
    rootNode.put("enName", "YourBatman");
    rootNode.put("age", 18);

    // 增加数组容器节点
    ArrayNode arrayNode = factory.arrayNode();
    arrayNode.add("java")
            .add("javascript")
            .add("python");
    rootNode.set("languages", arrayNode);

    // 增加对象节点
    ObjectNode dogNode = factory.objectNode();
    dogNode.put("name", "大黄")
            .put("age", 3);
    rootNode.set("dog", dogNode);

    System.out.println(rootNode);
    System.out.println(rootNode.get("dog").get("name"));
}

运行程序,输入:

------ 构建一个 JSON 构造数据 ------
{"zhName":"A 哥","enName":"YourBatman","age":18,"languages":["java","javascript","python"],"dog":{"name":"大黄","age":3}}
"大黄"

ObjectMapper 中的树模型

树模型其实是底层 流式 API所提出和反对的,典型 API 便是com.fasterxml.jackson.core.TreeNode。但通过后面文章的示例解说能够晓得:底层流式 API 仅定义了接口而并未提供任何实现,甚至半成品都算不上。所以说要应用 Jackson 的树模型还得看 ObjectMapper,它提供了 TreeNode 等 API 的残缺实现。

不乏很多小伙伴对 ObjectMapper 的树模型是只知其一; 不知其二的,甚至素来都没有用过,其实它是 非常灵活 和弱小的。有了下面的根底示例做撑持,再来理解它的实现就得心应手多了。

ObjectMapper 中提供了树模型 (tree model) API 来生成和解析 json 字符串。如果你不想为你的 json 构造独自建类与之对应的话,则能够抉择该 API,如下图所示:

ObjectMapper 在读取 JSON 后提供指向树的根节点的指针,根节点可用于 遍历 残缺的树。同样的,咱们可从读(反序列化)、写(序列化)两个方面来开展。

写(序列化)

将 Object 写为 JsonNode,ObjectMapper 给咱们提供了三个实用 API 俩操作它:

1、valueToTree(Object)

该办法属绝对较为罕用:将任意对象(包含 null)写为一个 JsonNode 树模型。性能上相似于先将 Object 序列化为 JSON 串,再读为 JsonNode,但很显著这样一步到位更加高效。

小贴士:高效不代表性能高,因为其外部实现好还是调用了 readTree() 办法的

@Test
public void test1() {ObjectMapper mapper = new ObjectMapper();

    Person person = new Person();
    person.setName("YourBatman");
    person.setAge(18);

    person.setDog(new Person.Dog("旺财", 3));

    JsonNode node = mapper.valueToTree(person);

    System.out.println(person);
    // 遍历打印所有属性
    Iterator<JsonNode> it = node.iterator();
    while (it.hasNext()) {JsonNode nextNode = it.next();
        if (nextNode.isContainerNode()) {if (nextNode.isObject()) {System.out.println("狗的属性:::");

                System.out.println(nextNode.get("name"));
                System.out.println(nextNode.get("age"));
            }
        } else {System.out.println(nextNode.asText());
        }
    }

    // 间接获取
    System.out.println("---------------------------------------");
    System.out.println(node.get("dog").get("name"));
    System.out.println(node.get("dog").get("age"));
}

运行程序,控制台输入:

Person(name=YourBatman, age=18, dog=Person.Dog(name= 旺财, age=3))
YourBatman
18
狗的属性:::"旺财"
3
---------------------------------------
"旺财"
3

对于 JsonNode 在这里补充一个要点:读取其属性,你既能够用迭代器遍历,也能够依据 key(属性)间接获取,是不是和 Map 的应用简直一毛一样?

2、writeTree(JsonGenerator, JsonNode)

顾名思义:将一个 JsonNode 应用 JsonGenerator 写到输入流里,此办法间接应用到了 JsonGenerator 这个 API,灵便度杠杠的,但绝对偏底层,本处仍旧给个示例玩玩吧(底层 API 更多详解,请参见本系列后面几篇文章):

@Test
public void test2() throws IOException {ObjectMapper mapper = new ObjectMapper();

    JsonFactory factory = new JsonFactory();
    try (JsonGenerator jsonGenerator = factory.createGenerator(System.err, JsonEncoding.UTF8)) {

        // 1、失去一个 jsonNode(为了不便我间接用下面 API 生成了哈)Person person = new Person();
        person.setName("YourBatman");
        person.setAge(18);
        JsonNode jsonNode = mapper.valueToTree(person);

        // 应用 JsonGenerator 写到输入流
        mapper.writeTree(jsonGenerator, jsonNode);
    }
}

运行程序,控制台输入:

{"name":"YourBatman","age":18,"dog":null}

3、writeTree(JsonGenerator,TreeNode)

JsonNode 是 TreeNode 的实现类,下面办法曾经给出了应用示例,所以本办法不在赘述你应该不会有意见了吧。

读(反序列化)

将一个资源(如字符串)读取为一个 JsonNode 树模型。

这是典型的办法重载设计,API 更加敌对,所有办法底层均为 _readTreeAndClose() 这个 protected 办法,堪称“万剑归宗”。

上面以最为常见的:读取 JSON 字符串为例,其它的触类旁通即可。

@Test
public void test3() throws IOException {ObjectMapper mapper = new ObjectMapper();

    String jsonStr = "{\"name\":\"YourBatman\",\"age\":18,\"dog\":null}";
    // 间接映射为一个实体对象
    // mapper.readValue(jsonStr, Person.class);
    // 读取为一个树模型
    JsonNode node = mapper.readTree(jsonStr);
    
    // ... 略
}

至于底层 _readTreeAndClose(JsonParser) 办法的具体实现,就有得捞了。不过鉴于它过于干燥和稍有些烧脑,前面撰有专文详解,有趣味可继续关注。

场景演练

实践和示例讲完了,光说不练假把式,上面 A 哥依据教训,举两个树模型的理论应用示例供你参考。

1、偌大 JSON 串中仅需 1 个值

这种场景其实还蛮常见的,比方有个很经典的场景便是在 MQ 生产中:生产者个别会巴不得把它能吐出来的属性尽可能都扔出来,但对于不同的消费者而言它们的所需往往是不一样的:

  • 须要较多的属性值,这时候用 齐全数据绑定 转换成 POJO 来操作更为不便和正当
  • 须要 1 个 (较少) 的属性值,这时候“杀鸡岂能用牛刀”呢,这种 case 应用树模型来做就显得更为优雅和高效了

譬如,生产者生产的音讯 JSON 串如下(模仿数据,总之你就当做它属性很多、嵌套很深就对了):

{"name":"YourBatman","age":18,"dog":{"name":"旺财","color":"WHITE"},"hobbies":["篮球","football"]}

这时候,我仅关怀狗的色彩,肿么办呢?置信你曾经想到了:树模型

@Test
public void test4() throws IOException {ObjectMapper mapper = new ObjectMapper();

    String jsonStr = "{\"name\":\"YourBatman\",\"age\":18,\"dog\":{\"name\":\" 旺财 \",\"color\":\"WHITE\"},\"hobbies\":[\" 篮球 \",\"football\"]}";
    JsonNode node = mapper.readTree(jsonStr);

    System.out.println(node.get("dog").get("color").asText());
}

运行程序,控制台输入:WHITE,指标达成。值得注意的是:如果 node.get("dog") 没有这个节点 (或者值为 null),是会抛出NPE 异样的,因而请你本人保障代码的健壮性。

当你不想创立一个 Java Bean 与 JSON 属性绝对应时,树模型的 所见即所得 个性就很好解决了这个问题。

2、数据结构高度动态化

当数据结构高度动态化(随时可能新增、删除节点)时,应用树模型去解决是一个较好的计划(稳固之后再转为 Java Bean 即可)。这次要是利用了树模型它具备动静可扩大的个性,满足咱们日益变动的构造:

@Test
public void test5() throws JsonProcessingException {String jsonStr = "{\"name\":\"YourBatman\",\"age\":18}";

    JsonNode node = new ObjectMapper().readTree(jsonStr);

    System.out.println("------------- 向构造里动静增加节点 ------------");
    // 动静增加一个 myDiy 节点,并且该节点还是 ObjectNode 节点
    ((ObjectNode) node).with("myDiy").put("contry", "China");

    System.out.println(node);
}

运行程序,控制台输入:

------------- 向构造里动静增加节点 ------------
{"name":"YourBatman","age":18,"myDiy":{"contry":"China"}}

说白了,也没啥非凡的。拿到一个 JsonNode 后你能够任意的造它,就像 Map<Object,Object> 一样~

✍总结

树模型 (tree model) API 比 Jackson 流式(Streaming) API 简略了很多,不论是生成 json 字符串还是解析 json 字符串。然而绝对于 自动化 的数据绑定而言还是比较复杂的。

树模型 (tree model) API 在只须要取出一个大 json 串中的几个值时比拟不便。如果 json 中每个(大部分)值都须要取得,那么这种形式便显得比拟繁琐了。因而在理论利用中具体问题具体分析, 然而,Jackson 的树模型你必须得把握

✔举荐浏览:
  • Fastjson 到了说再见的时候了
  • 1. 初识 Jackson — 世界上最好的 JSON 库
  • 2. 妈呀,Jackson 原来是这样写 JSON 的
  • 3. 懂了这些,方敢在简历上说会用 Jackson 写 JSON
  • 4. JSON 字符串是如何被解析的?JsonParser 理解一下
  • 5. JsonFactory 工厂而已,还蛮有料,这是我没想到的
  • 6. 二十不惑,ObjectMapper 应用也不再蛊惑

♥关注 A 哥♥

Author A 哥(YourBatman)
集体站点 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
沉闷平台
公众号 BAT 的乌托邦(ID:BAT-utopia)
常识星球 BAT 的乌托邦
每日文章举荐 每日文章举荐

退出移动版