2. 媽呀,Jackson原來是這樣寫JSON的
- 2020 年 7 月 24 日
- 筆記
- A哥學Jackson, Fastjson, jackson, JSON庫
沒有人永遠18歲,但永遠有人18歲。本文已被 //www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。
前言
各位好,我是A哥(YourBatman)。上篇文章 整體介紹了世界上最好的JSON庫 — Jackson,對它有了整體了解:知曉了它是個生態,其它的僅是個JSON庫而已。
有人說Jackson小眾?那麼請先看看上篇文章吧。學Jackson性價比特別高,因為它使用廣泛、會的人少,因此在團隊內如果你能精通,附加價值的效應就會非常明顯了…
我撓頭想了想,本系列來不了虛的,只能肝。本系列教程不僅僅教授基本使用,目標是搞完後能夠解決日常99.99%的問題,畢竟每個小團隊都最好能有某些方面的小專家,畢竟大家都不乏遇見過一個技術問題卡一天的情況。只有從底層把握,方能遊刃有餘。
命名為core的模組一般都不簡單,jackson-core
自然也不例外。它是三大核心模組之一,並且是核心中的核心,提供了對JSON數據的完整支援(包括各種讀、寫)。它是三者中最強大的模組,具有最低的開銷和最快的讀/寫操作。
此模組提供了最具底層的Streaming JSON解析器/生成器,這組流式API屬於Low-Level API,具有非常顯著的特點:
- 開銷小,損耗小,性能極高
- 因為是Low-Level API,所以靈活度極高
- 又因為是Low-Level API,所以易錯性高,可讀性差
jackson-core模組提供了兩種處理JSON的方式(縱纜整個Jackson共三種):
- 流式API:讀取並將JSON內容寫入作為離散事件 ->
JsonParser
讀取數據,而JsonGenerator
負責寫入數據 - 樹模型:JSON文件在記憶體里以樹形式表示。此種方式也很靈活,它類似於XML的DOM解析,層層嵌套的
作為「底層」技術,應用級開發中確實接觸不多。為了引起你的重視,提前預告一下:Spring MVC
對JSON消息的轉換器AbstractJackson2HttpMessageConverter
它就用到了底層流式API -> JsonGenerator寫數據。想不想拿下Spring呢?我想你的答案應該是Yes吧~
相信做難事必有所得,你我他都會用的技術、都能解決的問題,那絕成不了你的核心競爭力,自然在團隊內就難成發光體。
版本約定
原則:均選當前最新版本(忽略小版本)
- Jackson版本:
2.11.0
- Spring Framework版本:
5.2.6.RELEASE
- Spring Boot版本:
2.3.0.RELEASE
- 內置的Jackson和Spring版本均和👆保持一致,避免了版本交叉
說明:類似2.11.0和2.11.x這種小版本號的差異,你權可認為沒有區別
工程結構
鑒於是首次展示工程示例程式碼,將基本結構展示如下:
全部源碼地址在本系列的最後一篇文章中會全部公示出來
正文
Jackson提供了一種對性能有極致要求的方式:流式API。它用於對性能有極致要求的場景,這個時候就可以使用此種方式來對JSON進行讀寫。
概念解釋:流式、增量模式、JsonToken
- 流式(Streaming):此概念和Java8中的Stream流是不同的。這裡指的是IO流,因此具有最低的開銷和最快的讀/寫操作(記得關流哦)
- 增量模式(incremental mode):它表示每個部分一個一個地往上增加,類似於壘磚。使用此流式API讀寫JSON的方式使用的均是增量模式
- JsonToken:每一部分都是一個獨立的Token(有不同類型的Token),最終被「拼湊」起來就是一個JSON。這是流式API里很重要的一個抽象概念。
關於增量模式和Token概念,在Spirng的SpEL表達式中也有同樣的概念,這在Spring相關專欄里你將會再次體會到
本文將看看它是如何寫JSON數據的,也就是JsonGenerator
。
JsonGenerator使用Demo
JsonGenerator
定義用於編寫JSON內容的公共API的基類(抽象類)。實例使用的工廠方法創建,也就是JsonFactory
。
小貼士:縱觀整個Jackson,它更多的是使用抽象類而非介面,這是它的一大「特色」。因此你熟悉的面向介面編程,到這都要轉變為面向抽象類編程嘍。
話不多說,先來一個Demo感受一把:
@Test
public void test1() throws IOException {
JsonFactory factory = new JsonFactory();
// 本處只需演示,向控制台寫(當然你可以向文件等任意地方寫都是可以的)
JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8);
try {
jsonGenerator.writeStartObject(); //開始寫,也就是這個符號 {
jsonGenerator.writeStringField("name", "YourBatman");
jsonGenerator.writeNumberField("age", 18);
jsonGenerator.writeEndObject(); //結束寫,也就是這個符號 }
} finally {
jsonGenerator.close();
}
}
因為JsonGenerator實現了AutoCloseable
介面,因此可以使用try-with-resources
優雅關閉資源(這也是推薦的使用方式),程式碼改造如下:
@Test
public void test1() throws IOException {
JsonFactory factory = new JsonFactory();
// 本處只需演示,向控制台寫(當然你可以向文件等任意地方寫都是可以的)
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject(); //開始寫,也就是這個符號 {
jsonGenerator.writeStringField("name", "YourBatman");
jsonGenerator.writeNumberField("age", 18);
jsonGenerator.writeEndObject(); //結束寫,也就是這個符號 }
}
}
運行程式,控制台輸出:
{"name":"YourBatman","age":18}
這是最簡使用示例,這也就是所謂的序列化底層實現,從示例中對增量模式能夠有所感受吧。
純手動檔有木有,靈活性和性能極高,但易出錯。這就像頭文字D的賽車一樣,先要速度、高性能、靈活性,那必須上手動檔。
JsonGenerator詳細介紹
JsonGenerator是個抽象類,它的繼承體系如下:
WriterBasedJsonGenerator
:基於java.io.Writer處理字元編碼(話外音:使用Writer輸出JSON)- 因為UTF-8編碼基本標準化了,因此Jackson內部也提供了
SegmentedStringWriter/UTF8Writer
來簡化操作
- 因為UTF-8編碼基本標準化了,因此Jackson內部也提供了
UTF8JsonGenerator
:基於OutputStream + UTF-8處理字元編碼(話外音:明確指定了使用UTF-8編碼把位元組變為字元)
默認情況下(不指定編碼),Jackson默認會使用UTF-8進行編碼,也就是說會使用UTF8JsonGenerator
作為實際的JSON生成器實現類,具體邏輯將在講述JsonFactory
章節中有所體現,敬請關注。
值得注意的是,抽象基類JsonGenerator
它只負責JSON的生成,至於把生成好的JSON寫到哪裡去它並不關心。比如示例中我給寫到了控制台,當然你也可以寫到文件、寫到網路等等。
Spring MVC中的JSON消息轉換器就是向
HttpOutputMessage
(網路輸出流)里寫JSON數據
關鍵API
JsonGenerator
雖然僅是抽象基類,但Jackson它建議我們使用JsonFactory
工廠來創建其實例,並不需要使用者去關心其底層實現類,因此我們僅需要面向此抽象類編程即可,此為對使用者非常友好的設計。
對於JSON生成器來說,寫方法自然是它的靈魂所在。眾所周知,JSON屬於K-V數據結構,因此針對於一個JSON來說,每一段都k額分為寫key和寫value兩大階段。
寫JSON Key
JsonGenerator一共提供了3個方法用於寫JSON的key:
@Test
public void test2() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("zhName");
jsonGenerator.writeEndObject();
}
}
運行程式,輸出:
{"zhName"}
可以發現,key可以獨立存在(無需value),但value是不能獨立存在的哦,下面你會看到效果。而3個方法中的其它2個方法:
public abstract void writeFieldName(SerializableString name) throws IOException;
public void writeFieldId(long id) throws IOException {
writeFieldName(Long.toString(id));
}
這兩個方法,你可以忘了吧,記住writeFieldName()
就足夠了。
總的來說,寫JSON的key非常簡單的,這得益於JSON的key有且僅可能是String類型,所以情況單一。下面繼續了解較為複雜的寫Value的情況。
寫JSON Value
我們知道在Java中數據存在的形式(類型)非常之多,比如String、int、Reader、char[]…,而在JSON中值的類型只能是如下形式:
- 字元串(如
{ "name":"YourBatman" }
) - 數字(如
{ "age":18 }
) - 對象(JSON 對象)(如
{ "person":{ "name":"YourBatman", "age":18}}
) - 數組(如
{"names":[ "YourBatman", "A哥" ]}
) - 布爾(如
{ "success":true }
) - null(如:
{ "name":null }
)
小貼士:像數組、對象等這些「高級」類型可以互相無限嵌套
很明顯,Java中的數據類型和JSON中的值類型並不是一一對應的關係,那麼這就需要JsonGenerator
在寫入時起到一個橋樑(適配)作用:
下面針對不同的Value類型分別作出API講解,給出示例說明。在此之前,請先記住兩個結論,會更有利於你理解示例:
- JSON的順序,和你write的順序保持一致
- 寫任何類型的Value之前請記得先write寫key,否則可能無效
字元串
可把Java中的String類型、Reader類型、char[]字元數組類型等等寫為JSON的字元串形式。
@Test
public void test3() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("zhName");
jsonGenerator.writeString("A哥");
jsonGenerator.writeFieldName("enName");
jsonGenerator.writeString("YourBatman");
jsonGenerator.writeEndObject();
}
}
運行程式,輸出:
{"zhName":"A哥","enName":"YourBatman"}
數字
參考上例,不解釋。
對象(JSON 對象)
@Test
public void test4() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("zhName");
jsonGenerator.writeString("A哥");
// 寫對象(記得先寫key 否則無效)
jsonGenerator.writeFieldName("person");
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("enName");
jsonGenerator.writeString("YourBatman");
jsonGenerator.writeFieldName("age");
jsonGenerator.writeNumber(18);
jsonGenerator.writeEndObject();
jsonGenerator.writeEndObject();
}
}
運行程式,輸出:
{"zhName":"A哥","person":{"enName":"YourBatman","age":18}}
對象屬於一個比較特殊的value值類型,可以實現各種嵌套。也就是我們平時所說的JSON套JSON
數組
寫數組和寫對象有點類似,也會有先start再end的閉環思路。
如何向數組裡寫入Value值?我們知道JSON數組裡可以裝任何數據類型,因此往裡寫值的方法都可使用,形如這樣:
@Test
public void test5() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("zhName");
jsonGenerator.writeString("A哥");
// 寫數組(記得先寫key 否則無效)
jsonGenerator.writeFieldName("objects");
jsonGenerator.writeStartArray();
// 1、寫字元串
jsonGenerator.writeString("YourBatman");
// 2、寫對象
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("enName", "YourBatman");
jsonGenerator.writeEndObject();
// 3、寫數字
jsonGenerator.writeNumber(18);
jsonGenerator.writeEndArray();
jsonGenerator.writeEndObject();
}
}
運行程式,輸出:
{"zhName":"A哥","objects":["YourBatman",{"enName":"YourBatman"},18]}
理論上JSON數組裡的每個元素可以是不同類型,但原則上請確保是同一類型哦
對於JSON數組類型,很多時候裡面裝載的是數字或者普通字元串類型,因此JsonGenerator
也很暖心的為此提供了專用方法(可以調用該方法來一次性便捷的寫入單個數組):
@Test
public void test6() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("zhName");
jsonGenerator.writeString("A哥");
// 快捷寫入數組(從第index = 2位開始,取3個)
jsonGenerator.writeFieldName("values");
jsonGenerator.writeArray(new int[]{1, 2, 3, 4, 5, 6}, 2, 3);
jsonGenerator.writeEndObject();
}
}
運行程式,輸出:
{"zhName":"A哥","values":[3,4,5]}
布爾和null
比較簡單,JsonGenerator各提供了一個方法供你使用:
public abstract void writeBoolean(boolean state) throws IOException;
public abstract void writeNull() throws IOException;
示例程式碼:
@Test
public void test7() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();
jsonGenerator.writeFieldName("success");
jsonGenerator.writeBoolean(true);
jsonGenerator.writeFieldName("myName");
jsonGenerator.writeNull();
jsonGenerator.writeEndObject();
}
}
運行程式,輸出:
{"success":true,"myName":null}
組合寫JSON Key和Value
在寫每個value之前,都必須寫key。為了簡化書寫,JsonGenerator提供了二合一的組合方法,一個頂兩:
@Test
public void test8() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeStartObject();
jsonGenerator.writeStringField("zhName","A哥");
jsonGenerator.writeBooleanField("success",true);
jsonGenerator.writeNullField("myName");
// jsonGenerator.writeObjectFieldStart();
// jsonGenerator.writeArrayFieldStart();
jsonGenerator.writeEndObject();
}
}
運行程式,輸出:
{"zhName":"A哥","success":true,"myName":null}
實際使用時,推薦使用這些組合方法去簡化書寫,畢竟新蓋中蓋高鈣片,一片能頂過去2片,效率高。
其它寫方法
如果說上面寫方法是必修課,那下面的write寫方法就當選修課吧。
writeRaw()和writeRawValue():
該方法將強制生成器不做任何修改地逐字複製輸入文本(包括不進行轉義,也不添加分隔符,即使上下文[array,object]可能需要這樣做)。如果需要這樣的分隔符,請改用writeRawValue方法。
絕大多數情況下,使用writeRaw()就夠了,writeRawValue的使用場景愈發的少
@Test
public void test9() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.out, JsonEncoding.UTF8)) {
jsonGenerator.writeRaw("{'name':'YourBatman'}");
}
}
運行程式,輸出:
{'name':'YourBatman'}
如果換成writeString()
方法,結果為(請注意比較差異):
"{'name':'YourBatman'}"
writeBinary():
使用Base64編碼把數據寫進去。
writeEmbeddedObject():
2.8版本新增的方法。看看此方法的源碼你就知道它是什麼意思,不解釋:
public void writeEmbeddedObject(Object object) throws IOException {
// 01-Sep-2016, tatu: As per [core#318], handle small number of cases
if (object == null) {
writeNull();
return;
}
if (object instanceof byte[]) {
writeBinary((byte[]) object);
return;
}
throw new JsonGenerationException(...);
}
writeObject()(重要):
寫POJO,但前提是你必須給JsonGenerator
指定一個ObjectCodec
解碼器才能正常work,否則拋出異常:
java.lang.IllegalStateException: No ObjectCodec defined for the generator, can only serialize simple wrapper types (type passed cn.yourbatman.jackson.core.beans.User)
at com.fasterxml.jackson.core.JsonGenerator._writeSimpleObject(JsonGenerator.java:2238)
at com.fasterxml.jackson.core.base.GeneratorBase.writeObject(GeneratorBase.java:391)
...
值得注意的是,Jackson里我們最為熟悉的API ObjectMapper
它就是一個ObjectCodec解碼器,具體我們在數據綁定章節會再詳細討論,下面我給出個簡單的使用示例模擬一把:
準備一個User對象,以及解碼器UserObjectCodec:
@Data
public class User {
private String name = "YourBatman";
private Integer age = 18;
}
// 自定義ObjectCodec解碼器 用於把User寫為JSON
// 因為本例只關注write寫,因此只需要實現此這一個方法即可
public class UserObjectCodec extends ObjectCodec {
...
@Override
public void writeValue(JsonGenerator gen, Object value) throws IOException {
User user = User.class.cast(value);
gen.writeStartObject();
gen.writeStringField("name",user.getName());
gen.writeNumberField("age",user.getAge());
gen.writeEndObject();
}
...
}
測試用例:
@Test
public void test11() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
jsonGenerator.setCodec(new UserObjectCodec());
jsonGenerator.writeObject(new User());
}
}
運行程式,輸出:
{"name":"YourBatman","age":18}
😄這就是ObjectMapper
的原理雛形,是不是開始著道了?😄
writeTree():
顧名思義,它便是Jackson大名鼎鼎的樹模型。可惜的是core模組並沒有提供樹模型TreeNode的實現,以及它也是得依賴於ObjectCodec才能正常完成解碼。
方法用來編寫給定的JSON樹(表示為樹,其中給定的JsonNode是根)。這通常只調用給定節點的writeObject,但添加它是為了方便起見,並使程式碼在專門處理樹的情況下更顯式。
可能你會想,已經有了writeObject()
方法還要它幹啥呢?這其實是蠻有必要的,因為有時候你並不想定義POJO時,就可以用它快速寫/讀數據,同時它也可以達到模糊掉類型的概念,做到更抽象和更公用。
說到模糊掉類型的的操作,你也可以輔以Spring的
AnnotationAttributes
的設計和使用來理解
準備一個TreeNode的實現UserTreeNode:
public class UserTreeNode implements TreeNode {
private User user;
public User getUser() {
return user;
}
public UserTreeNode(User user) {
this.user = user;
}
...
}
UserObjectCodec改寫如下:
public class UserObjectCodec extends ObjectCodec {
...
@Override
public void writeValue(JsonGenerator gen, Object value) throws IOException {
User user = null;
if (value instanceof User) {
user = User.class.cast(value);
} else if (value instanceof TreeNode) {
user = UserTreeNode.class.cast(value).getUser();
}
gen.writeStartObject();
gen.writeStringField("name", user.getName());
gen.writeNumberField("age", user.getAge());
gen.writeEndObject();
}
...
}
書寫測試用例:
@Test
public void test12() throws IOException {
JsonFactory factory = new JsonFactory();
try (JsonGenerator jsonGenerator = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
jsonGenerator.setCodec(new UserObjectCodec());
jsonGenerator.writeObject(new UserTreeNode(new User()));
}
}
運行程式,輸出:
{"name":"YourBatman","age":18}
本案例繞過了TreeNode
的真實處理邏輯,是因為樹模型這塊會放在databind數據綁定模組進行更加詳細的描述,後面再會嘍。
說明:Jackson的樹模型是比較重要的,當然直接使用core模組的樹模型沒有意義,所以這裡先賣個關子,保持好奇心哈😄
思考題
國人很喜歡把Jackson的序列化(寫JSON)效率和Fastjson進行對比,那麼你敢使用本文的流式API和Fastjson比嗎?結果你猜一下呢?
總結
本文介紹了jackson-core模組的流式API,以及JsonGenerator寫JSON的使用,相信對你理解Jackson生成JSON方面是有幫助的。它作為JSON處理的基石,雖然並不推薦直接使用,但僅僅是應用開發級別不推薦哦,如果你是個框架、中間件開發者,這些原理你很可能繞不過。
還是那句話,本文介紹它的目的並不是建議大家去項目上使用,而是為了後面理解ObjectMapper
夯實基礎,畢竟做技術的要知其然,知其所以然了後,面對問題才能坦然。