1. 程式人生 > >7. Jackson用樹模型處理JSON是必備技能,不信你看

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

> 每棵大樹,都曾只是一粒種子。本文已被 [**https://www.yourbatman.cn**](https://www.yourbatman.cn) 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的**專欄**供以免費學習。關注公眾號【**BAT的烏托邦**】逐個擊破,深入掌握,拒絕淺嘗輒止。 [TOC] ![](https://img-blog.csdnimg.cn/20200821210157583.png#pic_center) # ✍前言 你好,我是YourBatman。 [上篇文章](https://mp.weixin.qq.com/s/VYy1QVeFLRkciymFHueb5w) 體驗了一把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`迭代器介面。 ```java public abstract class JsonNode extends JsonSerializable.Base implements TreeNode, Iterable { ... } ``` JsonNode的繼承圖譜如下(部分): ![](https://img-blog.csdnimg.cn/20200728214449119.png) 一目瞭然了吧,基本上每個資料型別都會有一個JsonNode的實現型別對應。譬如陣列節點`ArrayNode`、數字節點`NumericNode`等等。 一般情況下,我們並不需要通過new關鍵字去構建一個JsonNode例項,而是藉助`JsonNodeFactory`工廠來做。 ## JsonNodeFactory 構建JsonNode工廠類。話不多說,用幾個例子跑一跑。 ### 值型別節點(ValueNode) 此類節點均為`ValueNode`的子類,特點是:一個節點表示一個值。 ```java @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() + "---"); } ``` 執行程式,輸出: ```java ------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、陣列。裡面可以放置任何節點 下面用示例感受一下它們的使用: ```java @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")); } ``` 執行程式,輸出: ```java ------構建一個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,如下圖所示: ![](https://img-blog.csdnimg.cn/20200820212454191.png#pic_center) ObjectMapper在讀取JSON後提供指向樹的根節點的指標, 根節點可用於**遍歷**完整的樹。 同樣的,我們可從讀(反序列化)、寫(序列化)兩個方面來展開。 ### 寫(序列化) 將Object寫為JsonNode,ObjectMapper給我們提供了三個實用API倆操作它: ![](https://img-blog.csdnimg.cn/20200821184426585.png#pic_center) #### 1、valueToTree(Object) 該方法屬相對較為常用:將任意物件(包括null)寫為一個JsonNode樹模型。功能上類似於先將Object序列化為JSON串,再讀為JsonNode,但很明顯這樣一步到位更加高效。 > 小貼士:高效不代表性能高,因為其內部實現好還是呼叫了`readTree()`方法的 ```java @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 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")); } ``` 執行程式,控制檯輸出: ```json 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更多詳解,請參見本系列前面幾篇文章): ```java @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); } } ``` 執行程式,控制檯輸出: ```json {"name":"YourBatman","age":18,"dog":null} ``` #### 3、writeTree(JsonGenerator,TreeNode) JsonNode是TreeNode的實現類,上面方法已經給出了使用示例,所以本方法不在贅述你應該不會有意見了吧。 ### 讀(反序列化) 將一個資源(如字串)讀取為一個JsonNode樹模型。 ![](https://img-blog.csdnimg.cn/2020082119261653.png#pic_center) 這是典型的方法過載設計,API更加友好,所有方法底層均為`_readTreeAndClose()`這個protected方法,可謂“萬劍歸宗”。 下面以最為常見的:讀取JSON字串為例,其它的舉一反三即可。 ```java @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串如下(模擬資料,總之你就當做它屬性很多、巢狀很深就對了): ```json {"name":"YourBatman","age":18,"dog":{"name":"旺財","color":"WHITE"},"hobbies":["籃球","football"]} ``` 這時候,我僅關心狗的顏色,腫麼辦呢?相信你已經想到了:樹模型 ```java @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即可)。這主要是利用了樹模型它具有動態可擴充套件的特性,滿足我們日益變化的結構: ```java @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); } ``` 執行程式,控制檯輸出: ```json -------------向結構裡動態新增節點------------ {"name":"YourBatman","age":18,"myDiy":{"contry":"China"}} ``` 說白了,也沒啥特殊的。拿到一個`JsonNode`後你可以任意的造它,就像`Map`一樣~ # ✍總結 樹模型(tree model) API比Jackson 流式(Streaming) API 簡單了很多,不管是生成 json字串還是解析json字串。但是相對於**自動化**的資料繫結而言還是比較複雜的。 樹模型(tree model) API在只需要取出一個大json串中的幾個值時比較方便。如果json中每個(大部分)值都需要獲得,那麼這種方式便顯得比較繁瑣了。因此在實際應用中具體問題具體分析,**但是,Jackson的樹模型你必須得掌握**。 ##### ✔推薦閱讀: - [Fastjson到了說再見的時候了](https://mp.weixin.qq.com/s/z6T9n9YvwjmDwuj-YTa0_w) - [1. 初識Jackson -- 世界上最好的JSON庫](https://mp.weixin.qq.com/s/iqSN4HUoIdX0kGcSdnD7EA) - [2. 媽呀,Jackson原來是這樣寫JSON的](https://mp.weixin.qq.com/s/p6cwP2BVrC8VxkN-T3uAxg) - [3. 懂了這些,方敢在簡歷上說會用Jackson寫JSON](https://mp.weixin.qq.com/s/ZHb3P06IC4xElHwJqDepxw) - [4. JSON字串是如何被解析的?JsonParser瞭解一下](https://mp.weixin.qq.com/s/syAOETfEawiGItaQO35mLA) - [5. JsonFactory工廠而已,還蠻有料,這是我沒想到的](https://mp.weixin.qq.com/s/0ZaDYb_ueFLbvOf3FbetfQ) - [6. 二十不惑,ObjectMapper使用也不再迷惑](https://mp.weixin.qq.com/s/VYy1QVeFLRkciymFHueb5w)