08_jackson-2

第七十三章 Jackson库 之二(解析和生成JSON) 续

在上一章我们介绍了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'
}

1 Tree Model解析方法

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

2 Map与Json映射方法

一种与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}

3 Jackson 对象映射方法

无论是使用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}]}

4 Jackson Annotation的使用方法

上述程序非常简洁,但是,Jackson库的使用对程序有着严格的要求。比如:Json文档中的键值对必须与Java对象保持高度一致。属性名称必须相同;值的类型必须匹配;Json字符串中的键值对不能比Java对象中定义的属性多或者少,必须完全比配。

但是,由于各种原因,有时程序做不到完全比配。因此,Jackson提供了一套Annotation,帮助开发人员"矫正"Jackson默认的转换过程。

Annotation的工作原理和细节可参考Java Annotation

4.1 属性名称 @JsonProperty

当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;
    }
    ...
}

4.2 忽略属性 @JsonIgnoreProperties 和 @JsonIgnore

如果在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;
    ...
}

4.3 包含属性 @JsonInclude

在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;
    ...
}

4.4 指定类型 @JsonSerialize和@JsonDeserialize

在序列化过程中,可以使用@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;
}

4.5 多态类型(Polymorphic Types)

在反序列化的过程中,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 {
}

5 结语

本章介绍了Jackson的Tree Model的序列化/反序列化方法。为了进一步简化代码复杂度,Jackson还提供了一套标注。开发人员只需定义数据的结构,和与Json中数据的对应关系,Jackson可以自动完成序列化和反序列化的过程,极大的降低了开发难度与工作量。本文中介绍的五种标注可以结合在一起使用,非常方便。

 

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.