json格式轉換文本的方法:jsonobject解析json字元串特別慢

版本約定

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

什麼叫讀 JSON?就是把一個 JSON 字元串 解析為對象 or 樹模型嘛,因此也稱作解析 JSON 串。Jackson 底層流式 API 使用JsonParser來完成JSON 字元串的解析。

最簡使用 Demo

準備一個 POJO:

@Data
public class Person {
    private String name;
    private Integer age;
}

測試用例:把一個 JSON 字元串綁定(封裝)進一個 POJO 對象里

@Test
public void test1() throws IOException {
    String jsonStr = "{"name":"YourBatman","age":18}";
    Person person = new Person();

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {

        // 只要還沒結束"}",就一直讀
        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("name".equals(fieldname)) {
                jsonParser.nextToken();
                person.setName(jsonParser.getText());
            } else if ("age".equals(fieldname)) {
                jsonParser.nextToken();
                person.setAge(jsonParser.getIntValue());
            }
        }

        System.out.println(person);
    }
}

運行程序,輸出:

Person(name=YourBatman, age=18)

成功把一個 JSON 字元串的值解析到 Person 對象。你可能會疑問,怎麼這麼麻煩?那當然,這是底層流式 API,純手動檔嘛。你獲得了性能,可不要失去一些便捷性嘛。

小貼士:底層流式 API 一般面向「專業人士」,應用級開發使用高階 API ObjectMapper即可。當然,讀完本系列就能讓你完全具備「專業人士」的實力

JsonParser針對不同的 value 類型,提供了非常多的方法用於實際值的獲取。

直接值獲取:

// 獲取字元串類型
public abstract String getText() throws IOException;

// 數字 Number 類型值 標量值(支持的 Number 類型參照 NumberType 枚舉)
public abstract Number getNumberValue() throws IOException;
public enum NumberType {
    INT, LONG, BIG_INTEGER, FLOAT, DOUBLE, BIG_DECIMAL
};

public abstract int getIntValue() throws IOException;
public abstract long getLongValue() throws IOException;
...
public abstract byte[] getBinaryValue(Base64Variant bv) throws IOException;

這類方法可能會拋出異常:比如 value 值本不是數字但你調用了 getInValue()方法~

小貼士:如果 value 值是 null,像 getIntValue()、getBooleanValue()等這種直接獲取方法是會拋出異常的,但 getText()不會

帶默認值的值獲取,具有更好安全性:

public String getValueAsString() throws IOException {
    return getValueAsString(null);
}
public abstract String getValueAsString(String def) throws IOException;
...
public long getValueAsLong() throws IOException {
    return getValueAsLong(0);
}
public abstract long getValueAsLong(long def) throws IOException;
...

此類方法若碰到數據的轉換失敗時,不會拋出異常,把def作為默認值返回。

組合方法

同JsonGenerator一樣,JsonParser 也提供了高鈣片組合方法,讓你更加便捷的使用。

JSON 字元串是如何被解析的?JsonParser了解一下

自動綁定

聽起來像高級功能,是的,它必須依賴於ObjectCodec去實現,因為實際是全部委託給了它去完成的,也就是我們最為熟悉的 readXXX 系列方法:

JSON 字元串是如何被解析的?JsonParser了解一下

我們知道,ObjectMapper 就是一個 ObjectCodec,它屬於高級 API,本文顯然不會用到 ObjectMapper 它嘍,因此我們自己手敲一個實現來完成此功能。

自定義一個 ObjectCodec,Person 類專用:用於把 JSON 串自動綁定到實例屬性。

public class PersonObjectCodec extends ObjectCodec {
    ...
    @SneakyThrows
    @Override
    public <T> T readValue(JsonParser jsonParser, Class<T> valueType) throws IOException {
        Person person = (Person) valueType.newInstance();

        // 只要還沒結束"}",就一直讀
        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("name".equals(fieldname)) {
                jsonParser.nextToken();
                person.setName(jsonParser.getText());
            } else if ("age".equals(fieldname)) {
                jsonParser.nextToken();
                person.setAge(jsonParser.getIntValue());
            }
        }

        return (T) person;
    }
    ...
}

有了它,就可以實現我們的自動綁定了,書寫測試用例:

@Test
public void test3() throws IOException {
    String jsonStr = "{"name":"YourBatman","age":18, "pickName":null}";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        jsonParser.setCodec(new PersonObjectCodec());

        System.out.println(jsonParser.readValueAs(Person.class));
    }
}

運行程序,輸出:

Person(name=YourBatman, age=18)

這就是 ObjectMapper 自動綁定的核心原理所在,其它更為強大能力將在後續章節詳細展開。

JsonToken

在上例解析過程中,有一個非常重要的角色,那便是:JsonToken。它表示解析 JSON 內容時,用於返回結果的基本標記類型的枚舉。

public enum JsonToken {
    NOT_AVAILABLE(null, JsonTokenId.ID_NOT_AVAILABLE),

    START_OBJECT("{", JsonTokenId.ID_START_OBJECT),
    END_OBJECT("}", JsonTokenId.ID_END_OBJECT),
    START_ARRAY("[", JsonTokenId.ID_START_ARRAY),
    END_ARRAY("]", JsonTokenId.ID_END_ARRAY),

    // 屬性名(key)
    FIELD_NAME(null, JsonTokenId.ID_FIELD_NAME),

    // 值(value)
    VALUE_EMBEDDED_OBJECT(null, JsonTokenId.ID_EMBEDDED_OBJECT),
    VALUE_STRING(null, JsonTokenId.ID_STRING),
    VALUE_NUMBER_INT(null, JsonTokenId.ID_NUMBER_INT),
    VALUE_NUMBER_FLOAT(null, JsonTokenId.ID_NUMBER_FLOAT),
    VALUE_TRUE("true", JsonTokenId.ID_TRUE),
    VALUE_FALSE("false", JsonTokenId.ID_FALSE),
    VALUE_NULL("null", JsonTokenId.ID_NULL),
}

為了輔助理解,A 哥用一個例子,輸出各個部分一目了然:

@Test
public void test2() throws IOException {
    String jsonStr = "{"name":"YourBatman","age":18, "pickName":null}";
    System.out.println(jsonStr);
    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {

        while (true) {
            JsonToken token = jsonParser.nextToken();
            System.out.println(token + " -> 值為:" + jsonParser.getValueAsString());

            if (token == JsonToken.END_OBJECT) {
                break;
            }
        }
    }
}

運行程序,輸出:

{"name":"YourBatman","age":18, "pickName":null}
START_OBJECT -> 值為:null

FIELD_NAME -> 值為:name
VALUE_STRING -> 值為:YourBatman

FIELD_NAME -> 值為:age
VALUE_NUMBER_INT -> 值為:18

FIELD_NAME -> 值為:pickName
VALUE_NULL -> 值為:null

END_OBJECT -> 值為:null

從左至右解析,一一對應。各個部分用下面這張圖可以簡略表示出來:

JSON 字元串是如何被解析的?JsonParser了解一下

小貼士:解析時請確保你的的 JSON 串是合法的,否則拋出JsonParseException異常

JsonParser 的 Feature

它是 JsonParser 的一個內部枚舉類,共 15 個枚舉值:

public enum Feature {
    AUTO_CLOSE_SOURCE(true),

    ALLOW_COMMENTS(false),
    ALLOW_YAML_COMMENTS(false),
    ALLOW_UNQUOTED_FIELD_NAMES(false),
    ALLOW_SINGLE_QUOTES(false),
    @Deprecated
    ALLOW_UNQUOTED_CONTROL_CHARS(false),
    @Deprecated
    ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false),
    @Deprecated
    ALLOW_NUMERIC_LEADING_ZEROS(false),
    @Deprecated
    ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false),
    @Deprecated
    ALLOW_NON_NUMERIC_NUMBERS(false),
    @Deprecated
    ALLOW_MISSING_VALUES(false),
    @Deprecated
    ALLOW_TRAILING_COMMA(false),

    STRICT_DUPLICATE_DETECTION(false),
    IGNORE_UNDEFINED(false),
    INCLUDE_SOURCE_IN_LOCATION(true);
}

小貼士:枚舉值均為 bool 類型,括弧內為默認值

每個枚舉值都控制著JsonParser不同的行為。下面分類進行解釋

底層 I/O 流相關

自 2.10 版本後,使用StreamReadFeature#AUTO_CLOSE_SOURCE代替

Jackson 的流式 API 指的是 I/O 流,所以即使是,底層也是用 I/O 流(Reader)去讀取然後解析的。

AUTOCLOSESOURCE(true)

原理和 JsonGenerator 的AUTO_CLOSE_TARGET(true)一樣,不再解釋。

支持非標準格式

JSON 是有規範的,在它的規範里並沒有描述到對注釋的規定、對控制字元的處理等等,也就是說這些均屬於非標準行為。比如這個 JSON 串:

{
    "name" : "YourBarman", // 名字
    "age" : 18 // 年齡
}

你看,若你這麼寫 IDEA 都會飄紅提示你:

JSON 字元串是如何被解析的?JsonParser了解一下

但是,在很多使用場景(特別是 JavaScript)里,我們會在 JSON 串里寫注釋(屬性多時尤甚)那麼對於這種串,JsonParser 如何控制處理呢?它提供了對非標準 JSON 格式的兼容,通過下面這些特徵值來控制。

ALLOW_COMMENTS(false)

自 2.10 版本後,使用JsonReadFeature#ALLOW_JAVA_COMMENTS代替

是否允許/* */或者//這種類型的注釋出現。

@Test
public void test4() throws IOException {
    String jsonStr = "{n" +
            "t"name" : "YourBarman", // 名字n" +
            "t"age" : 18 // 年齡n" +
            "}";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // 開啟注釋支持
        // jsonParser.enable(JsonParser.Feature.ALLOW_COMMENTS);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("name".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getText());
            } else if ("age".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getIntValue());
            }
        }
    }
}

運行程序,拋出異常:

com.fasterxml.jackson.core.JsonParseException: Unexpected character ('/' (code 47)): maybe a (non-standard) comment? (not recognized as one since Feature 'ALLOW_COMMENTS' not enabled for parser)
 at [Source: (String)"{
    "name" : "YourBarman", // 名字
    "age" : 18 // 年齡
}"; line: 2, column: 26]

放開注釋的代碼,再次運行程序,正常 work

ALLOWYAMLCOMMENTS(false)

自 2.10 版本後,使用JsonReadFeature#ALLOW_YAML_COMMENTS代替

顧名思義,開啟後將支持 Yaml 格式的的注釋,也就是#形式的注釋語法。

ALLOWUNQUOTEDFIELD_NAMES(false)

自 2.10 版本後,使用JsonReadFeature#
ALLOW_UNQUOTED_FIELD_NAMES代替

是否允許屬性名不帶雙引號””,比較簡單,示例略。

ALLOWSINGLEQUOTES(false)

自 2.10 版本後,使用JsonReadFeature#ALLOW_SINGLE_QUOTES代替

是否允許屬性名支持單引號,也就是使用”包裹,形如這樣:

{
    'age' : 18
}

ALLOWUNQUOTEDCONTROL_CHARS(false)

自 2.10 版本後,使用JsonReadFeature#
ALLOW_UNESCAPED_CONTROL_CHARS代替

是否允許 JSON 字元串包含非引號控制字元(值小於 32 的 ASCII 字元,包含製表符和換行符)。 由於 JSON 規範要求對所有控制字元使用引號,這是一個非標準的特性,因此默認禁用。

那麼,哪些字元屬於控制字元呢?做個簡單科普:我們一般說的 ASCII 碼共 128 個字元(7bit),共分為兩大類

控制字元

控制字元,也叫不可列印字元。第0~32 號及第 127 號(共 34 個)是控制字元,例如常見的:LF(換行)CR(回車)、FF(換頁)、DEL(刪除)、BS(退格)等都屬於此類。

控制字元大部分已經廢棄不用了,它們的用途主要是用來操控已經處理過的文字,ASCII 值為 8、9、10 和 13 分別轉換為退格、製表、換行和回車字元。它們並沒有特定的圖形顯示,但會依不同的應用程序,而對文本顯示有不同的影響。

話外音:你看不見我,但我對你影響還蠻大

非控制字元

也叫可顯示字元,或者可列印字元,能從鍵盤直接輸入的字元。比如 0-9 數字,逗號、分號這些等等。

話外音:你肉眼能看到的字元就屬於非控制字元

ALLOWBACKSLASHESCAPINGANYCHARACTER(false)

自 2.10 版本後,使用JsonReadFeature#
ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER代替

是否允許反斜杠轉義任何字元。這句話不是非常好理解,看下面這個例子:

@Test
public void test4() throws IOException {
    String jsonStr = "{"name" : "YourB\'atman" }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("name".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getText());
            }
        }
    }
}

運行程序,報錯:

com.fasterxml.jackson.core.JsonParseException: Unrecognized character escape ''' (code 39)
 at [Source: (String)"{"name" : "YourB'atman" }"; line: 1, column: 19]
 ...

放開注釋掉的代碼,再次運行程序,一切正常,輸出:YourB’atman。

ALLOWNUMERICLEADING_ZEROS(false)

自 2.10 版本後,使用JsonReadFeature#
ALLOW_LEADING_ZEROS_FOR_NUMBERS代替

是否允許像00001這樣的「數字」出現(而不報錯)。看例子:

@Test
public void test5() throws IOException {
    String jsonStr = "{"age" : 00018 }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.enable(JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS);

        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: Invalid numeric value: Leading zeroes not allowed
 at [Source: (String)"{"age" : 00018 }"; line: 1, column: 11]
 ...

放開注掉的代碼,再次運行程序,一切正常。輸出18。

ALLOWLEADINGDECIMALPOINTFOR_NUMBERS(false)

自 2.10 版本後,使用JsonReadFeature#
ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS代替

是否允許小數點.打頭,也就是說.1這種小數格式是否合法。默認是不合法的,需要開啟此特徵才能支持,例子就略了,基本同上。

ALLOWNONNUMERIC_NUMBERS(false)

自 2.10 版本後,使用JsonReadFeature#ALLOW_NON_NUMERIC_NUMBERS代替

是否允許一些解析器識別一組「非數字」(如 NaN)作為合法的浮點數值。這個屬性和上篇文章的JsonGenerator#QUOTE_NON_NUMERIC_NUMBERS特徵值是遙相呼應的。

@Test
public void test5() throws IOException {
    String jsonStr = "{"percent" : NaN }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("percent".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getFloatValue());
            }
        }
    }
}

運行程序,拋錯:

com.fasterxml.jackson.core.JsonParseException: Non-standard token 'NaN': enable JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS to allow
 at [Source: (String)"{"percent" : NaN }"; line: 1, column: 17]

放開注釋掉的代碼,再次運行,一切正常。輸出:

NaN

小貼士:NaN 也可以表示一個 Float 對象,是的你沒聽錯,即使它不是數字但它也是 Float 類型。具體你可以看看 Float 源碼里的那幾個常量

ALLOWMISSINGVALUES(false)

自 2.10 版本後,使用JsonReadFeature#ALLOW_MISSING_VALUES代替

是否允許支持JSON 數組中「缺失」值。怎麼理解:數組中缺失了值表示兩個逗號之間,啥都沒有,形如這樣[value1, , value3]。

@Test
public void test6() throws IOException {
    String jsonStr = "{"names" : ["YourBatman",,"A 哥",,] }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("names".equals(fieldname)) {
                jsonParser.nextToken();

                while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
                    System.out.println(jsonParser.getText());
                }
            }
        }
    }
}

運行程序,拋錯:

YourBatman // 能輸出一個,畢竟第一個 part(JsonToken)是正常的嘛

com.fasterxml.jackson.core.JsonParseException: Unexpected character (',' (code 44)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (String)"{"names" : ["YourBatman",,"A 哥",,] }"; line: 1, column: 27]

放開注釋掉的代碼,再次運行,一切正常,結果為:

YourBatman
null
A 哥
null
null

請注意:此時數組的長度是 5 哦。

小貼士:此處用的 String 類型展示結果,是因為 null 可以作為 String 類型(jsonParser.getText()得到 null 是合法的)。但如果你使用的 int 類型(或者 bool 類型),那麼如果是 null 的話就報錯嘍Current token (VALUE_NULL) not of boolean type,有興趣的親可自行嘗試,鞏固下理解的效果。報錯原因文上已有說明~

ALLOWTRAILINGCOMMA(false)

自 2.10 版本後,使用JsonReadFeature#ALLOW_TRAILING_COMMA代替

是否允許最後一個多餘的逗號(一定是最後一個)。這個特徵是非常重要的,若開關打開,有如下效果:

  • [true,true,]等價於[true, true]
  • {“a”: true,}等價於{“a”: true}

當這個特徵和上面的ALLOW_MISSING_VALUES特徵同時使用時,本特徵優先順序更高。也就是說:會先去除掉最後一個逗號後,再進行數組長度的計算。

舉個例子:當然這兩個特徵開關都打開時,[true,true,]等價於[true, true]好理解;並且呢,[true,true,,]是等價於[true, true, null]的哦,可千萬別忽略最後的這個 null

@Test
public void test7() throws IOException {
    String jsonStr = "{"results" : [true,true,,] }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES);
        // jsonParser.enable(JsonParser.Feature.ALLOW_TRAILING_COMMA);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("results".equals(fieldname)) {
                jsonParser.nextToken();

                while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
                    System.out.println(jsonParser.getBooleanValue());
                }
            }
        }
    }
}

運行程序,輸出:

YourBatman
null
A 哥
null
null

這完全就是上例的效果嘛。現在我放開注釋掉的代碼,再次運行,結果為:

YourBatman
null
A 哥
null

請注意對比前後的結果差異,並自己能能自己合理解釋

校驗相關

Jackson 在 JSON 標準之外,給出了兩個校驗相關的特徵。

STRICTDUPLICATEDETECTION(false)

自 2.10 版本後,使用StreamReadFeature#
STRICT_DUPLICATE_DETECTION代替

是否允許 JSON 串有兩個相同的屬性 key,默認是允許的

@Test
public void test8() throws IOException {
    String jsonStr = "{"age":18, "age": 28 }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.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());
            }
        }
    }
}

運行程序,正常輸出:

18
28

若放開注釋代碼,再次運行,則拋錯:

18 // 第一個數字還是能正常輸出的喲

com.fasterxml.jackson.core.JsonParseException: Duplicate field 'age'
 at [Source: (String)"{"age":18, "age": 28 }"; line: 1, column: 17]

IGNORE_UNDEFINED(false)

自 2.10 版本後,使用StreamReadFeature#IGNORE_UNDEFINED代替

是否忽略沒有定義的屬性 key。和JsonGenerator.Feature#IGNORE_UNKNOWN的這個特徵一樣,它作用於預先定義了格式的數據類型,如Avro、protobuf等等,JSON 是不需要預先定義的哦~

同樣的,你可以通過這個 API 預先設置格式:

JsonParser:

    public void setSchema(FormatSchema schema) {
        ...
    }

其它

INCLUDESOURCEIN_LOCATION(true)

自 2.10 版本後,使用StreamReadFeature#
INCLUDE_SOURCE_IN_LOCATION代替

是否構建JsonLocation對象來表示每個 part 的來源,你可以通過JsonParser#getCurrentLocation()來訪問。作用不大,就此略過。

總結

本文介紹了底層流式 API JsonParser 讀 JSON 的方式,它不僅僅能夠處理標準 JSON,也能通過 Feature 特徵值來控制,開啟對一些非標準但又比較常用的 JSON 串的支持,這不正式一個優秀框架/庫應有的態度麽:兼容性

結合上篇文章對寫 JSON 時JsonGenerator的描述,能夠總結出兩點原則:

  • 寫:100%遵循規範
  • 讀:最大程度兼容並包

寫代表你的輸出,遵循規範的輸出能確保第三方在用你輸出的數據時不至於對你破口大罵,所以這是你應該做好的本分。讀代表你的輸入,能夠處理規範的格式是你的職責,但我若還能額外的處理一些非標準格式(一般為常用的),那絕對是閃耀點,也就是你給的情分。本分是你應該做的,而情分就是你的加分項

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-tw/n/226124.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-09 14:48
下一篇 2024-12-09 14:48

相關推薦

發表回復

登錄後才能評論