天天看點

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

沒有人永遠18歲,但永遠有人18歲。本文已被 https://www.yourbatman.cn 收錄,裡面一并有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以免費學習。關注公衆号【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗辄止。
2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

前言

各位好,我是A哥(YourBatman)。

上篇文章

整體介紹了世界上最好的JSON庫 -- Jackson,對它有了整體了解:知曉了它是個生态,其它的僅是個JSON庫而已。

有人說Jackson小衆?那麼請先看看上篇文章吧。學Jackson成本效益特别高,因為它使用廣泛、會的人少,是以在團隊内如果你能精通,附加價值的效應就會非常明顯了...

我撓頭想了想,本系列來不了虛的,隻能肝。本系列教程不僅僅教授基本使用,目标是搞完後能夠解決日常99.99%的問題,畢竟每個小團隊都最好能有某些方面的小專家,畢竟大家都不乏遇見過一個技術問題卡一天的情況。隻有從底層把握,方能遊刃有餘。

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

命名為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吧~

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

相信做難事必有所得,你我他都會用的技術、都能解決的問題,那絕成不了你的核心競争力,自然在團隊内就難成發光體。

版本約定

原則:均選目前最新版本(忽略小版本)

  • Jackson版本:

    2.11.0

  • Spring Framework版本:

    5.2.6.RELEASE

  • Spring Boot版本:

    2.3.0.RELEASE

    • 内置的Jackson和Spring版本均和👆保持一緻,避免了版本交叉
說明:類似2.11.0和2.11.x這種小版本号的差異,你權可認為沒有差別

工程結構

鑒于是首次展示工程示例代碼,将基本結構展示如下:

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結
2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結
全部源碼位址在本系列的最後一篇文章中會全部公示出來

正文

Jackson提供了一種對性能有極緻要求的方式:流式API。它用于對性能有極緻要求的場景,這個時候就可以使用此種方式來對JSON進行讀寫。

概念解釋:流式、增量模式、JsonToken

  • 流式(Streaming):此概念和Java8中的Stream流是不同的。這裡指的是IO流,是以具有最低的開銷和最快的讀/寫操作(記得關流哦)
  • 增量模式(incremental mode):它表示每個部分一個一個地往上增加,類似于壘磚。使用此流式API讀寫JSON的方式使用的均是增量模式
  • JsonToken:每一部分都是一個獨立的Token(有不同類型的Token),最終被“拼湊”起來就是一個JSON。這是流式API裡很重要的一個抽象概念。
關于增量模式和Token概念,在Spirng的SpEL表達式中也有同樣的概念,這在Spring相關專欄裡你将會再次體會到
2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

本文将看看它是如何寫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的賽車一樣,先要速度、高性能、靈活性,那必須上手動檔。

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

JsonGenerator詳細介紹

JsonGenerator是個抽象類,它的繼承體系如下:

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結
  • 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兩大階段。

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

寫JSON Key

JsonGenerator一共提供了3個方法用于寫JSON的key:

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結
@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

在寫入時起到一個橋梁(适配)作用:

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

下面針對不同的Value類型分别作出API講解,給出示例說明。在此之前,請先記住兩個結論,會更有利于你了解示例:

  • JSON的順序,和你write的順序保持一緻
  • 寫任何類型的Value之前請記得先write寫key,否則可能無效

字元串

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

可把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"}           

數字

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

參考上例,不解釋。

對象(JSON 對象)

2. 媽呀,Jackson原來是這樣寫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的閉環思路。

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

如何向數組裡寫入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

也很暖心的為此提供了專用方法(可以調用該方法來一次性便捷的寫入單個數組):

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結
@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提供了二合一的組合方法,一個頂兩:

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結
@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片,效率高。

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

其它寫方法

如果說上面寫方法是必修課,那下面的write寫方法就當選修課吧。

writeRaw()和writeRawValue():

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

該方法将強制生成器不做任何修改地逐字複制輸入文本(包括不進行轉義,也不添加分隔符,即使上下文[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():

2. 媽呀,Jackson原來是這樣寫JSON的前言正文思考題總結

使用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

夯實基礎,畢竟做技術的要知其然,知其是以然了後,面對問題才能坦然。