在上一章我们介绍了Jackson库的内部结构和Streaming API的使用方法。我们将在本章中介绍Jackson Data Binding和Annotation模块的使用方法。
如果使用Maven管理项目,需要将如下依赖关系加入项目的pom.xml文件中。
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.11.1</version>
</dependency>
如果使用Gradle管理项目,需要将如下依赖加入项目的build.gradle文件中。
repositories {
mavenCentral()
}
dependencies {
implementation 'com.fasterxml.jackson.core:jackson-core:2.11.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1'
implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.1'
}
Jackson Data Binding模块通过ObjectMapper对象提供了两种解析和生成JSON的方法。我们将在本小节介绍Tree Model方法;把POJO(Plain Old Java Object)到JSON的映射方法放在下一节介绍。
Jackson的Tree Model方法的概念非常简单。因为JSON将所有数据存放在一个树形结构之中,所以,Jackson的Tree Model方法是把JSON字符串转换成一颗树,然后通过遍历树中的每一个节点来获取JSON字符串中的内容。
如下面的例子所示,程序首先创建一个ObjectMapper对象,然后调用readTree()方法将JSON字符串转换为一棵树,函数返回的是该树的根节点。在获得根节点之后,可以通过从根节点到对应节点的路径来获取对应的节点,并获取该节点的值。若该节点的值是字符串类型,则调用textValue()方法;若该节点是整数类型,则调用intValue()方法。以此类推。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
public class TreeModelExample {
public static void main(String[] args) {
String aJson = "{ \"name\" : \"Adam\", \"age\" : 18 }";
try {
ObjectMapper mapper = new ObjectMapper();
// 解析JSON字符串
JsonNode root = mapper.readTree(aJson);
JsonNode nameNode = root.path("name");
System.out.println("name=" + nameNode.textValue());
JsonNode ageNode = root.path("age");
System.out.println("age=" + ageNode.intValue());
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
}
name=Adam
age=18
一种与Tree Model方法十分相似的方法是建立Map对象和Json的映射。在这种方法下,多级的树状结构变为多级的Map对象。
如下面的代码所示,readValue()方法能将一个JSON字符串转换为一个Map对象。该Map对象包含JSON字符串中的属性。下面的程序在获得Map对象之后打印了所有的键值对。
调用writeValueAsString()方法可以将一个Map对象序列化为一个JSON字符串。下面的程序在最后打印了生成的JSON字符串的内容。
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.Map;
import java.util.HashMap;
import java.io.IOException;
public class TreeModelExample {
public static void main(String[] args) {
String aJson = "{ \"name\" : \"Adam\", \"age\" : 18 }";
try {
ObjectMapper mapper = new ObjectMapper();
// 解析JSON字符串
Map<Object, Object> values = mapper.readValue(aJson, Map.class);
for (var aValue: values.entrySet()) {
System.out.println(aValue.getKey() + "=" + aValue.getValue());
}
Map<Object, Object> newObject = new HashMap<Object, Object>();
newObject.put("name", "David");
newObject.put("age", Integer.valueOf(18));
// 生成JSON字符串
System.out.println(mapper.writeValueAsString(newObject));
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
}
程序的输出为:
name=Adam
age=18
{"name":"David","age":18}
无论是使用Stream API方法还是使用Tree Model方法,开发人员都需要编写代码来处理JSON字符串中的树形结构。有时,这些代码逻辑复杂,且容易出错。为了解决这个问题,Jackson提供了第四种解析生成JSON字符串的方法。这种方法通过定义POJO(Plain Old Java Object)来建立与JSON树形结构中各节点之间的映射。有了这些映射,Jackson库可以帮助开发人员自动完成Json字符串的解析和生成。
如下面的代码所示,程序首先定义了Person类;该类包含两个属性name和age,和对应的getter和setter方法。我们称这样的类为POJO(Plain Old Java Object)。在main函数中,程序首先创建一个ObjectMapper对象,然后调用readValue()函数将程序中的JSON字符串转换为一个Person类的对象。注意,此时Person类中属性的名字需与JSON字符串中的数据的名称相同,并且Person类中的属性可被外界访问(即要么是public成员,要么该类声明了相应的getter和setter方法)。
在JSON生成的过程中,程序首先重新设置了Person对象的name和age属性,然后调用writeValueAsString()函数将Person对象包含的数据转换成一个JSON字符串。
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Person {
private String name = null;
private Integer age = null;
public Person() {
}
public Person(String name, Integer age) {
this.name = name;
this.age = age;
}
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public static void main(String[] args) {
try {
String aJson = "{ \"name\" : \"Adam\", \"age\" : 18 }";
ObjectMapper mapper = new ObjectMapper();
// 解析JSON字符串
Person person = mapper.readValue(aJson, Person.class);
System.out.println("name=" + person.getName());
System.out.println("age=" + person.getAge());
// 生成JSON字符串
person.setName("David");
person.setAge(22);
System.out.println(mapper.writeValueAsString(person));
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
}
从上述的例子中可以看出,开发人员只需定义出数据的结构(POJO类),JSON的解析和生成只需要非常少量的代码,而且这些代码易于维护、不易出错。程序的输出为:
name=Adam
age=18
{"name":"David","age":22}
当在Java对象中使用集合(Collection)时,Jackson库可以将其与树状的JSON结构一一对应。例如,我们定义一个Course类,其中包含一个Teacher对象和一组Student对象,Jackson能按照对象之间的关系解析和生成JSON字符串。
import java.util.Arrays;
import java.util.List;
import java.io.IOException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Teacher extends Person {
private Integer level = null;
public Teacher() {
}
public Teacher(String name, Integer age, Integer level) {
super(name, age);
this.level = level;
}
public void setLevel(Integer level) {
this.level = level;
}
public Integer getLevel() {
return level;
}
}
public class Student extends Person {
public Student() {
}
public Student(String name, Integer age) {
super(name, age);
}
}
public class Course {
private Teacher teacher = null;
private List<Student> students = null;
// Getters and Setters
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public Teacher getTeacher() {
return this.teacher;
}
public void setStudents(List<Student> students) {
this.students = students;
}
public List<Student> getStudents() {
return this.students;
}
public static void main(String[] args) {
try {
String aJson = "{" +
" \"teacher\" : { " +
" \"name\": \"Linda\", "+
" \"age\": 38" +
" }," +
" \"students\": [ " +
" { " +
" \"name\": \"Jim\", " +
" \"age\": 20 " +
" } " +
" ] " +
"}";
// 解析JSON字符串
ObjectMapper mapper = new ObjectMapper();
Course course = mapper.readValue(aJson, Course.class);
System.out.println("Teacher's Name: " + course.getTeacher().getName());
System.out.println("Number of Students is " + course.getStudents().size());
// 生成JSON字符串
course = new Course();
course.setTeacher(new Teacher("Amy", 35, 1));
course.setStudents(Arrays.asList(new Student("Bob", 21)));
System.out.println(mapper.writeValueAsString(course));
} catch (IOException ex) {
System.out.println(ex.getMessage());
}
}
}
上述程序的输出如下。因为这个JSON字符串是以“紧凑”格式输出的。读者可将其拷贝至JSON格式化页面,格式化之后阅读其结构和内容。
Teacher's Name: Linda
Number of Students is 1
{"teacher":{"name":"Amy","age":35,"level":1},"students":[{"name":"Bob","age":21}]}
上述程序非常简洁,但是,Jackson库的使用对程序有着严格的要求。比如:Json文档中的键值对必须与Java对象保持高度一致。属性名称必须相同;值的类型必须匹配;Json字符串中的键值对不能比Java对象中定义的属性多或者少,必须完全比配。
但是,由于各种原因,有时程序做不到完全比配。因此,Jackson提供了一套Annotation,帮助开发人员"矫正"Jackson默认的转换过程。
Annotation的工作原理和细节可参考Java Annotation。
当JSON文档中的属性名称与类中成员变量的名称不一致时,开发人员可以使用@JsonProperty指定一个专门用于JSON转换的属性名称。
例如,如果下面的程序使用ObjectMapper来转换Person对象的话,Json字符串将使用"fullname"作为名称的属性名,而不会再使用"name"作为属性名。
import com.fasterxml.jackson.annotation.JsonProperty;
public class Person {
@JsonProperty("fullname")
private String name = null;
// Getters and Setters
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
...
}
如果在JSON生成过程中,不想将一些成员变量转换为JSON的字符串的话,可以使用@JsonIgnoreProperties标注或者@JsonIgnore标注。在转换过程中,Jackson库会忽略使用了这两个标注的属性。
例如,在如下的程序中,Jackson将忽略BasketballPlayer类和FootballPlayer类中的属性height和weight。值得注意的是,@JsonIgnoreProperties标注用于类上,而@JsonIgnore标注用于成员变量上。
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties({ "height", "weight" })
public class BasketballPlayer {
private Integer height;
private Integer weight;
...
}
public class FootballPlayer {
@JsonIgnore private Integer height;
@JsonIgnore private Integer weight;
...
}
有时Json字符串还包含了额外的键值对。这时,可以在对应的类上使用@JsonIgnoreProperties(ignoreUnknown=true),Jackson会忽略那些在Json字符串中出现,但是未在类中定义的属性。
@JsonIgnoreProperties(ignoreUnknown=true)
public class BasketballPlayer {
private Integer height;
private Integer weight;
...
}
在Json序列化过程中,有时只需要序列化值为非Null的属性。在这种情况下,可以使用@JsonInclude标注来标识可能为Null的属性。@JsonInclude可用于类上,也可用于属性上。
例如,在下面的代码中,当序列化BasketballPlayer时,如果height或者weight的值为Null,那么Jackson不会将其写入Json字符串中。当序列化FootballPlayer时,如果height的值为Null,Jackson不会将其写入JSON字符串中。成员变量weight不受影响。
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(Include.NON_NULL)
public class BasketballPlayer {
private Integer height;
private Integer weight;
...
}
public class FootballPlayer {
@JsonInclude(Include.NON_NULL)
private Integer height;
private Integer weight;
...
}
在序列化过程中,可以使用@JsonSerialize和@JsonDeserialize标注来改变序列化/反序列化对象的类型。例如,在序列化Class对象过程中,成员变量teacher可能指向的是一个Teacher对象。然而,在一些应用场景下,我们只希望序列化Person类中定义的成员变量,而不希望把Teacher类中的成员变量也写入Json字符串中。此时,我们可以使用@JsonSerialize标注来指定用于序列化的对象类型。换句话说,Teacher中的成员变量level不会被写入Json字符串。
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
public class Class {
@JsonSerialize(as=Person.class)
private Person teacher = null;
}
类似的,在反序列化过程中,@JsonDeserialize标注可被用于生成对象的类型。例如,如果我们非常确信Json字符串中包含的是Teacher的信息,那么,我们可以使用@JsonDeserialize(as=Teacher.class),让Jackson反序列化过程中生成一个Teacher对象。
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
public class Class {
@JsonDeserialize(as=Teacher.class)
private Person teacher = null;
}
在反序列化的过程中,Jackson标注的一个非常强大的功能是可以根据属性的值,创建不同的对象。例如下面的代码示例所示,Person是一个父类;Teacher类和Student类继承自Person类。在Person类中,属性role表示了这个对象的角色,即Teacher或者Student。因此,在反序列化过程中,我们可以使用@JsonTypeInfo标注来指定需要关注的属性名称role,使用@JsonSubTypes标注来指定值与对象类型的映射。例如,在该示例中,如果role属性的值是teacher,则该对象是Teacher对象;如果role属性的值是student,则该对象是Student对象。
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonSubTypes;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "role", visible = true)
@JsonSubTypes({
@JsonSubTypes.Type(value = Teacher.class, name = "teacher"),
@JsonSubTypes.Type(value = Student.class, name = "student")
})
public abstract class Person {
private String role = null;
...
}
public abstract class Teacher extends Person {
}
public abstract class Student extends Person {
}
本章介绍了Jackson的Tree Model的序列化/反序列化方法。为了进一步简化代码复杂度,Jackson还提供了一套标注。开发人员只需定义数据的结构,和与Json中数据的对应关系,Jackson可以自动完成序列化和反序列化的过程,极大的降低了开发难度与工作量。本文中介绍的五种标注可以结合在一起使用,非常方便。
注册用户登陆后可留言