少年易學老難成,一寸光陰不可輕。本文已被 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機制的設計,使用補碼、掩碼來實現是高效的體現,同時設計上也非常優美,下文見。