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

YourBatman發表於2020-08-20

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

相關推薦:

相關文章