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

Введение в ObjectMappers: использование Jackson в дикой природе

Вступление Этот пост мотивирован недавней реальной работой, которую мне пришлось проделать с участием Джексона… Помеченный как java, веб-разработчик, картографы, сериализация.

Вступление

Этот пост мотивирован некоторой недавней реальной работой, которую мне пришлось выполнить с использованием Jackson ObjectMapper и в нем я попытаюсь объяснить, почему это важно, а также некоторые из его интересных трюков и причуд. Я также перечислю некоторые из его реальных сценариев использования.

Что такое Jackson ObjectMapper

Цитирование документации:

ObjectMapper предоставляет функциональные возможности для чтения и записи JSON либо в базовые POJOS (простые старые объекты Java), либо в древовидную модель JSON общего назначения (JsonNode) и обратно, а также связанные с ней функции для выполнения преобразований. Он также легко настраивается для работы как с различными стилями содержимого JSON, так и для поддержки более продвинутых концепций объектов, таких как полиморфизм и идентификация объекта. ObjectMapper также действует как фабрика для более продвинутых классов ObjectReader и ObjectWriter. Mapper (и средства чтения объектов, средства записи объектов, которые он создает) будут использовать экземпляры JsonParser и JsonGenerator для реализации фактического чтения/записи JSON. Обратите внимание, что, хотя большинство методов чтения и записи доступны через этот класс, некоторые функциональные возможности доступны только через ObjectReader и ObjectWriter: в частности, чтение/запись более длинных последовательностей значений доступна только через ObjectReader.readvalues(InputStream) и ObjectWriter.writeValues(OutputStream).

Это очень много для обработки, но ключевой вывод находится в самом начале:

ObjectMapper предоставляет функциональные возможности для чтения и записи JSON либо в базовые POJOS (простые старые объекты Java), либо в древовидную модель JSON общего назначения (JsonNode) и обратно, а также связанные с ней функции для выполнения преобразований.

Это очень мощно по нескольким причинам:

  • API-интерфейсы REST обычно возвращают ответы JSON со своих конечных точек, и существует множество библиотек для извлечения информации из JSON
  • JSON – это согласованный стандартный формат, универсальный и может передаваться в виде строк, считываться стандартными текстовыми редакторами и т.д. Другими словами: это просто, универсально и очень, очень полезно для работы и понимания

Сила абстрагирования от представления этих объектов в Java заключается в том, что оно инкапсулирует их и не раскрывает при непосредственном обмене информацией о них, что хорошо.

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

Пример модели предметной области

Давайте посмотрим, как работает ObjectMapper с простыми примерами из хорошей области: ресторан, грузовик с едой, шеф-повара и хорошая еда:)

В нашем ресторане будут ежедневные меню блюд, шеф-повара, готовящие эти меню, и клиенты, посещающие ресторан. Каждая из этих сущностей может быть представлена следующими классами Java:

public class FoodtruckOwner implements Chef {

    private final String name;
    private MainDish mainDish;
    private String establishment;

    public FoodtruckOwner(String name, MainDish mainDish, String establishmentType){
        this.name = name;
        this.mainDish = mainDish;
        this.establishment=establishmentType;
    }

    public void setChefsMainDish(MainDish mainDish) {
        this.mainDish=mainDish;
    }

    public MainDish getChefsMainDish() {
        return mainDish;
    }

    public String getEstablishmentType() {
        return establishment;
    }

    public void setEstablishmentType(String typeOfEstablishment) {
        this.establishment=typeOfEstablishment;
    }
}
public class RestaurantChef implements Chef{

    private final String name;
    private MainDish mainDish;
    private String establishment;
    private final int michelinStars;

    public RestaurantChef(String name, MainDish mainDish, String establishmentType,int michelinStars){
        this.name = name;
        this.mainDish = mainDish;
        this.establishment=establishmentType;
        this.michelinStars=michelinStars;
    }

    public void setChefsMainDish(MainDish mainDish) {
        this.mainDish=mainDish;
    }

    public MainDish getChefsMainDish() {
        return mainDish;
    }

    public String getEstablishmentType() {
        return establishment;
    }

    public void setEstablishmentType(String typeOfEstablishment) {
        this.establishment=typeOfEstablishment;
    }
}

Тогда у нас есть Продовольственное заведение класс:

//Can be a restaurant or a food truck
public class FoodEstablishment {
    private final List chefs;
    private final int lotation;
    private final String type;

    public FoodEstablishment(List chefs, int lotation, String type){
        this.chefs = chefs;
        this.lotation = lotation;
        this.type = type;
    }

    public int getLotation() {
        return lotation;
    }

    public List getChefs() {
        return chefs;
    }

    public String getType() {
        return type;
    }
}

И интерфейс Chef :

public interface Chef {
     void setChefsMainDish(MainDish mainDish);
     MainDish getChefsMainDish();
     String getEstablishmentType();
     void setEstablishmentType(String typeOfEstablishment);
}

Давайте для начала рассмотрим этот небольшой домен.

У нас есть интерфейс Cook , который реализуется как Шеф-поваром ресторана , так и Владельцем Foodtruck поскольку у них обоих будет основное блюдо, на котором они специализируются, и оба они точно знают тип заведения, которым владеют (да: D ).

У нас также есть более общий класс, представляющий Продовольственное заведение , которое, конечно, может быть одним из ресторанов или продовольственных грузовиков.

Для понимания Jackson ObjectMapper и его внутренней работы обратите внимание, что у нас есть атрибут chefs в классе FoodEstablishment , который имеет тип “список Chef интерфейс”, таким образом, коллекция, параметризованная интерфейсом. Это будет важно позже, когда мы сосредоточимся на десериализации .

Погружение: Сериализация и десериализация с использованием Джексона

Теперь, когда мы представили наш небольшой домен для изучения Джексона, давайте углубимся в некоторые детали.

Сериализация

Сериализация – это процесс записи объекта Java в формат JSON (в Интернете вы найдете ссылки на POJOs (Простые старые объекты Java), но, по сути, вам нужен только класс, который вы хотите сериализовать в своем домене, чтобы следовать некоторым свойствам, и все может быть сериализовано.

Давайте рассмотрим выполнение небольшого модульного теста для сериализации в файл JSON класса Food truck Owner :

class FoodtruckOwnerMapperTest {

    ObjectMapper mapper = new ObjectMapper();
    @Test
    public void serialization() throws IOException {
        FoodtruckOwner owner = new FoodtruckOwner("John", new MainDish("rice",6.7),"foodTruck");
        File jsonOwner = new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json");
        mapper.writeValue(jsonOwner, owner);
    }
}

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

Затем мы вызываем метод writeValue класса mapper, который может выдавать исключение, которое мы устанавливаем для выдачи в самом методе тестирования, чтобы мы могли видеть, когда что-то выходит из строя и почему. Картограф может выдать исключение, потому что сериализация может иногда завершаться сбоем.

Если мы запустим тест, он пройдет, и если мы проверим содержимое файла owner.json файл, вот что у нас есть:

{"chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck"}

Теперь вы знаете, как сериализовать класс из Java в JSON!

Однако, некоторые важные моменты:

  • Имя повара – “Джон”, но его нет в файле JSON.
  • “Ключи” в файле JSON задаются из имен методов получения в классе.

Требования к аннотациям и объектному отображению

Имя cook отсутствует в сериализованной версии, потому что у нас нет средства получения для этого свойства . Итак, чтобы иметь возможность сериализовать атрибуты, нам нужно иметь геттеры для свойств, которые мы хотим сериализовать. Если мы добавим недостающий геттер в класс, даже с более конкретным именем:

 public String getFoodtruckOwnerName() {
        return name;
    }

Тогда наша сериализованная версия будет:

{"chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck","foodtruckOwnerName":"John"}

Итак, что касается сериализации, мы узнали:

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

Использование аннотаций для настройки сериализации

Мы можем использовать аннотации, которые позволяют нам настраивать сериализацию. Например, “foodtruckOwnerName” кажется довольно подробным, возможно, мы хотим просто сериализовать атрибут с помощью ключа “name”.

Для этого мы можем использовать аннотацию @JsonProperty в методе getter для атрибута , который получает в качестве аргумента новое имя для сериализации, так, например:

@JsonProperty("nameOfCook")
    public String getFoodtruckOwnerName() {
        return name;
    }

мы получаем следующий JSON:

{"chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck","nameOfCook":"John"}

Последнее свойство, которое нас может заинтересовать, – это, например, не сериализовать поля с нулевыми значениями, скажем, мы получаем экземпляр класса, в котором кто-то забыл задать имя cook, поэтому оно равно null. Тогда мы получаем этот JSON:

{"chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck","nameOfCook":null}

Если мы не хотим, чтобы поле с нулевым значением включалось в наш JSON, мы можем сделать две вещи:

  1. Используйте @JsonInclude(JsonInclude. Включать. NON_NULL) аннотация на уровне класса, которая будет игнорировать все поля с нулевым значением в классе.
  2. Используйте ту же аннотацию на уровне поля, которая будет игнорировать только определенные поля с нулевым значением.

И для этого урока это будет касаться сериализации . На десериализацию .

Десериализация

Десериализация с помощью Джексона

Теперь, когда мы рассмотрели основы сериализации, пришло время перейти к обратному процессу – десериализации .

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

@Test
    void deserialization() throws IOException {
        FoodtruckOwner owner = new FoodtruckOwner("John", new MainDish("rice",6.7),"foodTruck");
        File jsonOwner = new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json");
        mapper.writeValue(jsonOwner, owner);
        FoodtruckOwner chef = mapper.readValue(new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json"),
                FoodtruckOwner.class);
    }

Итак, мы выполняем сериализацию, как описано выше, и пытаемся десериализовать ее из JSON обратно в наш класс модели предметной области, владелец Продовольственного грузовика . Однако мы получаем эту ошибку:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `objectmapper.domain.FoodtruckOwner` (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (File); line: 1, column: 2]

Это несколько неожиданно. После правильной сериализации класса в JSON мы сталкиваемся с непредвиденными проблемами при чтении его обратно в Java.

Джексон требует, чтобы в целевом классе был пустой конструктор по умолчанию, поэтому, если у нас есть это сейчас:

 FoodtruckOwner(){

    }

    public FoodtruckOwner(String name, MainDish mainDish, String establishmentType){
        this.name = name;
        this.mainDish = mainDish;
        this.establishment=establishmentType;
    }

это наверняка сработает, верно?

Не совсем, оказывается, у нас был такой же отсутствующий конструктор по умолчанию в нашем классе Main Dish … Это начинает усложняться. Давайте добавим конструктор по умолчанию в наш класс Main Dish .

После добавления конструктора по умолчанию в класс Main Dish мы получаем то, что хотим, тест зеленый, и у нас есть наш экземпляр в Java, правильно десериализованный:

@Test
    void deserialization() throws IOException {
        FoodtruckOwner owner = new FoodtruckOwner("John", new MainDish("rice",6.7),"foodTruck");
        File jsonOwner = new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json");
        mapper.writeValue(jsonOwner, owner);
        FoodtruckOwner chef = mapper.readValue(new File("/home/bruno/IdeaProjects/objectMapperJackson/owner.json"),
                FoodtruckOwner.class);
        assertEquals("John", chef.getName());
        final MainDish chefsMainDish = chef.getChefsMainDish();
        assertNotNull(chefsMainDish);
        assertEquals("rice", chefsMainDish.getName());
        assertEquals(6.7, chefsMainDish.getCalories());

    }

Итак, мы видим, что после добавления конструкторов по умолчанию , наш процесс десериализации сработал, и нам удалось правильно прочитать в нашем JSON, представляющем владельца грузовика с едой, обратно в Java. Однако были некоторые проблемы.

Необходимо было модифицировать классы Java довольно навязчивыми способами, поскольку нам нужно было явно добавить конструкторы по умолчанию к этим классам, которые являются просто “клиентами” с точки зрения процесса десериализации. Это нехорошо. Что делать, если Основное блюдо класс на самом деле был просто библиотекой, к которой у нас не было прямого доступа? Тогда эти изменения в исходном коде были бы непрактичными, если вообще не невозможными (не исключено, что такие инструменты, как “asm inspector” в IntelliJ, могут эффективно декомпилировать библиотеки в исходный код, который вы можете скопировать и адаптировать к своим потребностям, но это не так).

Давайте предположим, что мы сталкиваемся с таким сценарием: мы не можем изменить источник Main Dish . Как же тогда к этому подойти?

Джексон Миксинс

Когда мы не можем изменить исходный код напрямую, чтобы соответствовать соглашениям, налагаемым Jackson, нам нужно прибегнуть к некоторым специальным функциям, которые позволяют Jackson взаимодействовать “косвенно” с кодом третьей стороны. Одна из таких функций называется “Mix-Ins” .

” Аннотации “Mix-in” – это способ связать аннотации с классами без изменения самих (целевых) классов, изначально предназначенный для поддержки сторонних типов данных, где пользователь не может изменять источники для добавления аннотаций.

С помощью микшеров вы можете:

  • Определите, что аннотации “класса mix-in” (или интерфейса) будут использоваться с “целевым классом” (или интерфейсом) таким образом, чтобы он выглядел так, как если бы “целевой класс” имел все аннотации, которые есть у класса “mix-in” (для целей настройки сериализация/десериализация) Вы можете думать об этом как о своего рода аспектно-ориентированном способе добавления дополнительных аннотаций во время выполнения, чтобы дополнить статически определенные.

Это именно то, что необходимо в нашем случае, поскольку мы не можем изменить класс Main Dish .

Мы можем использовать следующее сочетание:

public abstract class MainDishMixin extends MainDish {
    @JsonCreator
    public MainDishMixin(@JsonProperty("name") String name, @JsonProperty("calories") double calories) {
        super(name, calories);
    }
}

Здесь мы отмечаем некоторые важные вещи:

  • Соединение должно быть определено в пакете, отличном от целевого класса, и оно не может быть определено как внутренний класс целевого

  • Параметры, которые эмулируют исходный конструктор класса, аннотируются аннотацией @JsonProperty и теми же именами и типами как в целевом классе

Затем мы регистрируем смешивание в нашем классе ObjectMapper, используя mapper.addMixIn(MainDish.class , MainDishMixin.class ); и мы закончили!

Затем тест пройдет, и мы достигли того, чего хотели: без прямого изменения целевого класса нам удалось успешно десериализовать его в объект Java.

Десериализация полей интерфейса в классах

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

{"chefs":[{"name":"Michel","michelinStars":4,"chefsMainDish":{"name":"Chicken","calories":20.0},"establishmentType":"Restaurant"},{"name":"John","chefsMainDish":{"name":"rice","calories":6.7},"establishmentType":"foodTruck"}],"lotation":10}

Давайте предположим, что для этого последнего примера у нас есть этот класс:

//Can be a restaurant or a food truck
public class FoodEstablishment {
    private List chefs;
    private int lotation;

    public FoodEstablishment(){}

    public FoodEstablishment(List chefs, int lotation){
        this.chefs = chefs;
        this.lotation = lotation;
    }

    public int getLotation() {
        return lotation;
    }

    public List getChefs() {
        return chefs;
    }
}

К сожалению, попытка десериализовать этот класс как есть приведет нас к еще одной ошибке:

DefinitionException: Cannot construct instance of `objectmapper.domain.Chef` (no Creators, like default construct, exist): abstract types either need to be mapped to concrete types, have custom deserializer, or contain additional type information
 at [Source: (File); line: 1, column: 11] (through reference chain: objectmapper.domain.FoodEstablishment["chefs"]->java.util.ArrayList[0])

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

@JsonTypeInfo(
        use = JsonTypeInfo.Id.CLASS,
        include = JsonTypeInfo.As.PROPERTY,
        property = "class")
@JsonSubTypes({
        @JsonSubTypes.Type(value = RestaurantChef.class, name = "restaurantChef"),
        @JsonSubTypes.Type(value = FoodtruckOwner.class, name = "foodtruckOwner")})
     public interface Chef {
     void setChefsMainDish(MainDish mainDish);
     MainDish getChefsMainDish();
     String getEstablishmentType();
     void setEstablishmentType(String typeOfEstablishment);
}

После добавления вышеупомянутой аннотации информация, предоставленная ею, позволяет Джексону определить при десериализации, какую реализацию использовать, и все будет работать!

Выводы

Я надеюсь, что этот пост дал хорошее общее представление о процессе сериализации и десериализации в Java при использовании Jackson и что с этого момента он позволит изучить еще больше. Спасибо за чтение!

Оригинал: “https://dev.to/brunooliveira/introduction-to-objectmappers-using-jackson-in-the-wild-3ecf”