3. 懂了這些,方敢在簡歷上說會用Jackson寫JSON

YourBatman發表於2020-07-27

你必須非常努力,才能看起來毫不費力。本文已被 https://www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。

前言

各位好,我是A哥(YourBatman)。上篇文章:2. 媽呀,Jackson原來是這樣寫JSON的 知道了Jackson寫JSON的姿勢,切實感受了一把ObjectMapper原來是這樣完成序列化的...本文繼續深入討論JsonGenerator寫JSON的細節。

先閒聊幾句題外話哈。我們在書寫簡歷的時候,都會用一定篇幅展示自己的技能點(亮點),就像這樣:

這一part非常重要,它決定了面試官是否有跟你聊的興趣,決定了你是否能在浩如煙海的簡歷中夠脫穎而出。如何做到差異性?在當下如此發達的資訊社會裡,資訊的獲取唾手可得,所以在知識的廣度方面,我認為人與人之間的差異其實並不大:

你知道DDD領域驅動、讀過架構整潔之道、知道六邊形架構、知道DevOps......難道你還在想憑一些概念賣錢?拉出差距?
你在用Spring技術棧、在用Redis、在用ElasticSearch......難道你還以為現在像10年前一樣,會用就能加分?

一聊就會,一問就退,一寫就廢。這是很多公司程式設計師的真實寫照,基/中層管理者尤甚。早早的和技術漸行漸遠,導致裁員潮到來時很容易獲得一張“飛機票”,年紀越大,焦慮感越強。

在你的公司是否有過這種場景:四五個人指揮一個人幹活。對,就像這樣:


扎不扎心,老鐵?。不過不用悲觀,從這應該你看到的是機會,習xx都說了實幹才能興邦嘛,2019年裁員潮洗牌後,適者生存,不適者很多回老家了,這也讓大批很有實力的程式設計師享受到了紅利。應正了那句:當大潮褪去,才知道誰在裸泳

扯遠了,言歸正傳。Jackson單會簡單使用我認為還不足矣立足,那就跟我來吧~

版本約定

  • Jackson版本:2.11.0
  • Spring Framework版本:5.2.6.RELEASE
  • Spring Boot版本:2.3.0.RELEASE

正文

一個框架/庫好不好,不是看它的核心功能做得怎麼樣,而是非核心功能處理得如何。比如後臺頁面做得咋樣?容錯機制呢?定製化、可配置化,擴充套件性等等。

Jackson稱得上優秀(甚至最佳)最主要是得益於它優秀的module模組化設計,在接觸其之前,我們先完成本章節的內容:JsonGenerator寫JSON的行為控制(配置)。

配置屬於程式的一部分,它影響著程式執行的方方面面。Spring使用Environment/PropertySource管理配置,對應的在Jackson裡會看到有很多Feature類來控制Jackson的讀/寫行為,均是使用enum列舉型別來管理。

上篇文章 我們學會了如何使用JsonGenerator去寫一個JSON,本文將來學習它的需要掌握的使用細節。同樣的,為圍繞著JsonGenerator展開。

JsonGenerator的Feature

它是JsonGenerator的一個內部列舉類,共10個列舉值:

public enum Feature {

	// Low-level I/O
	AUTO_CLOSE_TARGET(true),
	AUTO_CLOSE_JSON_CONTENT(true),
	FLUSH_PASSED_TO_STREAM(true),

	// Quoting-related features
	@Deprecated
	QUOTE_FIELD_NAMES(true),
	@Deprecated
	QUOTE_NON_NUMERIC_NUMBERS(true),
	@Deprecated
	ESCAPE_NON_ASCII(false),
	@Deprecated
	WRITE_NUMBERS_AS_STRINGS(false),

	// Schema/Validity support features
	WRITE_BIGDECIMAL_AS_PLAIN(false),
	STRICT_DUPLICATE_DETECTION(false),
	IGNORE_UNKNOWN(false);
	
	...
}

小貼士:列舉值均為bool型別,括號內為預設值

這個Feature的每個列舉值都控制著JsonGenerator寫JSON時的不同行為,並且可分為三大類(原始碼處我也有標註):

  • Low-level I/O:底層I/O流相關。

Jackson的流式API指的是I/O流,因此就涉及到關流、flush重新整理流等操作

  • Quoting-related:雙引號""引用相關。

JSON規範規定key都必須有雙引號,但這對於某些場景下並不需要

  • Schema/Validity support:約束/規範/校驗相關。

JSON作為K-V結構的資料,那麼允許相同key出現嗎?這便由這些特徵去控制

下面分別來認識認識它們。

AUTO_CLOSE_TARGET(true)

含義即為字面意:自動關閉目標(流)。

  • true:呼叫JsonGenerator#close()便會自動關閉底層的I/O流,你無需再關心
  • false:底層I/O流請手動關閉

自動關閉:

@Test
public void test1() throws IOException {
    JsonFactory factory = new JsonFactory();
    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
        // doSomething
    }
}

如果改為false:那麼你就需要自己手動去close底層使用的OutputStream或者Writer。形如這樣:

@Test
public void test2() throws IOException {
    JsonFactory factory = new JsonFactory();
    try (PrintStream err = System.err; JsonGenerator jg = factory.createGenerator(err, JsonEncoding.UTF8)) {
        // 特徵置為false 採用手動關流的方式
        jg.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);

        // doSomething
    }
}

小貼士:例子均採用try-with-resources方式關流,所以並沒有顯示呼叫close()方法,你應該能懂吧?

AUTO_CLOSE_JSON_CONTENT(true)

先來看下面這段程式碼:

@Test
public void test3() throws IOException {
    JsonFactory factory = new JsonFactory();
    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
        jg.writeStartObject();
        jg.writeFieldName("names");

        // 寫陣列
        jg.writeStartArray();
        jg.writeString("A哥");
        jg.writeString("YourBatman");
    }
}

執行程式,輸出:

{"names":["A哥","YourBatman"]}

wow,竟然輸出一切正常。細心的你會發現,我的程式碼是缺胳膊少腿的:不管是Object還是Array都只start了,並沒有顯示呼叫end進行閉合。但是呢,結果卻正常得很,這便是此Feature的作用了。

  • true:自動補齊(閉合)JsonToken#START_ARRAYJsonToken#START_OBJECT型別的內容
  • false:啥都不做(不會主動拋錯哦)

不過還是要囉嗦一句:雖然Jackson通過此Feature做了容錯,但是自己在使用時,請務必顯示書寫閉合

FLUSH_PASSED_TO_STREAM(true)

在使用帶有緩衝區的I/O寫資料時,缺少“臨門一腳”是初學者很容易犯的錯誤,比如下面這個例子:

@Test
public void test4() throws IOException {
    JsonFactory factory = new JsonFactory();
    JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8);

    jg.writeStartObject();
    jg.writeStringField("name","A哥");
    jg.writeEndObject();

    // jg.flush();
    // jg.close();
}

執行程式,控制檯沒有任何輸出。把註釋程式碼放開任何一行,再次執行程式,控制檯正常輸出:

{"name":"A哥"}
  • true:當JsonGenerator呼叫close()/flush()方法時,自動強刷I/O流裡面的資料
  • false:請手動處理

為何需要flush()?

對於此問題這裡小科普一下。因為向磁碟、網路寫入資料的時候,出於效率的考慮,作業系統(話外音:這是作業系統為之)並不是輸出一個位元組就立刻寫入到檔案或者傳送到網路,而是把輸出的位元組先放到記憶體的一個緩衝區裡(本質上就是一個byte[]陣列),等到緩衝區寫滿了,再一次性寫入檔案或者網路。對於很多IO裝置來說,一次寫一個位元組和一次寫1000個位元組,花費的時間幾乎是完全一樣的,所以OutputStream有個flush()方法,能強制把緩衝區內容輸出。

小貼士:InputStream是沒有flush()方法的哦

通常情況下,我們不需要呼叫這個flush()方法,因為緩衝區寫滿了,OutputStream會自動呼叫它,並且,在呼叫close()方法關閉OutputStream之前,也會自動呼叫flush()方法強制刷一次緩衝區。但是,在某些情況下,我們必須手動呼叫flush()方法,比如上例子,比如發IM訊息...

QUOTE_FIELD_NAMES(true)

此屬性自2.10版本後已過期,使用JsonWriteFeature#QUOTE_FIELD_NAMES代替,應用在JsonFactory上,後文詳解

JSON物件欄位名是否為使用""雙引號括起來,這是JSON規範(RFC4627)規定的。

  • true:欄位名使用""括起來 -> 遵循JSON規範
  • false:欄位名使用""括起來 -> 遵循JSON規範
@Test
public void test5() throws IOException {
    JsonFactory factory = new JsonFactory();
    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
		// jg.disable(QUOTE_FIELD_NAMES);
		
        jg.writeStartObject();
        jg.writeStringField("name","A哥");
        jg.writeEndObject();
    }
}

執行程式,輸出:

{"name":"A哥"}

99.99%的情況下我們不需要改變預設值。Jackson新增了禁用引號的功能以支援那非常不常見的情況,最常見的情況直接從Javascript中使用時可能會發生。

開啟註釋掉的語句,再次執行程式,輸出:

{name:"A哥"}

QUOTE_NON_NUMERIC_NUMBERS(true)

此屬性自2.10版本後已過期,使用JsonWriteFeature#WRITE_NAN_AS_STRINGS代替,應用在JsonFactory上,後文詳解

這個特徵挺有意思,看例子(以寫Float為例):

@Test
public void test6() throws IOException {
    JsonFactory factory = new JsonFactory();
    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
    	// jg.disable(JsonGenerator.Feature.QUOTE_NON_NUMERIC_NUMBERS);

        jg.writeNumber(0.9);
        jg.writeNumber(1.9);

        jg.writeNumber(Float.NaN);
        jg.writeNumber(Float.NEGATIVE_INFINITY);
        jg.writeNumber(Float.POSITIVE_INFINITY);
    }
}

執行程式,輸出:

0.9 1.9 "NaN" "-Infinity" "Infinity"

同為Float數字型別,有的輸出有""雙引號包著,有的沒有。放開註釋的語句(禁用此特徵),再次執行程式,輸出:

0.9 1.9 NaN -Infinity Infinity

很明顯,如果你是這麼輸出為一個JSON的話,那它就會是非法的JSON,是不符合JSON標準的(因為像NaN、Infinity這種明顯是字串嘛,必須用""包起來才是合法的value值)。

由於JSON規範中對數字的嚴格定義,加上Java可能具有的開放式數字集(如上例中Float型別並不100%是數字),很難做到既安全又方便,因此有了此特徵讓你根據需要來控制。

ESCAPE_NON_ASCII(false)

此屬性自2.10版本後已過期,使用JsonWriteFeature#ESCAPE_NON_ASCII代替,應用在JsonFactory上,後文詳解

@Test
public void test7() throws IOException {
    JsonFactory factory = new JsonFactory();
    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
        // jg.enable(ESCAPE_NON_ASCII);
        jg.writeString("A哥");
    }
}

執行程式,輸出:

"A哥"

放開注掉的程式碼(開啟此屬性),再次執行,輸出:

"A\u54E5"

WRITE_NUMBERS_AS_STRINGS(false)

此屬性自2.10版本後已過期,使用JsonWriteFeature#WRITE_NUMBERS_AS_STRINGS代替,應用在JsonFactory上,後文詳解

該特性強制所有Java數字寫成字串,即使底層資料格式真的是數字。

  • true:所有數字強制寫為字串
  • false:不做處理
@Test
public void test8() throws IOException {
    JsonFactory factory = new JsonFactory();
    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
        // jg.enable(WRITE_NUMBERS_AS_STRINGS);

        Long num = Long.MAX_VALUE;
        jg.writeNumber(num);
    }
}

執行程式,輸出:

9223372036854775807

放開註釋程式碼(開啟此特徵),再次執行程式,輸出:

"9223372036854775807"

有什麼使用場景?一個用例是避免Javascript限制的問題:因為Javascript標準規定所有的數字處理都應該使用64位ieee754浮點值來完成,結果是一些64位整數值不能被精確表示(因為尾數只有51位寬)。

採坑提醒:時間戳後端用Long型別反給前端是沒有問題的。但如果你是很大的一個Long值(如雪花演算法算出的很大的Long值),直接返回前端的話,Javascript就會出現精度丟失的bug

WRITE_BIGDECIMAL_AS_PLAIN(false)

控制寫java.math.BigDecimal的行為:

  • true:使用BigDecimal#toPlainString()方法輸出
  • false: 使用預設輸出方式(取決於BigDecimal是如何構造的)
@Test
public void test7() throws IOException {
    JsonFactory factory = new JsonFactory();
    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
        // jg.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN);

        BigDecimal bigDecimal1 = new BigDecimal(1.0);
        BigDecimal bigDecimal2 = new BigDecimal("1.0");
        BigDecimal bigDecimal3 = new BigDecimal("1E11");
        jg.writeNumber(bigDecimal1);
        jg.writeNumber(bigDecimal2);
        jg.writeNumber(bigDecimal3);
    }
}

執行程式,輸出:

1 1.0 1E+11

放開註釋程式碼,再次執行程式,輸出:

1 1.0 100000000000

STRICT_DUPLICATE_DETECTION(false)

是否去嚴格的檢測重複屬性名。

  • true:檢測是否有重複欄位名,若有,則丟擲JsonParseException異常
  • false:不檢測JSON物件重複的欄位名,即:相同欄位名都要解析
@Test
public void test8() throws IOException {
    JsonFactory factory = new JsonFactory();
    try (JsonGenerator jg = factory.createGenerator(System.err, JsonEncoding.UTF8)) {
        // jg.enable(JsonGenerator.Feature.STRICT_DUPLICATE_DETECTION);

        jg.writeStartObject();
        jg.writeStringField("name","YourBatman");
        jg.writeStringField("name","A哥");
        jg.writeEndObject();
    }
}

執行程式,輸出:

{"name":"YourBatman","name":"A哥"}

開啟註釋掉的哪行程式碼:開啟此特徵值為true。再次執行程式,輸出:

com.fasterxml.jackson.core.JsonGenerationException: Duplicate field 'name'

	at com.fasterxml.jackson.core.json.JsonWriteContext._checkDup(JsonWriteContext.java:224)
	at com.fasterxml.jackson.core.json.JsonWriteContext.writeFieldName(JsonWriteContext.java:217)
	...

注意:謹慎開啟此開關,如果檢查的話效能會下降20%-30%。

IGNORE_UNKNOWN(false)

如果底層資料格式需要輸出所有屬性,以及如果找不到呼叫者試圖寫入的屬性的定義,則該特性確定是否要執行的操作。

可能你聽完還一臉懵逼,什麼底層資料格式,什麼找不到,我明明是寫JSON啊,何解?其實這不是針對於寫JSON來說的,對於JSON,這個特性沒有效果,因為屬性不需要預先定義。通常,大多數文字資料格式不需要模式資訊,而某些二進位制資料格式需要定義(如Avro、protobuf),因此這個屬性是為它們而生(Smile、BSON等這些二進位制也是不需要預定模式資訊的哦)。

強調:JsonGenerator不是隻能寫JSON格式,畢竟底層是I/O流嘛,理論上啥都能寫

  • true:啟動該功能

可以預先呼叫(在寫資料之前)這個API設定好模式資訊即可:

JsonGenerator:

	public void setSchema(FormatSchema schema) {
		...
	}
  • false:禁用該功能。如果底層資料格式需要所有屬性的知識才能輸出,那就丟擲JsonProcessingException異常

定製Feature

通過上一part知曉了控制JsonGenerator的特徵值們,以及其作用是。Feature的每個列舉值都有個預設值(括號裡面),那麼如果我們希望對不同的JsonGenerator例項應用不同的配置該怎麼辦呢?

自然而然的JsonGenerator提供了相關API供以我們操作:

// 開啟
public abstract JsonGenerator enable(Feature f);
// 關閉
public abstract JsonGenerator disable(Feature f);
// 開啟/關閉
public final JsonGenerator configure(Feature f, boolean state) { ... };

public abstract boolean isEnabled(Feature f);
public boolean isEnabled(StreamWriteFeature f) { ... };

替換者:StreamWriteFeature

本類是2.10版本新增的,用於完全替換上面的Feature。目的:完全獨立的屬性配置,不依賴於任何後端格式,因為JsonGenerator並不侷限於寫JSON,因此把Feature放在JsonGenerator作為內部類是不太合適的,所以單獨摘出來。

StreamWriteFeature用在JsonFactory裡,後面再講解到它的構建器JsonFactoryBuilder時再詳細探討。

序列化POJO物件

上篇文章用程式碼演示過了如何使用writeObject(Object pojo)來把一個POJO一次性序列化成為一個JSON串,它主要依賴於ObjectCodec去完成:

public abstract JsonGenerator setCodec(ObjectCodec oc);

ObjectCodec可謂是Jackson裡極其重要的一個基礎元件,我們最熟悉的ObjectMapper它就是一個解碼器,實現了序列化和反序列化、樹模型等操作。這將在後面章節裡重點介紹~

輸出漂亮的JSON格式

我們知道JSON之所以快速流行的原因之一是得益於它的可讀性好,可讀性好又表現在它漂亮的(規則)的展示格式上。

預設情況下,使用JsonGenerator寫JSON時,所有的部分都是輸出在同一行裡,顯然這種格式對人閱讀來說是不夠友好的。作為最流行的JSON庫自然考慮到了這一點,提供了格式化器來美化輸出

// 自己指定漂亮格式列印器
public JsonGenerator setPrettyPrinter(PrettyPrinter pp) { ... }

// 應用預設的漂亮格式列印器
public abstract JsonGenerator useDefaultPrettyPrinter();

PrettyPrinter有如下兩個實現類:

使用不同的實現類,對輸出結果的影響如下:

什麼都不設定:
MinimalPrettyPrinter:
{"zhName":"A哥","enName":"YourBatman","age":18}

DefaultPrettyPrinter:
useDefaultPrettyPrinter():
{
  "zhName" : "A哥",
  "enName" : "YourBatman",
  "age" : 18
}

由此可見,在什麼都不設定的情況下,結果會全部在一行顯示(緊湊型輸出)。DefaultPrettyPrinter表示帶層級格式的輸出(可讀性好),若有此需要,建議直接呼叫更為快捷的useDefaultPrettyPrinter()方法,而不用自己去new一個例項。

總結

本文的主要內容和重點是介紹了用Feature去控制JsonGenerator的寫行為,不同的特徵值控制著不同的行為。在實際使用時可針對不同的需求,定製出不同的JsonGenerator例項,因地制宜和互相隔離。

相關推薦:

相關文章