每棵大樹,都曾只是一粒種子。本文已被 https://www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。
✍前言
你好,我是YourBatman。
上篇文章 體驗了一把ObjectMapper在資料繫結方面的應用,用起來還是蠻方便的有木有,為啥不少人說它難用呢,著實費解。我群裡問了問,主要原因是它不是靜態方法呼叫,並且方法名取得不那麼見名之意......
雖然ObjectMapper
在資料繫結上既可以處理簡單型別(如Integer、List、Map等),也能處理完全型別(如POJO),看似無所不能。但是,若有如下場景它依舊不太好實現:
- 碩大的JSON串中我只想要某一個(某幾個)屬性的值而已
- 臨時使用,我並不想建立一個POJO與之對應,只想直接使用值即可(型別轉換什麼的我自己來就好)
- 資料結構高度動態化
為了解決這些問題,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結構,根節點 就是一個ObjectNodeArrayNode
:類比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的樹模型你必須得掌握。