本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結
上節,我們介紹了Java中的標準序列化機制,我們提到,它有一些重要的限制,最重要的是不能跨語言,實踐中經常使用一些替代方案,比如XML/JSON/MessagePack。
Java SDK中對這些格式的支援有限,有很多第三方的類庫,提供了更為方便的支援,Jackson是其中一種,它支援多種格式,包括XML/JSON/MessagePack等,本文就來介紹如果使用Jackson進行序列化。我們先來簡單瞭解下這些格式以及Jackson。
基本概念
XML/JSON都是文字格式,都容易閱讀和理解,格式細節我們就不介紹了,後面我們會看到一些例子,來演示其基本格式。
XML是最早流行的跨語言資料交換標準格式,如果不熟悉,可以檢視www.w3school.com.cn/xml/快速瞭解。
JSON是一種更為簡單的格式,最近幾年來越來越流行,如果不熟悉,可以檢視json.org/json-zh.htm…。
MessagePack是一種二進位制形式的JSON,編碼更為精簡高效,官網地址是msgpack.org/,JSON有多種二進位制形式,MessagePack只是其中一種。
Jackson的Wiki地址是wiki.fasterxml.com/JacksonHome,它起初主要是用來支援JSON格式的,但現在也支援很多其他格式,它的各種方式的使用方式是類似的。
要使用Jackson,需要下載相應的庫。
- JSON格式參考:github.com/FasterXML/j…
- XML格式參考:github.com/FasterXML/j…
- MessagePack格式參考:github.com/msgpack/msg…
對於JSON/XML,本文使用2.8.5版本,對於MessagePack,本文使用0.8.11版本。如果使用Maven管理專案,可引入下面檔案中的依賴:
https://github.com/swiftma/program-logic/blob/master/jackson_libs/dependencies.xml
複製程式碼
如果非Maven,可從下面地址下載所有的依賴庫:
https://github.com/swiftma/program-logic/tree/master/jackson_libs
複製程式碼
配置好了依賴庫後,下面我們就來介紹如何使用。
基本用法
我們以在57節介紹的Student類來演示Jackson的基本用法。
JSON
序列化一個Student物件的基本程式碼為:
Student student = new Student("張三", 18, 80.9d);
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String str = mapper.writeValueAsString(student);
System.out.println(str);
複製程式碼
Jackson序列化的主要類是ObjectMapper,它是一個執行緒安全的類,可以初始化並配置一次,被多個執行緒共享,SerializationFeature.INDENT_OUTPUT的目的是格式化輸出,以便於閱讀,ObjectMapper的writeValueAsString方法就可以將物件序列化為字串,輸出為:
{
"name" : "張三",
"age" : 18,
"score" : 80.9
}
複製程式碼
ObjectMapper還有其他方法,可以輸出位元組陣列,寫出到檔案、OutputStream、Writer等,方法宣告如下:
public byte[] writeValueAsBytes(Object value)
public void writeValue(OutputStream out, Object value)
public void writeValue(Writer w, Object value)
public void writeValue(File resultFile, Object value)
複製程式碼
比如,輸出到檔案”student.json”,程式碼為:
mapper.writeValue(new File("student.json"), student);
複製程式碼
ObjectMapper怎麼知道要儲存哪些欄位呢?與Java標準序列化機制一樣,它也使用反射,預設情況下,它會儲存所有宣告為public的欄位,或者有public getter方法的欄位。
反序列化的程式碼如下所示:
ObjectMapper mapper = new ObjectMapper();
Student s = mapper.readValue(new File("student.json"), Student.class);
System.out.println(s.toString());
複製程式碼
使用readValue方法反序列化,有兩個引數,一個是輸入源,這裡是檔案student.json,另一個是反序列化後的物件型別,這裡是Student.class,輸出為:
Student [name=張三, age=18, score=80.9]
複製程式碼
說明反序列化的結果是正確的,除了接受檔案,還可以是位元組陣列、字串、InputStream、Reader等,如下所示:
public <T> T readValue(InputStream src, Class<T> valueType)
public <T> T readValue(Reader src, Class<T> valueType)
public <T> T readValue(String content, Class<T> valueType)
public <T> T readValue(byte[] src, Class<T> valueType)
複製程式碼
在反序列化時,預設情況下,Jackson假定物件型別有一個無參的構造方法,它會先呼叫該構造方法建立物件,然後再解析輸入源進行反序列化。
XML
使用類似的程式碼,格式可以為XML,唯一需要改變的是,替換ObjectMapper為XmlMapper,XmlMapper是ObjectMapepr的子類,序列化程式碼為:
Student student = new Student("張三", 18, 80.9d);
ObjectMapper mapper = new XmlMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String str = mapper.writeValueAsString(student);
mapper.writeValue(new File("student.xml"), student);
System.out.println(str);
複製程式碼
輸出為:
<Student>
<name>張三</name>
<age>18</age>
<score>80.9</score>
</Student>
複製程式碼
反序列化程式碼為:
ObjectMapper mapper = new XmlMapper();
Student s = mapper.readValue(new File("student.xml"), Student.class);
System.out.println(s.toString());
複製程式碼
MessagePack
類似的程式碼,格式可以為MessagePack,同樣使用ObjectMapper類,但傳遞一個MessagePackFactory物件,另外,MessagePack是二進位制格式,不能寫出為String,可以寫出為檔案、OutpuStream或位元組陣列,序列化程式碼為:
Student student = new Student("張三", 18, 80.9d);
ObjectMapper mapper = new ObjectMapper(new MessagePackFactory());
byte[] bytes = mapper.writeValueAsBytes(student);
mapper.writeValue(new File("student.bson"), student);
複製程式碼
序列後的位元組如下圖所示:
反序列化程式碼為:
ObjectMapper mapper = new ObjectMapper(new MessagePackFactory());
Student s = mapper.readValue(new File("student.bson"), Student.class);
System.out.println(s.toString());
複製程式碼
容器物件
對於容器物件,Jackson也是可以自動處理的,但用法稍有不同,我們來看下List和Map。
List
序列化一個學生列表的程式碼為:
List<Student> students = Arrays.asList(new Student[] {
new Student("張三", 18, 80.9d), new Student("李四", 17, 67.5d) });
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String str = mapper.writeValueAsString(students);
mapper.writeValue(new File("students.json"), students);
System.out.println(str);
複製程式碼
這與序列化一個學生物件的程式碼是類似的,輸出為:
[ {
"name" : "張三",
"age" : 18,
"score" : 80.9
}, {
"name" : "李四",
"age" : 17,
"score" : 67.5
} ]
複製程式碼
反序列化程式碼不同,要新建一個TypeReference匿名內部類物件來指定型別,程式碼如下所示:
ObjectMapper mapper = new ObjectMapper();
List<Student> list = mapper.readValue(new File("students.json"),
new TypeReference<List<Student>>() {});
System.out.println(list.toString());
複製程式碼
XML/MessagePack的程式碼是類似的,我們就不贅述了。
Map
Map與List類似,序列化不需要特殊處理,但反序列化需要通過TypeReference指定型別,我們看一個XML的例子。
序列化一個學生Map的程式碼為:
Map<String, Student> map = new HashMap<String, Student>();
map.put("zhangsan", new Student("張三", 18, 80.9d));
map.put("lisi", new Student("李四", 17, 67.5d));
ObjectMapper mapper = new XmlMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String str = mapper.writeValueAsString(map);
mapper.writeValue(new File("students_map.xml"), map);
System.out.println(str);
複製程式碼
輸出為:
<HashMap>
<lisi>
<name>李四</name>
<age>17</age>
<score>67.5</score>
</lisi>
<zhangsan>
<name>張三</name>
<age>18</age>
<score>80.9</score>
</zhangsan>
</HashMap>
複製程式碼
反序列化的程式碼為:
ObjectMapper mapper = new XmlMapper();
Map<String, Student> map = mapper.readValue(new File("students_map.xml"),
new TypeReference<Map<String, Student>>() {});
System.out.println(map.toString());
複製程式碼
複雜物件
對於複雜一些的物件,Jackson也是可以自動處理的,我們讓Student類稍微複雜一些,改為如下定義:
public class ComplexStudent {
String name;
int age;
Map<String, Double> scores;
ContactInfo contactInfo;
//... 構造方法,和getter/setter方法
}
複製程式碼
分數改為一個Map,鍵為課程,ContactInfo表示聯絡資訊,是一個單獨的類,定義如下:
public class ContactInfo {
String phone;
String address;
String email;
// ...構造方法,和getter/setter方法
}
複製程式碼
構建一個ComplexStudent物件,程式碼為:
ComplexStudent student = new ComplexStudent("張三", 18);
Map<String, Double> scoreMap = new HashMap<>();
scoreMap.put("語文", 89d);
scoreMap.put("數學", 83d);
student.setScores(scoreMap);
ContactInfo contactInfo = new ContactInfo();
contactInfo.setPhone("18500308990");
contactInfo.setEmail("zhangsan@sina.com");
contactInfo.setAddress("中關村");
student.setContactInfo(contactInfo);
複製程式碼
我們看JSON序列化,程式碼沒有特殊的,如下所示:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.writeValue(System.out, student);
複製程式碼
輸出為:
{
"name" : "張三",
"age" : 18,
"scores" : {
"語文" : 89.0,
"數學" : 83.0
},
"contactInfo" : {
"phone" : "18500308990",
"address" : "中關村",
"email" : "zhangsan@sina.com"
}
}
複製程式碼
XML格式的程式碼也是類似的,替換ObjectMapper為XmlMapper即可,輸出為:
<ComplexStudent>
<name>張三</name>
<age>18</age>
<scores>
<語文>89.0</語文>
<數學>83.0</數學>
</scores>
<contactInfo>
<phone>18500308990</phone>
<address>中關村</address>
<email>zhangsan@sina.com</email>
</contactInfo>
</ComplexStudent>
複製程式碼
反序列化的程式碼也不需要特殊處理,指定型別為ComplexStudent.class即可。
定製序列化
配置方法和場景
上面的例子中,我們沒有做任何定製,預設的配置就是可以的。但很多情況下,我們需要做一些配置,Jackson主要支援兩種配置方法:
- 一種是註解,後續文章會詳細介紹註解,這裡主要是介紹Jackson一些註解的用法
- 另外一種是配置ObjectMapper物件,ObjectMapper支援對序列化和反序列化過程做一些配置,前面使用的SerializationFeature.INDENT_OUTPUT是其中一種
哪些情況需要配置呢?我們看一些典型的場景:
- 如何達到類似標準序列化中transient關鍵字的效果,忽略一些欄位?
- 在標準序列化中,可以自動處理引用同一個物件、迴圈引用的情況,反序列化時,可以自動忽略不認識的欄位,可以自動處理繼承多型,但Jackson都不能自動處理,這些情況都需要進行配置
- 標準序列化的結果是二進位制、不可讀的,但XML/JSON格式是可讀的,有時我們希望控制這個顯示的格式
- 預設情況下,反序列時,Jackson要求類有一個無參構造方法,但有時類沒有無參構造方法,Jackson支援配置其他構造方法
針對這些場景,我們分別來看下。
忽略欄位
在Java標準序列化中,如果欄位標記為了transient,就會在序列化中被忽略,在Jackson中,可以使用以下兩個註解之一:
- @JsonIgnore:用於欄位, getter或setter方法,任一地方的效果都一樣
- @JsonIgnoreProperties:用於類宣告,可指定忽略一個或多個欄位
比如,上面的Student類,忽略分數字段,可以為:
@JsonIgnore
double score;
複製程式碼
也可以修飾getter方法,如:
@JsonIgnore
public double getScore() {
return score;
}
複製程式碼
也可以修飾Student類,如:
@JsonIgnoreProperties("score")
public class Student {
複製程式碼
加了以上任一標記後,序列化後的結果中將不再包含score欄位,在反序列化時,即使輸入源中包含score欄位的內容,也不會給score欄位賦值。
引用同一個物件
我們看個簡單的例子,有兩個類Common和A,A中有兩個Common物件,為便於演示,我們將所有屬性定義為了public,它們的類定義如下:
static class Common {
public String name;
}
static class A {
public Common first;
public Common second;
}
複製程式碼
有一個A物件,如下所示:
Common c = new Common();
c.name= "common";
A a = new A();
a.first = a.second = c;
複製程式碼
a物件的first和second都指向都一個c物件,不加額外配置,序列化a的程式碼為:
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String str = mapper.writeValueAsString(a);
System.out.println(str);
複製程式碼
輸出為:
{
"first" : {
"name" : "abc"
},
"second" : {
"name" : "abc"
}
}
複製程式碼
在反序列化後,first和second將指向不同的物件,如下所示:
A a2 = mapper.readValue(str, A.class);
if(a2.first == a2.second){
System.out.println("reference same object");
}else{
System.out.println("reference different objects");
}
複製程式碼
輸出為:
reference different objects
複製程式碼
那怎樣才能保持這種對同一個物件的引用關係呢?可以使用註解@JsonIdentityInfo,對Common類做註解,如下所示:
@JsonIdentityInfo(
generator = ObjectIdGenerators.IntSequenceGenerator.class,
property="id")
static class Common {
public String name;
}
複製程式碼
@JsonIdentityInfo中指定了兩個屬性,property=”id”表示在序列化輸出中新增一個屬性”id”以表示物件的唯一標示,generator表示物件唯一ID的產生方法,這裡是使用整數順序數產生器IntSequenceGenerator。
加了這個標記後,序列化輸出會變為:
{
"first" : {
"id" : 1,
"name" : "common"
},
"second" : 1
}
複製程式碼
注意,”first”中加了一個屬性”id”,而”second”的值只是1,表示引用第一個物件,這個格式反序列化後,first和second會指向同一個物件。
迴圈引用
我們看個迴圈引用的例子,有兩個類Parent和Child,它們相互引用,為便於演示,我們將所有屬性定義為了public,類定義如下:
static class Parent {
public String name;
public Child child;
}
static class Child {
public String name;
public Parent parent;
}
複製程式碼
有一個物件,如下所示:
Parent parent = new Parent();
parent.name = "老馬";
Child child = new Child();
child.name = "小馬";
parent.child = child;
child.parent = parent;
複製程式碼
如果序列化parent這個物件,Jackson會進入無限迴圈,最終丟擲異常,解決這個問題,可以分別標記Parent類中的child和Child類中的parent欄位,將其中一個標記為主引用,而另一個標記為反向引用,主引用使用@JsonManagedReference,反向引用使用@JsonBackReference,如下所示:
static class Parent {
public String name;
@JsonManagedReference
public Child child;
}
static class Child {
public String name;
@JsonBackReference
public Parent parent;
}
複製程式碼
加了這個註解後,序列化就沒有問題了,我們看XML格式的序列化程式碼:
ObjectMapper mapper = new XmlMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String str = mapper.writeValueAsString(parent);
System.out.println(str);
複製程式碼
輸出為:
<Parent>
<name>老馬</name>
<child>
<name>小馬</name>
</child>
</Parent>
複製程式碼
在輸出中,反向引用沒有出現。不過,在反序列化時,Jackson會自動設定Child物件中的parent欄位的值,比如:
Parent parent2 = mapper.readValue(str, Parent.class);
System.out.println(parent2.child.parent.name);
複製程式碼
輸出為:
老馬
複製程式碼
說明標記為反向引用的欄位的值也被正確設定了。
反序列化時忽略未知欄位
在Java標準序列化中,反序列化時,對於未知欄位,會自動忽略,但在Jackson中,預設情況下,會拋異常。比如,還是以Student類為例,如果student.json檔案的內容為:
{
"name" : "張三",
"age" : 18,
"score": 333,
"other": "其他資訊"
}
複製程式碼
其中,other屬性是Student類沒有的,如果使用標準的反序列化程式碼:
ObjectMapper mapper = new ObjectMapper();
Student s = mapper.readValue(new File("student.json"), Student.class);
複製程式碼
Jackson會丟擲異常:
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "other" ...
複製程式碼
怎樣才能忽略不認識的欄位呢?可以配置ObjectMapper,如下所示:
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
Student s = mapper.readValue(new File("student.json"), Student.class);
複製程式碼
這樣就沒問題了,這個屬性是配置在整個ObjectMapper上的,如果只是希望配置Student類,可以在Student類上使用如下註解:
@JsonIgnoreProperties(ignoreUnknown=true)
public class Student {
複製程式碼
繼承和多型
Jackson也不能自動處理多型的情況,我們看個例子,有四個類,定義如下,我們忽略了構造方法和getter/setter方法:
static class Shape {
}
static class Circle extends Shape {
private int r;
}
static class Square extends Shape {
private int l;
}
static class ShapeManager {
private List<Shape> shapes;
}
複製程式碼
ShapeManager中的Shape列表,其中的物件可能是Circle,也可能是Square,比如,有一個ShapeManager物件,如下所示:
ShapeManager sm = new ShapeManager();
List<Shape> shapes = new ArrayList<Shape>();
shapes.add(new Circle(10));
shapes.add(new Square(5));
sm.setShapes(shapes);
複製程式碼
使用JSON格式序列化,輸出為:
{
"shapes" : [ {
"r" : 10
}, {
"l" : 5
} ]
}
複製程式碼
這個輸出看上去是沒有問題的,但由於輸出中沒有型別資訊,反序列化時,Jackson不知道具體的Shape型別是什麼,就會丟擲異常。
解決方法是在輸出中包含型別資訊,在基類Shape前使用如下註解:
@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Circle.class, name = "circle"),
@JsonSubTypes.Type(value = Square.class, name = "square") })
static class Shape {
}
複製程式碼
這些註解看上去比較多,含義是指在輸出中增加屬性”type”,表示物件的實際型別,對Circle類,使用”circle”表示其型別,而對於Square類,使用”square”,加了註解後,序列化輸出會變為:
{
"shapes" : [ {
"type" : "circle",
"r" : 10
}, {
"type" : "square",
"l" : 5
} ]
}
複製程式碼
這樣,反序列化時就可以正確解析了。
修改欄位名稱
對於XML/JSON格式,有時,我們希望修改輸出的名稱,比如對Student類,我們希望輸出的欄位名變為對應的中文,可以使用@JsonProperty進行註解,如下所示:
public class Student {
@JsonProperty("名稱")
String name;
@JsonProperty("年齡")
int age;
@JsonProperty("分數")
double score;
//...
}
複製程式碼
加了這個註解後,輸出的JSON格式會變為:
{
"名稱" : "張三",
"年齡" : 18,
"分數" : 80.9
}
複製程式碼
對於XML格式,一個常用的修改是根元素的名稱,預設情況下,它是物件的類名,比如對Student物件,它是”Student”,如果希望修改呢?比如改為小寫”student”,可以使用@JsonRootName修飾整個類,如下所示:
@JsonRootName("student")
public class Student {
複製程式碼
格式化日期
預設情況下,日期的序列化格式為一個長整數,比如:
static class MyDate {
public Date date = new Date();
}
複製程式碼
序列化程式碼:
MyDate date = new MyDate();
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(System.out, date);
複製程式碼
輸出如下所示:
{"date":1482758152509}
複製程式碼
這個格式是不可讀的,怎樣才能可讀呢?使用@JsonFormat註解,如下所示:
static class MyDate {
@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone="GMT+8")
public Date date = new Date();
}
複製程式碼
加註解後,輸出會變為如下所示:
{"date":"2016-12-26 21:26:18"}
複製程式碼
配置構造方法
前面的Student類,如果沒有定義預設構造方法,只有如下構造方法:
public Student(String name, int age, double score) {
this.name = name;
this.age = age;
this.score = score;
}
複製程式碼
則反序列化時會拋異常,提示找不到合適的構造方法,可以使用@JsonCreator和@JsonProperty標記該構造方法,如下所示:
@JsonCreator
public Student(
@JsonProperty("name") String name,
@JsonProperty("age") int age,
@JsonProperty("score") double score) {
this.name = name;
this.age = age;
this.score = score;
}
複製程式碼
這樣,反序列化就沒有問題了。
Jackson對XML支援的侷限性
需要說明的是,對於XML格式,Jackson的支援不是太全面,比如說,對於一個Map<String, List>物件,Jackson可以序列化,但不能反序列化,如下所示:
Map<String, List<String>> map = new HashMap<>();
map.put("hello", Arrays.asList(new String[]{"老馬","小馬"}));
ObjectMapper mapper = new XmlMapper();
String str = mapper.writeValueAsString(map);
System.out.println(str);
Map<String, List<String>> map2 = mapper.readValue(str,
new TypeReference<Map<String, List<String>>>() {});
System.out.println(map2);
複製程式碼
在反序列化時,程式碼會丟擲異常,如果mapper是一個ObjectMapper物件,反序列化就沒有問題。如果Jackson不能滿足需求,可以考慮其他庫,如XStream (x-stream.github.io/)。
小結
本節介紹瞭如何使用Jackson來實現JSON/XML/MessagePack序列化,使用方法是類似的,主要是建立的ObjectMapper物件不一樣,很多情況下,不需要做額外配置,但也有很多情況,需要做額外配置,配置方式主要是註解,我們介紹了Jackson中的很多典型註解,大部分註解適用於所有格式。
Jackson還支援很多其他格式,如YAML, AVRO, Protobuf, Smile等。Jackson中也還有很多其他配置和註解,用的相對較少,限於篇幅,我們就不介紹了。
從註解的用法,我們可以看出,它也是一種神奇的特性,它類似於註釋,但卻能實實在在改變程式的行為,它是怎麼做到的呢?我們暫且擱置這個問題,留待後續章節。
接下來,我們介紹一些常見檔案型別的處理,包括屬性檔案、CSV、Excel、HTML和壓縮檔案。
(與其他章節一樣,本節所有程式碼位於 github.com/swiftma/pro…)
未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。