少年易學老難成,一寸光陰不可輕。本文已被 https://www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。
前言
各位好,我是YourBatman。前面用四篇文章介紹完了Jackson底層流式API的讀(JsonParser)、寫(JsonGenerator)操作,我們清楚的知道,這哥倆都是abstract抽象類,使用時並沒有顯示的去new它們的(子類)例項,均通過一個工廠來搞定,這便就是本文的主角JsonFactory
。
通過名稱就知道,這是工廠設計模式。Jackson它並不建議你直接new讀/寫例項,因為那過於麻煩。為了對使用者遮蔽這些複雜的構造細節,於是就有了JsonFactory
例項工廠的出現。
可能有的人會說,一個物件工廠有什麼好了解的,很簡單嘛。非也非也,一件事情本身的複雜度並不會憑空消失,而是從一個地方轉移到另外一個地方,這另外一個地方指的就是JsonFactory。因此按照本系列的定位,瞭解它你繞不過去。
版本約定
- Jackson版本:
2.11.0
- Spring Framework版本:
5.2.6.RELEASE
- Spring Boot版本:
2.3.0.RELEASE
正文
JsonFactory是Jackson的(最)主要工廠類,用於 配置和構建JsonGenerator
和JsonParser
,這個工廠例項是執行緒安全的,因此可以重複使用。
作為一個例項工廠,它最重要的職責當然是建立例項物件。本工廠職責並不單一,它負責讀、寫兩種例項的建立工作。
建立JsonGenerator例項
JsonGenerator它負責向目的地寫資料,因此強調的是目的地在哪?如何寫?
如截圖所示,一共有六個過載方法用於構建JsonGenerator例項,多個過載方法目的是對使用者友好,我們可以認為最終效果是一樣的。比如,底層實現是:
JsonFactory:
@Override
public JsonGenerator createGenerator(OutputStream out, JsonEncoding enc) throws IOException {
IOContext ctxt = _createContext(out, false);
ctxt.setEncoding(enc);
// 如果編碼是UTF-8
if (enc == JsonEncoding.UTF8) {
return _createUTF8Generator(_decorate(out, ctxt), ctxt);
}
// 使用指定的編碼把OutputStream包裝為一個writer
Writer w = _createWriter(out, enc, ctxt);
return _createGenerator(_decorate(w, ctxt), ctxt);
}
這就解釋了,為何在詳解JsonGenerator的這篇文章中,我一直以UTF8JsonGenerator
作為例項進行講解,因為例子中指定的編碼就是UTF-8嘛。當然,即使你自己不顯示的指定編碼集,預設情況下Jackson也是使用UTF-8:
JsonFactory:
@Override
public JsonGenerator createGenerator(OutputStream out) throws IOException {
return createGenerator(out, JsonEncoding.UTF8);
}
示例:
@Test
public void test1() throws IOException {
JsonFactory jsonFactory = new JsonFactory();
JsonGenerator jsonGenerator1 = jsonFactory.createGenerator(System.out);
JsonGenerator jsonGenerator2 = jsonFactory.createGenerator(System.out, JsonEncoding.UTF8);
System.out.println(jsonGenerator1);
System.out.println(jsonGenerator2);
}
執行程式,輸出:
com.fasterxml.jackson.core.json.UTF8JsonGenerator@cb51256
com.fasterxml.jackson.core.json.UTF8JsonGenerator@59906517
建立JsonParser例項
JsonParser它負責從一個JSON字串中提取出值,因此它強調的是資料從哪來?如何解析?
如截圖所示,一共11個過載方法(其實最後一個不屬於過載)用於構建JsonParser例項,它的底層實現是根據不同的資料媒介,使用了不同的處理方式,最終生成UTF8StreamJsonParser/ReaderBasedJsonParser
。
你會發現這幾個過載方法均無需我們指定編碼集,那它是如何確定使用何種編碼去解碼形如byte[]陣列這種資料來源的呢?這得益於其內部的編碼自動發現機制實現,也就是ByteSourceJsonBootstrapper#detectEncoding()
這個方法。
示例:
@Test
public void test2() throws IOException {
JsonFactory jsonFactory = new JsonFactory();
JsonParser jsonParser1 = jsonFactory.createParser("{}");
// JsonParser jsonParser2 = jsonFactory.createParser(new FileReader("..."));
JsonParser jsonParser3 = jsonFactory.createNonBlockingByteArrayParser();
System.out.println(jsonParser1);
// System.out.println(jsonParser2);
System.out.println(jsonParser3);
}
執行程式,輸出:
com.fasterxml.jackson.core.json.ReaderBasedJsonParser@5f3a4b84
com.fasterxml.jackson.core.json.async.NonBlockingJsonParser@27f723
建立非阻塞例項
值得注意的是,上面截圖的11個方法中,最後一個並非過載。它建立的是一個非阻塞JSON解析器,也就是NonBlockingJsonParser
,並且它還沒有指定入參(資料來源)。
NonBlockingJsonParser
是Jackson在2.9版本新增的的一個解析器,目標是進一步提升效率、效能。但它也有侷限的地方:只能解析使用UTF-8編碼的內容,否則丟擲異常。
當然嘍,現在UTF-8編碼幾乎成為了標準編碼手段,問題不大。但是呢,我自己玩了玩NonBlockingJsonParser
,發現複雜度增加不少(玩半天才玩明白?),效果卻並不顯著,因此這裡瞭解一下便可,至少目前不建議深入探究。
小貼士:不管是Spring還是Redis的反序列化,使用的均是普通的解析器(阻塞IO)。因為JSON解析過程從來都不會是效能瓶頸(特殊場景除外)
JsonFactory的Feature
除了JsonGenerator和JsonParser有Feature來控制行為外,JsonFactory也有自己的Feature特徵,來控制自己的行為,可以理解為它對讀/寫均生效。
同樣的也是一個內部列舉類:
public enum Feature {
INTERN_FIELD_NAMES(true),
CANONICALIZE_FIELD_NAMES(true),
FAIL_ON_SYMBOL_HASH_OVERFLOW(true),
USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING(true)
}
小貼士:列舉值均為bool型別,括號內為預設值
每個列舉值都控制著JsonFactory不同的行為。
INTERN_FIELD_NAMES(true)
這是Jackson所謂的key快取:對JSON的欄位名是否呼叫String#intern
方法,放進字串常量池裡,以提高效率,預設是true。
小貼士:Jackson在呼叫String#intern之前使用
InternCache
(繼承自ConcurrentHashMap)擋了一層,以防止高併發條件下intern效果不顯著問題
intern()方法的作用這個老生常談的話題了,解釋為:當呼叫intern方法時,如果字串池已經包含一個等於此String物件的字串(內容相等),則返回池中的字串。否則,將此 String放進池子裡。下面寫個例子增加感受感受:
@Test
public void test2() {
String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str4 = str1 + str2;
String str5 = new String("ab");
System.out.println(str5.equals(str3)); // true
System.out.println(str5 == str3); // false
// str5.intern()去常量池裡找到了ab,所以直接返回常量池裡的地址值了,因此是true
System.out.println(str5.intern() == str3); // true
System.out.println(str5.intern() == str4); // false
}
可想而知,開啟這個小功能的意義還是蠻大的。因為同一個格式的JSON串被多次解析的可能性是非常之大的,想想你的Rest API介面,被呼叫多少次就會進行了多少次JSON解析(想想高併發場景)。這是一種用空間換時間的思想,所以小小功能,大大能量。
小貼士:如果你的應用對記憶體很敏感,你可以關閉此特徵。但,真的有這種應用嗎?有嗎?
值得注意的是:此特徵必須是CANONICALIZE_FIELD_NAMES
也為true(開啟)的情況下才有效,否則是無效的。
CANONICALIZE_FIELD_NAMES(true)
是否需要規範化屬性名。所謂的規範化處理,就是去字串池裡嘗試找一個字串出來,預設值為true。規範化藉助的是ByteQuadsCanonicalizer
去處理,簡而言之會根據Hash值來計算每個屬性名存放的位置~
小貼士:ByteQuadsCanonicalizer擁有一套優秀的Hash演算法來規範化屬性儲存,提高效率,抵禦攻擊(見下特徵)
此特徵開啟了,INTERN_FIELD_NAMES
特徵的開啟才有意義~
FAIL_ON_SYMBOL_HASH_OVERFLOW(true)
當ByteQuadsCanonicalizer
處理hash碰撞達到一個閾值時,是否快速失敗。
什麼時候能達到閾值?官方的說明是:若觸發了閾值,這基本可以確定是Dos(denial-of-service)攻擊,製造了非常多的相同Hash值的key,這在正常情況下幾乎是沒有發生的可能性的。
所以,開啟此特徵值,可以防止攻擊,在提高效能的同時也確保了安全。
USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING(true)
是否使用BufferRecycler、ThreadLocal、SoftReference
來有效的重用底層的輸入/輸出緩衝區。這個特性在後端服務(JavaEE)環境下是很有意義的,提效明顯。但是對於在Android環境下就不見得了~
總而言之言而總之,JsonFactory的這幾個特徵值都建議開啟,也就是維持預設即可。
定製讀/寫例項
讀寫行為的控制是通過各自的Feature來控制的,JsonFactory作為一個功能並非單一的工廠類,需要既能夠定製化讀JsonParser,也能定製化寫JsonGenerator。
為此,對應的API它都提供了三份(一份定製化自己的Feature):
public JsonFactory enable(JsonFactory.Feature f);
public JsonFactory enable(JsonParser.Feature f);
public JsonFactory enable(JsonGenerator.Feature f);
public JsonFactory disable(JsonFactory.Feature f);
public JsonFactory disable(JsonParser.Feature f);
public JsonFactory disable(JsonGenerator.Feature f);
// 合二為一的Configure方法
public JsonFactory configure(JsonFactory.Feature f, boolean state);
public JsonFactory configure(JsonParser.Feature f, boolean state);
public JsonFactory configure(JsonGenerator.Feature f, boolean state);
使用示例:
@Test
public void test3() throws IOException {
String jsonStr = "{\"age\":18, \"age\": 28 }";
JsonFactory factory = new JsonFactory();
factory.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);
try (JsonParser jsonParser = factory.createParser(jsonStr)) {
// 使用factory定製將不生效
// factory.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldname = jsonParser.getCurrentName();
if ("age".equals(fieldname)) {
jsonParser.nextToken();
System.out.println(jsonParser.getIntValue());
}
}
}
}
執行程式,丟擲異常。證明特徵開啟成功,符合預期。
com.fasterxml.jackson.core.JsonParseException: Duplicate field 'age'
at [Source: (String)"{"age":18, "age": 28 }"; line: 1, column: 17]
在使用JsonFactory定製化讀/寫例項的時需要特別注意:請務必確保在factory.createXXX()
之前配置好對應的Feature特徵,若在例項建立好之後再弄的話,對已經建立的例項無效。
小貼士:例項建立好後若你還想定製,可以使用例項自己的對應API操作
JsonFactoryBuilder
JsonFactory負責基類和實現類的雙重任務,是比較重的,分離得也不徹底。同時,現在都2020年了,對於這種構建類工廠如果還不用Builder模式就現在太out了,書寫起來也非常不便:
@Test
public void test4() throws IOException {
JsonFactory jsonFactory = new JsonFactory();
// jsonFactory自己的特徵
jsonFactory.enable(JsonFactory.Feature.INTERN_FIELD_NAMES);
jsonFactory.enable(JsonFactory.Feature.CANONICALIZE_FIELD_NAMES);
jsonFactory.enable(JsonFactory.Feature.USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING);
// JsonParser的特徵
jsonFactory.enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES);
jsonFactory.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER);
// JsonGenerator的特徵
jsonFactory.enable(JsonGenerator.Feature.QUOTE_FIELD_NAMES);
jsonFactory.enable(JsonGenerator.Feature.ESCAPE_NON_ASCII);
// 建立讀/寫例項
// jsonFactory.createParser(...);
// jsonFactory.createGenerator(...);
}
功能實現上沒毛病,但總顯得不夠優雅。同時上面也說了:定製化操作一定得在create建立動作之前執行,這全靠程式設計師自行控制。
Jackson在2.10版本新增了一個JsonFactoryBuilder
構件類,讓我們能夠基於builder模式優雅的構建出一個JsonFactory
例項。
小貼士:2.10版本是2019.09釋出的
比如上面例子的程式碼使用JsonFactoryBuilder
可重構為:
@Test
public void test4() throws IOException {
JsonFactory jsonFactory = new JsonFactoryBuilder()
// jsonFactory自己的特徵
.enable(INTERN_FIELD_NAMES)
.enable(CANONICALIZE_FIELD_NAMES)
.enable(USE_THREAD_LOCAL_FOR_BUFFER_RECYCLING)
// JsonParser的特徵
.enable(ALLOW_SINGLE_QUOTES, ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER)
// JsonGenerator的特徵
.enable(QUOTE_FIELD_NAMES, ESCAPE_NON_ASCII)
.build();
// 建立讀/寫例項
// jsonFactory.createParser(...);
// jsonFactory.createGenerator(...);
}
對比起來,使用Builder模式優雅太多了。
因為JsonFactory是執行緒安全的,因此一般情況下全域性我們只需要一個JsonFactory例項即可,推薦使用JsonFactoryBuilder
去完成你的構建。
小貼士:使用JsonFactoryBuilder確保你的Jackson版本至少是2.10版本哦~
SPI方式
從原始碼包裡發現,JsonFactory是支援Java SPI方式構建例項的。
檔案內容為:
com.fasterxml.jackson.core.JsonFactory
因此,我可以使用Java SPI的方式得到一個JsonFactory例項:
@Test
public void test5() {
ServiceLoader<JsonFactory> jsonFactories = ServiceLoader.load(JsonFactory.class);
System.out.println(jsonFactories.iterator().next());
}
執行程式,妥妥的輸出:
com.fasterxml.jackson.core.JsonFactory@4abdb505
這種方式,玩玩即可,在這裡沒實際用途。
總結
本文圍繞JsonFactory工廠為核心,講解了它是如何建立、定製讀/寫例項的。對於自己的例項的建立共有三種方式:
- 直接new例項
- 使用
JsonFactoryBuilder
構建(需要2.10或以上版本) - SPI方式建立例項
其中方式2是被推薦的,如果你的版本較低,就老老實實使用方式1唄。至於方式3嘛,玩玩就行,別當真。
至此,jackson-core的三大核心內容:JsonGenerator、JsonParser、JsonFactory
全部介紹完了,它們是jackson 其它所有模組 的基石,需要掌握紮實嘍。
下篇文章更有意思,會分析Jackson裡Feature機制的設計,使用補碼、掩碼來實現是高效的體現,同時設計上也非常優美,下文見。