你必須非常努力,才能看起來毫不費力。本文已被 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:調用
便會自動關閉底層的I/O流,你無需再關心JsonGenerator#close()
- 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
代替,應用在JsonFactory上,後文詳解
JsonWriteFeature#QUOTE_FIELD_NAMES
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
這個特征挺有意思,看例子(以寫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
@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
該特性強制将所有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
執行個體,因地制宜和互相隔離。