Contents

Jackson 对类继承的支持

1. 概述

在本文中,我们将看看在 Jackson 中使用类层次结构。 两个典型的用例是包含子类型元数据和忽略从超类继承的属性。我们将描述这两种情况以及需要对子类型进行特殊处理的几种情况。

2. 包含子类型信息

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

2.1. 全局默认类型

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

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
}

Car子类:

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
}

Truck子类:

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.activateDefaultTyping(PolymorphicTypeValidator ptv, 
  ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)

参数PolymorphicTypeValidator用于验证要反序列化的实际子类型是否根据指定的标准有效。此外,applicability参数确定需要类型信息的类型,includeAs参数是类型元数据包含的机制。此外,还提供了activateDefaultTyping方法的另外两个变体:

  • ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv, ObjectMapper.DefaultTyping applicability):允许调用者指定validatorapplicability,同时使用WRAPPER_ARRAY作为includeAs的默认值
  • ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv):允许调用者指定validator,同时使用OBJECT_AND_NON_CONCRETE作为applicability的默认值和WRAPPER_ARRAY作为includeAs的默认值

让我们看看它是如何工作的。首先,我们需要创建一个验证器:

PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
  .allowIfSubType("com.blogdemo.jackson.inheritance")
  .allowIfSubType("java.util.ArrayList")
  .build();

接下来,让我们创建一个ObjectMapper对象并使用上述验证器在其上激活默认类型:

ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(ptv, ObjectMapper.DefaultTyping.NON_FINAL);

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

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",
        [
            [
                "com.blogdemo.jackson.inheritance.Car",
                {
                    "make": "Mercedes-Benz",
                    "model": "S500",
                    "seatingCapacity": 5,
                    "topSpeed": 250.0
                }
            ],
            [
                "com.blogdemo.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. 混和

Mix-ins 允许我们应用行为(例如在序列化和反序列化时忽略属性),而无需直接将注解应用到类。这在处理第三方类时特别有用,我们无法直接修改代码。 本小节重用了上一节介绍的类继承链,只是去掉了Car类上的*@JsonIgnore@JsonIgnoreProperties*注解:

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

为了演示混入的操作,我们将忽略Vehicle.makeCar.topSpeed属性,然后使用测试确保一切按预期工作。 第一步是声明一个混合类型:

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

接下来,mix-in 通过一个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. 注解自省

Annotation introspection 是忽略超类型属性的最强大的方法,因为它允许使用AnnotationIntrospector.hasIgnoreMarker API 进行详细定制。 本小节使用与前一节相同的类层次结构。在这个用例中,我们将要求 Jackson 忽略Vehicle.modelCrossover.towingCapacityCar类中声明的所有属性。让我们从扩展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);