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