公司不是你家,上司不是你媽。本文已被 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)
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)
JsonReadFeature#ALLOW_YAML_COMMENTS
顧名思義,開啟後将支援Yaml格式的的注釋,也就是
#
形式的注釋文法。
ALLOW_UNQUOTED_FIELD_NAMES(false)
JsonReadFeature#ALLOW_UNQUOTED_FIELD_NAMES
是否允許屬性名不帶雙引号"",比較簡單,示例略。
ALLOW_SINGLE_QUOTES(false)
JsonReadFeature#ALLOW_SINGLE_QUOTES
是否允許屬性名支援單引号,也就是使用
''
包裹,形如這樣:
{
'age' : 18
}
ALLOW_UNQUOTED_CONTROL_CHARS(false)
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)
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)
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)
JsonReadFeature#ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS
是否允許小數點
.
打頭,也就是說
.1
這種小數格式是否合法。預設是不合法的,需要開啟此特征才能支援,例子就略了,基本同上。
ALLOW_NON_NUMERIC_NUMBERS(false)
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)
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類型(得到null是合法的)。但如果你使用的int類型(或者bool類型),那麼如果是null的話就報錯喽
jsonParser.getText()
,有興趣的親可自行嘗試,鞏固下了解的效果。報錯原因文上已有說明~
Current token (VALUE_NULL) not of boolean type
ALLOW_TRAILING_COMMA(false)
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)
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)
StreamReadFeature#IGNORE_UNDEFINED
是否忽略沒有定義的屬性key。和
JsonGenerator.Feature#IGNORE_UNKNOWN
的這個特征一樣,它作用于預先定義了格式的資料類型,如
Avro、protobuf
等等,JSON是不需要預先定義的哦~
同樣的,你可以通過這個API預先設定格式:
JsonParser:
public void setSchema(FormatSchema schema) {
...
}
其它
INCLUDE_SOURCE_IN_LOCATION(true)
StreamReadFeature#INCLUDE_SOURCE_IN_LOCATION
是否建構
JsonLocation
對象來表示每個part的來源,你可以通過
JsonParser#getCurrentLocation()
來通路。作用不大,就此略過。
總結
本文介紹了底層流式API JsonParser讀JSON的方式,它不僅僅能夠處理标準JSON,也能通過Feature特征值來控制,開啟對一些非标準但又比較常用的JSON串的支援,這不正式一個優秀架構/庫應有的态度麽:相容性。
結合上篇文章對寫JSON時
JsonGenerator
的描述,能夠總結出兩點原則:
- 寫:100%遵循規範
- 讀:最大程度相容并包
寫代表你的輸出,遵循規範的輸出能確定第三方在用你輸出的資料時不至于對你破口大罵,是以這是你應該做好的本分。讀代表你的輸入,能夠處理規範的格式是你的職責,但我若還能額外的處理一些非标準格式(一般為常用的),那絕對是閃耀點,也就是你給的情分。本分是你應該做的,而情分就是你的加分項。