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

每棵大樹,都曾只是一粒種子。本文已被 https://www.yourbatman.cn 收錄,裏面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的 專欄供以避免費學習。關注公衆號【 BAT的烏托邦】逐個擊破,深刻掌握,拒絕淺嘗輒止。

✍前言

你好,我是YourBatman。javascript

上篇文章 體驗了一把ObjectMapper在數據綁定方面的應用,用起來仍是蠻方便的有木有,爲啥很多人說它難用呢,着實費解。我羣裏問了問,主要緣由是它不是靜態方法調用,而且方法名取得不那麼見名之意......java

雖然ObjectMapper在數據綁定上既能夠處理簡單類型(如Integer、List、Map等),也能處理徹底類型(如POJO),看似無所不能。可是,如有以下場景它依舊不太好實現node

  1. 碩大的JSON串中我只想要某一個(某幾個)屬性的值而已
  2. 臨時使用,我並不想建立一個POJO與之對應,只想直接使用便可(類型轉換什麼的我本身來就好)
  3. 數據結構高度動態化

爲了解決這些問題,Jackson提供了強大的樹模型 API供以使用,這也就是本文的主要的內容。python

小貼士:樹模型雖然是jackson-core模塊裏定義的,可是是由jackson-databind高級模塊提供的實現

版本約定

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

✍正文

樹模型可能比數據綁定更方便,更靈活。特別是在結構高度動態或者不能很好地映射到Java類的狀況下,它就顯得更有價值了。git

樹模型

樹模型是JSON數據內存樹的表示形式,這是最靈活的方法,它就相似於XML的DOM解析器。Jackson提供了樹模型API來生成和解析 JSON串,主要用到以下三個核心類:github

  • JsonNodeFactory:顧名思義,用來構造各類JsonNode節點的工廠。例如對象節點ObjectNode、數組節點ArrayNode等等
  • JsonNode:表示json節點。能夠往裏面塞值,從而最終構造出一顆json樹
  • ObjectMapper:實現JsonNode和JSON字符串的互轉

這裏有個萌新的概念:JsonNode。它貫穿於整個樹模型中,因此有必要先來認識它。json

JsonNode

JSON節點,可類比XML的DOM樹節點結構來輔助理解。JsonNode是全部JSON節點的基類,它是一個抽象類,它有一個較大的特色:絕大多數的get方法均放在了此抽象類裏(即便它沒有實現),目的是:在不進行類型強制轉換的狀況下遍歷結構。可是,大多數的修改方法都必須經過特定的子類類型去調用,這實際上是合理的。由於在構建/修改某個Node節點時,類型類型信息通常是明確的,而在讀取Node節點時大多數時候並不 太關心節點類型。segmentfault

多個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倆操做它:

一、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的使用幾乎一毛同樣?

二、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)) {

        // 一、獲得一個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}
三、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哥根據經驗,舉兩個樹模型的實際使用示例供你參考。

一、偌大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屬性相對應時,樹模型的所見即所得特性就很好解決了這個問題。

二、數據結構高度動態化

當數據結構高度動態化(隨時可能新增、刪除節點)時,使用樹模型去處理是一個較好的方案(穩定以後再轉爲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的樹模型你必須得掌握

✔推薦閱讀:

♥關注A哥♥

Author A哥(YourBatman)
我的站點 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
活躍平臺
公衆號 BAT的烏托邦(ID:BAT-utopia)
知識星球 BAT的烏托邦
每日文章推薦 每日文章推薦

BAT的烏托邦

相關文章
相關標籤/搜索