Вступление
Этот пост мотивирован некоторой недавней реальной работой, которую мне пришлось выполнить с использованием 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 Listchefs; 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, мы можем сделать две вещи:
- Используйте
@JsonInclude(JsonInclude. Включать. NON_NULL)
аннотация на уровне класса, которая будет игнорировать все поля с нулевым значением в классе. - Используйте ту же аннотацию на уровне поля, которая будет игнорировать только определенные поля с нулевым значением.
И для этого урока это будет касаться сериализации . На десериализацию .
Десериализация
Десериализация с помощью Джексона
Теперь, когда мы рассмотрели основы сериализации, пришло время перейти к обратному процессу – десериализации .
При десериализации мы получаем в качестве входных данных файл 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 Listchefs; 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”