5. JsonFactory工廠而已,還蠻有料,這是我沒想到的

少年易學老難成,一寸光陰不可輕。本文已被 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的(最)主要工廠類,用於 配置和構建JsonGeneratorJsonParser,這個工廠實例是線程安全的,因此可以重複使用。

作爲一個實例工廠,它最重要的職責當然是創建實例對象。本工廠職責並不單一,它負責讀、寫兩種實例的創建工作。

創建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工廠爲核心,講解了它是如何創建、定製讀/寫實例的。對於自己的實例的創建共有三種方式:

  1. 直接new實例
  2. 使用JsonFactoryBuilder構建(需要2.10或以上版本)
  3. SPI方式創建實例

其中方式2是被推薦的,如果你的版本較低,就老老實實使用方式1唄。至於方式3嘛,玩玩就行,別當真。

至此,jackson-core的三大核心內容:JsonGenerator、JsonParser、JsonFactory全部介紹完了,它們是jackson 其它所有模塊 的基石,需要掌握紮實嘍。

下篇文章更有意思,會分析Jackson裏Feature機制的設計,使用補碼、掩碼來實現是高效的體現,同時設計上也非常優美,下文見。

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