沒有人永遠18歲,但永遠有人18歲。本文已被 https://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
夯實基礎,畢竟做技術的要知其然,知其所以然了後,面對問題才能坦然。