7. Jackson用樹模型處理JSON是必備技能,不信你看

YourBatman發表於2020-08-25

每棵大樹,都曾只是一粒種子。本文已被 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的樹模型你必須得掌握

✔推薦閱讀:

相關文章