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 系統交互,並且僅適用於幾種預定義的類型。
Fleet 類中的實例被填充使用上面顯示的 Vehicle 結構:
public class Fleet {
private List<Vehicle> vehicles;
// getters and setters
}為了嵌入類型元數據,我們需要啓用 ObjectMapper 對象的類型功能,該對象將用於後續的數據對象序列化和反序列化:
ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv,
ObjectMapper.DefaultTyping applicability, JsonTypeInfo.As includeAs)PolymorphicTypeValidator 參數用於驗證實際的子類型是否符合指定 criteria 的有效性。 此外,applicability 參數確定需要類型信息的類型,includeAs 參數則是類型元數據包含的機制。 另外,activateDefaultTyping 方法還提供了兩種變體:
- ObjectMapper. activateDefaultTyping(PolymorphicTypeValidator ptv, ObjectMapper.DefaultTyping applicability): 允許調用者指定validator 和 applicability,同時使用 WRAPPER_ARRAY 作為 includeAs 的默認值
- ObjectMapper.activateDefaultTyping(PolymorphicTypeValidator ptv): 允許調用者指定validator,同時使用 OBJECT_AND_NON_CONCRETE 作為 applicability 的默認值,以及 WRAPPER_ARRAY 作為 includeAs 的默認值
下面我們來看它的工作原理。 首先,我們需要創建一個驗證器:
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
.allowIfSubType("com.baeldung.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.baeldung.jackson.inheritance.Car",
{
"make": "Mercedes-Benz",
"model": "S500",
"seatingCapacity": 5,
"topSpeed": 250.0
}
],
[
"com.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 註解 (Annotations)
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.model 和 Car.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
Mix-ins 允許我們應用行為(例如在序列化和反序列化時忽略屬性)而無需直接在類上應用註解。這在處理第三方類時尤其有用,因為我們無法直接修改代碼。
本子部分重用上一部分介紹的類繼承鏈,但已從 Car 類中移除了 @JsonIgnore 和 @JsonIgnoreProperties 註解。
public abstract class Car extends Vehicle {
private int seatingCapacity;
private double topSpeed;
// fields, constructors, getters and setters
}為了演示混合模式(mix-in)的操作,我們將忽略 Vehicle.make 和 Car.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 註解反射 (Annotation Introspection)
註解反射是忽略超類屬性的最強大方法,因為它允許使用 AnnotationIntrospector.hasIgnoreMarker API 進行詳細自定義。
本節使用與上一節相同的類層次結構。在此用例中,我們將要求 Jackson 忽略 Vehicle.model、Crossover.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);
}
}這個檢查器將忽略任何滿足其他方法中定義的條件的屬性(即,它們將被視為通過其他方法標記為忽略的)。
下一步是使用 IgnoranceIntrospector 類的一個實例與 ObjectMapper 對象進行註冊:
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 層次結構,並在 Car 和 Truck 中添加 @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 對類型繼承的支持,重點關注多態性和超類屬性的忽略。