4. JSON字符串是如何被解析的?JsonParser瞭解一下

公司不是你家,領導不是你媽。本文已被 https://www.yourbatman.cn 收錄,裏面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的專欄供以免費學習。關注公衆號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。

前言

各位好,我是A哥(YourBatman)。上篇文章:3. 懂了這些,方敢在簡歷上說會用Jackson寫JSON 聊完,流式API的寫部分可以認爲你已完全掌握了,本文了解它讀的部分。

版本約定

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

小貼士:截止到本文,本系列前面所有示例都只僅僅導入jackson-core而已,後續若要新增jar包我會額外說明,否則相同

正文

什麼叫讀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也提供了高鈣片組合方法,讓你更加便捷的使用。

自動綁定

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

我們知道,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串是合法的,否則拋出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)去讀取然後解析的。

AUTO_CLOSE_SOURCE(true)

原理和JsonGenerator的AUTO_CLOSE_TARGET(true)一樣,不再解釋,詳見上篇文章對應部分。

支持非標準格式

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

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

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

但是,在很多使用場景(特別是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

ALLOW_YAML_COMMENTS(false)

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

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

ALLOW_UNQUOTED_FIELD_NAMES(false)

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

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

ALLOW_SINGLE_QUOTES(false)

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

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

{
    'age' : 18
}

ALLOW_UNQUOTED_CONTROL_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數字,逗號、分號這些等等。

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

ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(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

ALLOW_NUMERIC_LEADING_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

ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false)

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

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

ALLOW_NON_NUMERIC_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源碼裏的那幾個常量

ALLOW_MISSING_VALUES(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,有興趣的親可自行嘗試,鞏固下理解的效果。報錯原因文上已有說明~

ALLOW_TRAILING_COMMA(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標準之外,給出了兩個校驗相關的特徵。

STRICT_DUPLICATE_DETECTION(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) {
    	...
    }

其它

INCLUDE_SOURCE_IN_LOCATION(true)

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

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

總結

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

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

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

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

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