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

你必須非常努力,才能看起來毫不費力。本文已被 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實例,因地制宜和互相隔離。

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