Рубрики
Без рубрики

Наследование с Джексоном

В этом руководстве будет показано, как обрабатывать включение метаданных подтипов и игнорирование свойств, унаследованных от суперклассов с помощью Jackson.

Автор оригинала: baeldung.

1. Обзор

В этой статье мы рассмотрим работу с иерархиями классов в Джексоне.

Два типичных случая использования – это включение метаданных подтипа и игнорирование свойств, унаследованных от суперклассов. Мы опишем эти два сценария и пару обстоятельств, при которых требуется особое отношение к подтипам.

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 object. Затем метаданные этого типа будут применены ко всем назначенным типам. В результате очень удобно использовать этот метод для добавления метаданных типов, особенно когда задействовано большое количество типов. Недостатком является то, что он использует полные имена типов Java в качестве идентификаторов типов и, таким образом, непригоден для взаимодействия с системами, не являющимися Java, и применим только к нескольким предопределенным типам типов.

Структура Vehicle , показанная выше, используется для заполнения экземпляра класса Fleet :

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

Чтобы внедрить метаданные типа, нам необходимо включить функцию ввода в объекте ObjectMapper , который позже будет использоваться для сериализации и десериализации объектов данных:

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

Параметр применимость определяет типы, требующие информации о типе, а параметр включает является механизмом включения метаданных типа. Кроме того, предусмотрены два других варианта метода enableDefaultTyping :

  • ObjectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping 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 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 в предыдущем примере, для иллюстрации аннотаций для каждого класса. Единственным изменением является добавление аннотаций к транспортному средству абстрактному классу, как показано ниже:

@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. Аннотации

Для игнорирования свойств обычно используются две аннотации Джексона: @JsonIgnore и @JsonIgnoreProperties . Первое непосредственно применяется к членам типа, указывая Джексону игнорировать соответствующее свойство при сериализации или десериализации. Последний используется на любом уровне, включая тип и член типа, для перечисления свойств, которые следует игнорировать.

@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 говорит Джексону игнорировать 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 vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

строка данных json содержит следующий массив 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. Миксы

Миксины позволяют нам применять поведение (например, игнорировать свойства при сериализации и десериализации) без необходимости прямого применения аннотаций к классу. Это особенно полезно при работе со сторонними классами, в которых мы не можем напрямую изменять код.

Этот подраздел сокращает цепочку наследования классов, введенную в предыдущем, за исключением того, что @JsonIgnore и @JsonIgnoreProperties аннотации к классу Car были удалены:

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

Чтобы продемонстрировать работу миксинов, мы проигнорируем Vehicle.make и Car.top Speed свойства, а затем используем тест, чтобы убедиться, что все работает должным образом.

Первым шагом является объявление смешанного типа:

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

Затем смешивание привязывается к классу данных через ObjectMapper object:

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 vehicles = new ArrayList<>();
vehicles.add(sedan);
vehicles.add(crossover);

String jsonDataString = mapper.writeValueAsString(vehicles);

строка данных json теперь содержит следующий 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. Самоанализ аннотаций

Самоанализ аннотаций-самый мощный метод игнорирования свойств супертипа, поскольку он позволяет детализировать настройку с помощью API AnnotationIntrospector.hasIgnoreMarker .

В этом подразделе используется та же иерархия классов, что и в предыдущем. В этом случае мы попросим Джексона игнорировать Vehicle.model , Crossover.towing Capacity и все свойства, объявленные в классе 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);
    }
}

Интроспектор будет игнорировать любые свойства (то есть он будет рассматривать их так, как если бы они были помечены как игнорируемые с помощью одного из других методов), которые соответствуют набору условий, определенных в методе.

Следующим шагом является регистрация экземпляра класса Introspector с помощью ObjectMapper object:

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. Преобразование Между Подтипами

Джексон позволяет преобразовать объект в тип, отличный от исходного. На самом деле это преобразование может происходить между любыми совместимыми типами, но оно наиболее полезно при использовании между двумя подтипами одного и того же интерфейса или класса для защиты значений и функциональности.

Чтобы продемонстрировать преобразование одного типа в другой, мы повторно используем иерархию Vehicle , взятую из раздела 2, с добавлением аннотации @JsonIgnore о свойствах в Car и Truck , чтобы избежать несовместимости.

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. Десериализация Без Конструкторов No-arg

По умолчанию Джексон воссоздает объекты данных с помощью конструкторов без аргументов. Это неудобно в некоторых случаях, например, когда класс имеет конструкторы, отличные от стандартных, и пользователям приходится писать не большие, чтобы удовлетворить требования Джексона. Это еще более проблематично в иерархии классов, где конструктор no-arg должен быть добавлен в класс и все более высокие в цепочке наследования. В этих случаях методы создателя приходят на помощь.

В этом разделе будет использоваться структура объекта, аналогичная структуре в разделе 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
}

Тест проверит, что Джексон может работать с объектами, в которых отсутствуют конструкторы без аргументов:

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 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. Заключение

В этом учебнике было рассмотрено несколько интересных примеров использования, чтобы продемонстрировать поддержку Джексоном наследования типов, с акцентом на полиморфизм и незнание свойств супертипов.

Реализацию всех этих примеров и фрагментов кода можно найти в проекте GitHub .