1. 概述

在本文中,我们将了解 Jackson 中的类层次结构。

两个典型的用例是包含子类型元数据和忽略从超类继承的属性。我们将描述这两种情况以及需要对亚型进行特殊处理的几种情况。

2. 包含子类型信息

序列化和反序列化数据对象时添加类型信息有两种方法,即全局默认类型和每类注释。

2.1.全局默认输入

以下三个 Java 类将用于说明类型元数据的全局包含。

车辆 超类:

public abstract class Vehicle {
    private String make;
    private String model;

    protected Vehicle(String make, String model) {
        this.make = make;
        this.model = model;
    }

    // no-arg constructor, getters and setters
}

汽车 子类:

public class Car extends Vehicle {
    private int seatingCapacity;
    private double topSpeed;

    public Car(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }

    // no-arg constructor, getters and setters
}

卡车子 类:

public class Truck extends Vehicle {
    private double payloadCapacity;

    public Truck(String make, String model, double payloadCapacity) {
        super(make, model);
        this.payloadCapacity = payloadCapacity;
    }

    // no-arg constructor, getters and setters
}

全局默认类型允许通过在 ObjectMapper 对象上启用类型信息来声明一次。然后,该类型元数据将应用于所有指定的类型。因此,使用该方法添加类型元数据非常方便,尤其是在涉及大量类型时。缺点是它使用完全限定的 Java 类型名称作为类型标识符,因此不适合与非 Java 系统交互,并且仅适用于几种预定义的类型。

上面显示的 Vehicle 结构用于填充 Fleet 类的实例:

public class Fleet {
    private List<Vehicle> vehicles;
    
    // getters and setters
}

要嵌入类型元数据,我们需要在 ObjectMapper 对象上启用类型功能,该功能稍后将用于数据对象的序列化和反序列化:

ObjectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)

applicability 参数确定需要类型信息的类型, includeAs 参数是类型元数据包含的机制。此外,还提供了 enableDefaultTyping 方法的另外两个变体:

  • ObjectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping applicability) :允许调用者指定 applicability ,同时使用 WRAPPER_ARRAY 作为 includeAs 的默认值
  • ObjectMapper.enableDefaultTyping(): 使用 OBJECT_AND_NON_CONCRETE 作为 适用性的 默认值,使用 WRAPPER_ARRAY 作为 includeAs 的默认值

让我们看看它是如何工作的。首先,我们需要创建一个 ObjectMapper 对象并在其上启用默认类型:

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();

下一步是实例化并填充本小节开头介绍的数据结构。稍后将在后续小节中重复使用执行此操作的代码。为了方便和复用,我们将其命名为 车辆实例化块

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = new Truck("Isuzu", "NQR", 7500.0);

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(car);
vehicles.add(truck);

Fleet serializedFleet = new Fleet();
serializedFleet.setVehicles(vehicles);

然后这些填充的对象将被序列化:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

生成的 JSON 字符串:

{
    "vehicles": 
    [
        "java.util.ArrayList",
        [
            [
                "org.baeldung.jackson.inheritance.Car",
                {
                    "make": "Mercedes-Benz",
                    "model": "S500",
                    "seatingCapacity": 5,
                    "topSpeed": 250.0
                }
            ],

            [
                "org.baeldung.jackson.inheritance.Truck",
                {
                    "make": "Isuzu",
                    "model": "NQR",
                    "payloadCapacity": 7500.0
                }
            ]
        ]
    ]
}

在反序列化期间,从 JSON 字符串中恢复对象并保留类型数据:

Fleet deserializedFleet = mapper.readValue(jsonDataString, Fleet.class);

重新创建的对象将是与序列化之前相同的具体子类型:

assertThat(deserializedFleet.getVehicles().get(0), instanceOf(Car.class));
assertThat(deserializedFleet.getVehicles().get(1), instanceOf(Truck.class));

2.2.每类注释

每类注释是一种包含类型信息的强大方法,对于需要大量自定义的复杂用例非常有用。然而,这只能以复杂化为代价来实现。如果以两种方式配置类型信息,则每类注释将覆盖全局默认类型。

要使用此方法,应使用 @JsonTypeInfo 和其他几个相关注释对超类型进行注释。本小节将使用与上一示例中的 Vehicle 结构类似的数据模型来说明每类注释。唯一的变化是在 Vehicle 抽象类上添加了注解,如下所示:

@JsonTypeInfo(
  use = JsonTypeInfo.Id.NAME, 
  include = JsonTypeInfo.As.PROPERTY, 
  property = "type")
@JsonSubTypes({ 
  @Type(value = Car.class, name = "car"), 
  @Type(value = Truck.class, name = "truck") 
})
public abstract class Vehicle {
    // fields, constructors, getters and setters
}

数据对象是使用上一小节中介绍的 车辆实例化块 创建的,然后序列化:

String jsonDataString = mapper.writeValueAsString(serializedFleet);

序列化产生以下 JSON 结构:

{
    "vehicles": 
    [
        {
            "type": "car",
            "make": "Mercedes-Benz",
            "model": "S500",
            "seatingCapacity": 5,
            "topSpeed": 250.0
        },

        {
            "type": "truck",
            "make": "Isuzu",
            "model": "NQR",
            "payloadCapacity": 7500.0
        }
    ]
}

该字符串用于重新创建数据对象:

Fleet deserializedFleet = mapper.readValue(jsonDataString, Fleet.class);

最后,验证整个进度:

assertThat(deserializedFleet.getVehicles().get(0), instanceOf(Car.class));
assertThat(deserializedFleet.getVehicles().get(1), instanceOf(Truck.class));

3. 忽略超类型的属性

有时,在序列化或反序列化期间需要忽略从超类继承的某些属性。这可以通过以下三种方法之一来实现:注释、混合和注释内省。

3.1.注释

有两种常用的 Jackson 注解来忽略属性,它们是 @JsonIgnore@JsonIgnoreProperties 。前者直接应用于类型成员,告诉 Jackson 在序列化或反序列化时忽略相应的属性。后者可在任何级别(包括类型和类型成员)使用,以列出应忽略的属性。

@JsonIgnoreProperties 比另一个更强大,因为它允许我们忽略从我们无法控制的超类型继承的属性,例如外部库中的类型。此外,此注释允许我们一次忽略许多属性,这在某些情况下可以导致更易于理解的代码。

以下类结构用于演示注释的用法:

public abstract class Vehicle {
    private String make;
    private String model;

    protected Vehicle(String make, String model) {
        this.make = make;
        this.model = model;
    }

    // no-arg constructor, getters and setters
}

@JsonIgnoreProperties({ "model", "seatingCapacity" })
public abstract class Car extends Vehicle {
    private int seatingCapacity;
    
    @JsonIgnore
    private double topSpeed;

    protected Car(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
        this.topSpeed = topSpeed;
    }

    // no-arg constructor, getters and setters
}

public class Sedan extends Car {
    public Sedan(String make, String model, int seatingCapacity, double topSpeed) {
        super(make, model, seatingCapacity, topSpeed);
    }

    // no-arg constructor
}

public class Crossover extends Car {
    private double towingCapacity;

    public Crossover(String make, String model, int seatingCapacity, 
      double topSpeed, double towingCapacity) {
        super(make, model, seatingCapacity, topSpeed);
        this.towingCapacity = towingCapacity;
    }

    // no-arg constructor, getters and setters
}

如您所见, @JsonIgnore 告诉 Jackson 忽略 Car.topSpeed 属性,而 @JsonIgnoreProperties 则忽略 Vehicle.modelCar.seatingCapacity 属性。

两个注释的行为均通过以下测试进行验证。首先,我们需要实例化 ObjectMapper 和数据类,然后使用该 ObjectMapper 实例来序列化数据对象:

ObjectMapper mapper = new ObjectMapper();

Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0);
Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0);

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString 包含以下 JSON 数组:

[
    {
        "make": "Mercedes-Benz"
    },
    {
        "make": "BMW",
        "towingCapacity": 6000.0
    }
]

最后,我们将证明生成的 JSON 字符串中是否存在各种属性名称:

assertThat(jsonDataString, containsString("make"));
assertThat(jsonDataString, not(containsString("model")));
assertThat(jsonDataString, not(containsString("seatingCapacity")));
assertThat(jsonDataString, not(containsString("topSpeed")));
assertThat(jsonDataString, containsString("towingCapacity"));

3.2.混入

混合允许我们应用行为(例如在序列化和反序列化时忽略属性),而无需直接将注释应用于类。这在处理第三方类时尤其有用,因为我们无法直接修改代码。

本小节重用了上一节中介绍的类继承链,只是删除了 Car 类上的 @JsonIgnore@JsonIgnoreProperties 注解:

public abstract class Car extends Vehicle {
    private int seatingCapacity;
    private double topSpeed;
        
    // fields, constructors, getters and setters
}

为了演示 mix-ins 的操作,我们将忽略 Vehicle.makeCar.topSpeed 属性,然后使用测试来确保一切按预期工作。

第一步是声明一个混合类型:

private abstract class CarMixIn {
    @JsonIgnore
    public String make;
    @JsonIgnore
    public String topSpeed;
}

接下来,混合通过 ObjectMapper 对象绑定到数据类:

ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(Car.class, CarMixIn.class);

之后,我们实例化数据对象并将它们序列化为字符串:

Sedan sedan = new Sedan("Mercedes-Benz", "S500", 5, 250.0);
Crossover crossover = new Crossover("BMW", "X6", 5, 250.0, 6000.0);

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

jsonDataString 现在包含以下 JSON:

[
    {
        "model": "S500",
        "seatingCapacity": 5
    },
    {
        "model": "X6",
        "seatingCapacity": 5,
        "towingCapacity": 6000.0
    }
]

最后我们来验证一下结果:

assertThat(jsonDataString, not(containsString("make")));
assertThat(jsonDataString, containsString("model"));
assertThat(jsonDataString, containsString("seatingCapacity"));
assertThat(jsonDataString, not(containsString("topSpeed")));
assertThat(jsonDataString, containsString("towingCapacity"));

3.3.注解自省

注解内省是忽略超类型属性的最强大方法,因为它允许使用 AnnotationIntrospector.hasIgnoreMarker API 进行详细的自定义。

本小节使用与前一小节相同的类层次结构。在此用例中,我们将要求 Jackson 忽略 Vehicle.modelCrossover.towingCapacity 以及 Car 类中声明的所有属性。让我们从扩展 JacksonAnnotationIntrospector 接口的类的声明开始:

class IgnoranceIntrospector extends JacksonAnnotationIntrospector {
    public boolean hasIgnoreMarker(AnnotatedMember m) {
        return m.getDeclaringClass() == Vehicle.class && m.getName() == "model" 
          || m.getDeclaringClass() == Car.class 
          || m.getName() == "towingCapacity" 
          || super.hasIgnoreMarker(m);
    }
}

内省器将忽略与该方法中定义的条件集匹配的任何属性(也就是说,它将把它们视为通过其他方法之一标记为忽略的属性)。

下一步是使用 ObjectMapper 对象注册 IgnoranceIntrospector 类的实例:

ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new IgnoranceIntrospector());

现在我们以与 3.2 节相同的方式创建和序列化数据对象。新生成的字符串的内容是:

[
    {
        "make": "Mercedes-Benz"
    },
    {
        "make": "BMW"
    }
]

最后,我们将验证内省器是否按预期工作:

assertThat(jsonDataString, containsString("make"));
assertThat(jsonDataString, not(containsString("model")));
assertThat(jsonDataString, not(containsString("seatingCapacity")));
assertThat(jsonDataString, not(containsString("topSpeed")));
assertThat(jsonDataString, not(containsString("towingCapacity")));

4. 子类型处理场景

本节将讨论与子类处理相关的两个有趣的场景。

4.1.子类型之间的转换

Jackson 允许将对象转换为原始类型以外的类型。事实上,这种转换可能发生在任何兼容类型之间,但当在同一接口或类的两个子类型之间使用时,它最有帮助,以确保值和功能的安全。

为了演示一种类型到另一种类型的转换,我们将重用第 2 节中的 Vehicle 层次结构,并在 CarTruck 中的属性上添加 @JsonIgnore 注释以避免不兼容。

public class Car extends Vehicle {
    @JsonIgnore
    private int seatingCapacity;

    @JsonIgnore
    private double topSpeed;

    // constructors, getters and setters
}

public class Truck extends Vehicle {
    @JsonIgnore
    private double payloadCapacity;

    // constructors, getters and setters
}

以下代码将验证转换是否成功以及新对象是否保留旧对象的数据值:

ObjectMapper mapper = new ObjectMapper();

Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = mapper.convertValue(car, Truck.class);

assertEquals("Mercedes-Benz", truck.getMake());
assertEquals("S500", truck.getModel());

4.2.不使用无参数构造函数的反序列化

默认情况下,Jackson 使用无参数构造函数重新创建数据对象。这在某些情况下会很不方便,例如当类具有非默认构造函数并且用户必须编写无参数构造函数以满足 Jackson 的要求时。在类层次结构中,情况更加麻烦,因为必须将无参数构造函数添加到类以及继承链中的所有更高级别的构造函数中。在这些情况下, 创建者方法 可以发挥作用。

本节将使用与第 2 节中类似的对象结构,但对构造函数进行了一些更改。具体来说,所有无参数构造函数都会被删除,具体子类型的构造函数会使用 @JsonCreator@JsonProperty 进行注释,以使它们成为创建者方法。

public class Car extends Vehicle {

    @JsonCreator
    public Car(
      @JsonProperty("make") String make, 
      @JsonProperty("model") String model, 
      @JsonProperty("seating") int seatingCapacity, 
      @JsonProperty("topSpeed") double topSpeed) {
        super(make, model);
        this.seatingCapacity = seatingCapacity;
            this.topSpeed = topSpeed;
    }

    // fields, getters and setters
}

public class Truck extends Vehicle {

    @JsonCreator
    public Truck(
      @JsonProperty("make") String make, 
      @JsonProperty("model") String model, 
      @JsonProperty("payload") double payloadCapacity) {
        super(make, model);
        this.payloadCapacity = payloadCapacity;
    }

    // fields, getters and setters
}

测试将验证 Jackson 是否可以处理缺少无参数构造函数的对象:

ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping();
        
Car car = new Car("Mercedes-Benz", "S500", 5, 250.0);
Truck truck = new Truck("Isuzu", "NQR", 7500.0);

List<Vehicle> vehicles = new ArrayList<>();
vehicles.add(car);
vehicles.add(truck);

Fleet serializedFleet = new Fleet();
serializedFleet.setVehicles(vehicles);

String jsonDataString = mapper.writeValueAsString(serializedFleet);
mapper.readValue(jsonDataString, Fleet.class);

5. 结论

本教程涵盖了几个有趣的用例来演示 Jackson 对类型继承的支持,重点是多态性和对超类型属性的忽略。

所有这些示例和代码片段的实现都可以在GitHub 项目中找到。