計算機程序的思惟邏輯 (63) - 實用序列化: JSON/XML/MessagePack

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連接 html

上節,咱們介紹了Java中的標準序列化機制,咱們提到,它有一些重要的限制,最重要的是不能跨語言,實踐中常用一些替代方案,好比XML/JSON/MessagePack。java

Java SDK中對這些格式的支持有限,有不少第三方的類庫,提供了更爲方便的支持,Jackson是其中一種,它支持多種格式,包括XML/JSON/MessagePack等,本文就來介紹若是使用Jackson進行序列化。咱們先來簡單瞭解下這些格式以及Jackson。git

基本概念

XML/JSON都是文本格式,都容易閱讀和理解,格式細節咱們就不介紹了,後面咱們會看到一些例子,來演示其基本格式。github

XML是最先流行的跨語言數據交換標準格式,若是不熟悉,能夠查看www.w3school.com.cn/xml/快速瞭解。編程

JSON是一種更爲簡單的格式,最近幾年來愈來愈流行,若是不熟悉,能夠查看json.org/json-zh.htm…json

MessagePack是一種二進制形式的JSON,編碼更爲精簡高效,官網地址是msgpack.org/,JSON有多種二進制形式,MessagePack只是其中一種。swift

Jackson的Wiki地址是wiki.fasterxml.com/JacksonHome,它起初主要是用來支持JSON格式的,但如今也支持不少其餘格式,它的各類方式的使用方式是相似的。數組

要使用Jackson,須要下載相應的庫。安全

對於JSON/XML,本文使用2.8.5版本,對於MessagePack,本文使用0.8.11版本。若是使用Maven管理項目,可引入下面文件中的依賴:bash

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編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索