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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章