2. 媽呀,Jackson原來是這樣寫JSON的

YourBatman發表於2020-07-24

沒有人永遠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共三種):

  1. 流式API:讀取並將JSON內容寫入作為離散事件 -> JsonParser讀取資料,而JsonGenerator負責寫入資料
  2. 樹模型: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來簡化操作
  • 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夯實基礎,畢竟做技術的要知其然,知其所以然了後,面對問題才能坦然。

相關文章