一滴水,用顯微鏡看,也是一個大世界。本文已被 https://www.yourbatman.cn 收錄,裡面一併有Spring技術棧、MyBatis、JVM、中介軟體等小而美的專欄供以免費學習。關注公眾號【BAT的烏托邦】逐個擊破,深入掌握,拒絕淺嘗輒止。
✍前言
各位好,我是YourBatman。從本文起,終於要和Jackson的“高階”部分打交道了,也就是資料繫結jackson-databind
模組。通過接觸它的高階API,你會持續的發現,前面花那麼多篇幅講的core核心部分是價值連城的。畢竟村上春樹也告訴過我們:人生沒有無用的經歷嘛。
jackson-databind
包含用於Jackson資料處理器的通用 資料繫結功能和樹模型。它構建在Streaming API之上,並使用Jackson註解
進行配置。它就是Jackson提供的高層API,是開發者使用得最多的方式,因此重要程度可見一斑。
雖然Jackson最初的用例是JSON資料繫結,但現在它也可以用於其它資料格式,只要存在解析器和生成器實現即可。但需要注意的是:類的命名在很多地方仍舊使用了“JSON”這個詞(比如JsonGenerator),儘管它與JSON格式沒有實際的硬依賴關係。
小貼士:底層流式API使用的I/O進行輸入輸出,因此理論上是支援任何格式的
版本約定
- Jackson版本:
2.11.0
- Spring Framework版本:
5.2.6.RELEASE
- Spring Boot版本:
2.3.0.RELEASE
從本文開始,新增導包:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
Tips:jackson-databind
模組它強依賴於jackson-core和jackson-annotations,只需要匯入此包,另外兩個它自動會幫帶進來。
這裡需要說明幾句:我們知道core包中還有個
jackson-annotations
,難道不講了嗎?其實不是,是因為單獨講jackson-annotations
並無意義,畢竟註解還得靠資料繫結模組來解析,所以先搞定這個後再殺回去。
✍正文
據我瞭解,很多小夥伴對Jackson的瞭解起源於ObjectMapper
,止於ObjectMapper
。那行,作為接觸它的第一篇文章我們們就輕鬆點,以應用為主來整體的認識它。
功能介紹
ObjectMapper是jackson-databind模組最為重要的一個類,它完成了coder對資料繫結的幾乎所有功能。它是面向使用者的高層API,底層依賴於Streaming API來實現讀/寫。ObjectMapper主要提供的功能點如下:
- 它提供讀取和寫入JSON的功能(最重要的功能)
- 普通POJO的序列化/反序列化
- JSON樹模型的讀/寫
- 它可以被高度定製,以使用不同風格的JSON內容
- 使用Feature進行定製
- 使用可插拔
com.fasterxml.jackson.databind.Module
模組來擴充套件/豐富功能
- 它還支援更高階的物件概念:比如多型泛型、物件標識
- 它還充當了更為高階(更強大)的API:ObjectReader和ObjectWriter的工廠
ObjectReader
和ObjectWriter
底層亦是依賴於Streaming API實現讀寫
儘管絕大部分的讀/寫API都通過ObjectMapper暴露出去了,但有些功能函式還是隻放在了ObjectReader/ObjectWriter裡,比如對於讀/寫 長序列 的能力你只能通過ObjectReader#readValues(InputStream) / ObjectWriter#writeValues(OutputStream)
去處理,這是設計者有意為之,畢竟這種case很少很少,沒必要和常用的湊合在一起嘛。
資料繫結
資料繫結分為簡單資料繫結和完全資料繫結:
- 簡單資料繫結:比如繫結int型別、List、Map等…
@Test
public void test1() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
// 繫結簡單型別 和 Map型別
Integer age = objectMapper.readValue("1", int.class);
Map map = objectMapper.readValue("{\"name\": \"YourBatman\"}", Map.class);
System.out.println(age);
System.out.println(map);
}
執行程式,輸出:
1
{name=YourBatman}
- 完全資料繫結:繫結到任意的Java Bean物件…
準備一個POJO:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Person {
private String name;
private Integer age;
}
繫結資料到POJO:
@Test
public void test2() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
Person person = objectMapper.readValue("{\"name\": \"YourBatman\", \"age\": 18}", Person.class);
System.out.println(person);
}
執行程式,輸出:
Person(name=YourBatman, age=18)
ObjectMapper的使用
在應用及開發中,ObjectMapper絕對是最常使用的,也是你使用Jackson的入口,本文就列列它的那些使用場景。
小貼士:樹模型會單獨成文介紹,體現出它的重要性
寫(序列化)
提供writeValue()
系列方法用於寫資料(可寫任何型別),也就是我們常說的序列化。
- writeValue(File resultFile, Object value):寫到目標檔案裡
- writeValue(OutputStream out, Object value):寫到輸出流
- String writeValueAsString(Object value):寫成字串形式,此方法最為常用
- writeValueAsBytes(Object value):寫成位元組陣列
byte[]
@Test
public void test3() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
System.out.println("----------寫簡單型別----------");
System.out.println(objectMapper.writeValueAsString(18));
System.out.println(objectMapper.writeValueAsString("YourBatman"));
System.out.println("----------寫集合型別----------");
System.out.println(objectMapper.writeValueAsString(Arrays.asList(1, 2, 3)));
System.out.println(objectMapper.writeValueAsString(new HashMap<String, String>() {{
put("zhName", "A哥");
put("enName", "YourBatman");
}}));
System.out.println("----------寫POJO----------");
System.out.println(objectMapper.writeValueAsString(new Person("A哥", 18)));
}
執行程式,輸出:
----------寫簡單型別----------
18
"YourBatman"
----------寫集合型別----------
[1,2,3]
{"zhName":"A哥","enName":"YourBatman"}
----------寫POJO----------
{"name":"A哥","age":18}
讀(反序列化)
提供readValue()
系列方法用於讀資料(一般讀字串型別),也就是我們常說的反序列化。
readValue(String content, Class<T> valueType)
:讀為指定class型別的物件,此方法最常用readValue(String content, TypeReference<T> valueTypeRef)
:T表示泛型型別,如List<T>
這種型別,一般用於集合/Map的反序列化- readValue(String content, JavaType valueType):Jackson內建的JavaType型別,後再詳解(使用並不多)
@Test
public void test4() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
System.out.println("----------讀簡單型別----------");
System.out.println(objectMapper.readValue("18", Integer.class));
// 拋錯:JsonParseException 單獨的一個串,解析會拋錯
// System.out.println(objectMapper.readValue("YourBatman", String.class));
System.out.println("----------讀集合型別----------");
System.out.println(objectMapper.readValue("[1,2,3]", List.class));
System.out.println(objectMapper.readValue("{\"zhName\":\"A哥\",\"enName\":\"YourBatman\"}", Map.class));
System.out.println("----------讀POJO----------");
System.out.println(objectMapper.readValue("{\"name\":\"A哥\",\"age\":18}", Person.class));
}
執行程式,輸出:
----------讀簡單型別----------
18
----------讀集合型別----------
[1, 2, 3]
{zhName=A哥, enName=YourBatman}
----------讀POJO----------
Person(name=A哥, age=18)
不同於序列化,可以把“所有”寫成為一個字串。反序列化場景有它特殊的地方,比如例子中所示:不能反序列化一個“單純的”字串。
泛型擦除問題
從例舉出來的三個read讀方法中,就應該覺得事情還沒完,比如這個帶泛型的case:
@Test
public void test5() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
System.out.println("----------讀集合型別----------");
List<Long> list = objectMapper.readValue("[1,2,3]", List.class);
Long id = list.get(0);
System.out.println(id);
}
執行程式,拋錯:
----------讀集合型別----------
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Long
at cn.yourbatman.jackson.core.ObjectMapperDemo.test5(ObjectMapperDemo.java:100)
...
異常棧裡指出:Long id = list.get(0);
這一句出現了型別轉換異常,這便是問題原因所在:泛型擦除,參考圖示如下(明明泛型型別是Long,但實際裝的是Integer型別):
對這種問題,你可能會“動腦筋”思考:寫成[1L,2L,3L]
這樣行不行。思想很活躍,奈何現實依舊殘酷,執行拋錯:
com.fasterxml.jackson.core.JsonParseException: Unexpected character ('L' (code 76)): was expecting comma to separate Array entries
at [Source: (String)"[1L,2L,3L]"; line: 1, column: 4]
...
這是典型的泛型擦除問題。該問題只可能出現在讀(反序列化)上,不能出現在寫上。那麼這種問題怎麼破?
在解決此問題之前,我們得先對Java中的泛型擦除有所瞭解,至少知道如下兩點結論:
- Java 在編譯時會在位元組碼裡指令集之外的地方保留部分泛型資訊
- 泛型介面、類、方法定義上的所有泛型、成員變數宣告處的泛型都會被保留型別資訊,其它地方的泛型資訊都會被擦除
此問題在開發過程中非常高頻,有了此理論作為支撐,A哥提供兩種可以解決本問題的方案供以參考:
方案一:利用成員變數保留泛型
理論依據:成員變數的泛型型別不會被擦除
@Test
public void test6() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
System.out.println("----------讀集合型別----------");
Data data = objectMapper.readValue("{\"ids\" : [1,2,3]}", Data.class);
Long id = data.getIds().get(0);
System.out.println(id);
}
@lombok.Data
private static class Data {
private List<Long> ids;
}
執行程式,一切正常:
----------讀集合型別----------
1
方案二:使用官方推薦的TypeReference<T>
官方早早就為我們考慮好了這類泛型擦除的問題,所以它提供了TypeReference<T>
方便我們把泛型型別保留下來,使用起來是非常的方便的:
@Test
public void test7() throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
System.out.println("----------讀集合型別----------");
List<Long> ids = objectMapper.readValue("[1,2,3]", new TypeReference<List<Long>>() {
});
Long id = ids.get(0);
System.out.println(id);
}
執行程式,一切正常:
----------讀集合型別----------
1
本方案的理論依據是:泛型介面/類上的泛型型別不會被擦除。
對於泛型擦除情況,解決思路是hold住泛型型別,這樣反序列化的時候才不會抓瞎。但凡只要一抓瞎,Jackson就木有辦法只能採用通用/預設型別去裝載嘍。
加餐
自2.10
版本起,給ObjectMapper提供了一個子類:JsonMapper
,使得語義更加明確,專門用於處理JSON格式。
嚴格意義上講,ObjectMapper不侷限於處理JSON格式,比如後面會講到的它的另外一個子類
YAMLMapper
用於對Yaml格式的支援(需額外導包,後面見~)
另外,由於構建一個ObjectMapper例項屬於高頻動作,因此Jackson也順應潮流的提供了MapperBuilder
構建器(2.10版本起)。我們可以通過此構建起很容易的得到一個ObjectMapper(以JsonMapper為例)例項來使用:
@Test
public void test8() throws JsonProcessingException {
JsonMapper jsonMapper = JsonMapper.builder()
.configure(JsonReadFeature.ALLOW_SINGLE_QUOTES, true)
.build();
Person person = jsonMapper.readValue("{'name': 'YourBatman', 'age': 18}", Person.class);
System.out.println(person);
}
執行程式,正常輸出:
Person(name=YourBatman, age=18)
✍總結
本文內容很輕鬆,講述了ObjectMapper的日常使用,使用它進行讀/寫,完成日常功能。
對於寫來說比較簡單,一個writeValueAsString(obj)
方法走天下;但對於讀來說,除了使用readValue(String content, Class<T> valueType)
自動完成資料繫結外,需要特別注意泛型擦除問題:若反序列化成為一個集合型別(Collection or Map),泛型會被擦除,此時你應該使用readValue(String content, TypeReference<T> valueTypeRef)
方法代替。
小貼士:若你在工程中遇到
objectMapper.readValue(xxx, List.class)
這種程式碼,那肯定是有安全隱患的(但不一定報錯)