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

少年易學老難成,一寸光陰不可輕。本文已被 https://www.yourbatman.cn 收錄,裏面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的 專欄供以避免費學習。關注公衆號【 BAT的烏托邦】逐個擊破,深刻掌握,拒絕淺嘗輒止。

前言

各位好,我是YourBatman。前面用四篇文章介紹完了Jackson底層流式API的讀(JsonParser)、寫(JsonGenerator)操做,咱們清楚的知道,這哥倆都是abstract抽象類,使用時並無顯示的去new它們的(子類)實例,均經過一個工廠來搞定,這便就是本文的主角JsonFactoryjava

經過名稱就知道,這是工廠設計模式。Jackson它並不建議你直接new讀/寫實例,由於那過於麻煩。爲了對使用者屏蔽這些複雜的構造細節,因而就有了JsonFactory實例工廠的出現。git

可能有的人會說,一個對象工廠有什麼好了解的,很簡單嘛。非也非也,一件事情自己的複雜度並不會憑空消失,而是從一個地方轉移到另一個地方,這另一個地方指的就是JsonFactory。所以按照本系列的定位,瞭解它你繞不過去。程序員

版本約定

  • Jackson版本:2.11.0
  • Spring Framework版本:5.2.6.RELEASE
  • Spring Boot版本:2.3.0.RELEASE

正文

JsonFactory是Jackson的(最)主要工廠類,用於 配置和構建JsonGeneratorJsonParser,這個工廠實例是線程安全的,所以能夠重複使用。github

做爲一個實例工廠,它最重要的職責固然是建立實例對象。本工廠職責並不單一,它負責讀、寫兩種實例的建立工做。算法

建立JsonGenerator實例


JsonGenerator它負責向目的地寫數據,所以強調的是目的地在哪?如何寫?json

如截圖所示,一共有六個重載方法用於構建JsonGenerator實例,多個重載方法目的是對使用者友好,咱們能夠認爲最終效果是同樣的。好比,底層實現是:segmentfault

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機制的設計,使用補碼、掩碼來實現是高效的體現,同時設計上也很是優美,下文見。

相關推薦:

關注A哥

Author A哥(YourBatman)
我的站點 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
活躍平臺
公衆號 BAT的烏托邦(ID:BAT-utopia)
知識星球 BAT的烏托邦
每日文章推薦 每日文章推薦

BAT的烏托邦

相關文章
相關標籤/搜索