本系列文章经补充和完善,已修订整理成书《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的基本用法。
序列化一个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,惟一须要改变的是,替换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,一样使用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<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与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主要支持两种配置方法:
哪些状况须要配置呢?咱们看一些典型的场景:
针对这些场景,咱们分别来看下。
在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;
}
复制代码
这样,反序列化就没有问题了。
须要说明的是,对于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编程及计算机技术的本质。用心原创,保留全部版权。