上節,我們介紹了Java中的標准序列化機制,我們提到,它有一些重要的限制,最重要的是不能跨語言,實踐中經常使用一些替代方案,比如XML/JSON/MessagePack。
Java SDK中對這些格式的支持有限,有很多第三方的類庫,提供了更為方便的支持,Jackson是其中一種,它支持多種格式,包括XML/JSON/MessagePack等,本文就來介紹如果使用Jackson進行序列化。我們先來簡單了解下這些格式以及Jackson。
基本概念
XML/JSON都是文本格式,都容易閱讀和理解,格式細節我們就不介紹了,後面我們會看到一些例子,來演示其基本格式。
XML是最早流行的跨語言數據交換標准格式,如果不熟悉,可以查看http://www.w3school.com.cn/xml/快速了解。
JSON是一種更為簡單的格式,最近幾年來越來越流行,如果不熟悉,可以查看http://json.org/json-zh.html。
MessagePack是一種二進制形式的JSON,編碼更為精簡高效,官網地址是http://msgpack.org/,JSON有多種二進制形式,MessagePack只是其中一種。
Jackson的Wiki地址是http://wiki.fasterxml.com/JacksonHome,它起初主要是用來支持JSON格式的,但現在也支持很多其他格式,它的各種方式的使用方式是類似的。
要使用Jackson,需要下載相應的庫。
對於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("[email protected]"); 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" : "[email protected]" } }
XML格式的代碼也是類似的,替換ObjectMapper為XmlMapper即可,輸出為:
<ComplexStudent> <name>張三</name> <age>18</age> <scores> <語文>89.0</語文> <數學>83.0</數學> </scores> <contactInfo> <phone>18500308990</phone> <address>中關村</address> <email>[email protected]</email> </contactInfo> </ComplexStudent>
反序列化的代碼也不需要特殊處理,指定類型為ComplexStudent.class即可。
定制序列化
配置方法和場景
上面的例子中,我們沒有做任何定制,默認的配置就是可以的。但很多情況下,我們需要做一些配置,Jackson主要支持兩種配置方法:
哪些情況需要配置呢?我們看一些典型的場景:
針對這些場景,我們分別來看下。
忽略字段
在Java標准序列化中,如果字段標記為了transient,就會在序列化中被忽略,在Jackson中,可以使用以下兩個注解之一:
比如,上面的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<String>>對象,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 (http://x-stream.github.io/)。
小結
本節介紹了如何使用Jackson來實現JSON/XML/MessagePack序列化,使用方法是類似的,主要是創建的ObjectMapper對象不一樣,很多情況下,不需要做額外配置,但也有很多情況,需要做額外配置,配置方式主要是注解,我們介紹了Jackson中的很多典型注解,大部分注解適用於所有格式。
Jackson還支持很多其他格式,如YAML, AVRO, Protobuf, Smile等。Jackson中也還有很多其他配置和注解,用的相對較少,限於篇幅,我們就不介紹了。
從注解的用法,我們可以看出,它也是一種神奇的特性,它類似於注釋,但卻能實實在在改變程序的行為,它是怎麼做到的呢?我們暫且擱置這個問題,留待後續章節。
接下來,我們介紹一些常見文件類型的處理,包括屬性文件、CSV、Excel、HTML和壓縮文件。
(與其他章節一樣,本節所有代碼位於 https://github.com/swiftma/program-logic)
----------------
未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心原創,保留所有版權。