что такое маппер java
Interesting Tasks
воскресенье, 20 октября 2013 г.
Маппинг объектов с помощью java-object-merger
Для чего нужны мапперы объектов?
Простой ответ: чтобы копировать данные автоматически из одного объекта в другой. Но тогда вы можете спросить: зачем нужно это копирование? Можно усомниться, что это нужно очень часто. Значит следует дать более развернутый ответ.
В мире энтерпрайз приложений принято бить внутреннюю структуру на слои: слой доступа к базе, бизнес и представление/веб сервиса. В слое доступа к базе как правило обитают объекты мапящиеся на таблицы в базе. Условимся называть их DTO (от Data transfer object). По хорошему, они только переносят данные из таблиц и не содержат бизнес логики. На слое представления/веб сервисов, находятся объекты, доставляющие данные клиенту (браузер / клиенты веб сервисов). Назовем их VO (от View object). VO могут требовать только часть тех данных, которые есть в DTO, или же агрегировать данные из нескольких DTO. Они могут дополнительно заниматься локализацией или преобразованием данных в удобный для представления вид. Так что передавать DTO сразу на представление не совсем правильно. Так же иногда в бизнес слое выделяют бизнес объекты BO (Business object). Они являются обертками над DTO и содержат бизнес логику работы с ними: сохранение, модифицирование, бизнес операции. На фоне этого возникает задача по переносу данных между объектами из разных слоев. Скажем, замапить часть данных из DTO на VO. Или из VO на BO и потом сохранить то что получилось.
Если решать задачу в лоб, то получается примерно такой “тупой” код:
Мапперы объектов
Чем плох дозер
Какими качествами должен обладать маппер?
Реализация
Перед началом разработки был соблазн написать библиотеку на scala. Т.к. уже был положительный опыт ее использования.
Почему merger а не mapper?
Использование
Программа “Hello world” выглядит примерно так:
Во-первых, видим, что для маппинга необходимо, чтобы у свойства был геттер на обоих объектах. Это нужно для сравнения значений. И сеттер у целевого объекта, чтобы записывать новое значение. Сами свойства должны именоваться одинаково.
Посмотрим же как реализован метод map. Это поможет понять многие вещи о библиотеке.
Если исходный снапшот это бин, и если у него есть identifier, тогда пытаемся найти целевой бин для класса destinationClass используя IBeanFinder-ы [тут createSnapshot(destinationClass, identifier);]. Мы такие не регистрировали, да и identifier-а нет, значит идем дальше. В противном случает бин создается используя подходящий IObjectCreator [тут createSnapshot(destinationClass)]. Мы таких тоже не регистрировали, однако в стандартной поставке имеется создатель объектов конструктором по умолчанию — он и используется. Далее у целевого снапшота берется дифф от снапшота источника и применяется к целевому объекту. Все.
Кстати, дифф, для этого простого случая, будет выглядеть так:
Основные аннотации:
Преобразования типов
В IMergingContext можно регистрировать пользовательские преобразователи типов, из одного типа в другой (интерфейс TypeConverter). Стандартный набор преобразователей включает преобразования:
Русские Блоги
MAPSTRUCT (использование @Mapper)
2 Введение в MapStruct за 2 минуты
Следующий код демонстрирует, как использовать Map Struct для реализации сопоставления между Java Beans. Предположим, у нас есть класс Car, который представляет автомобиль, а также есть объект передачи данных (DTO) CarDTO.
Эти два класса очень похожи, за исключением того, что имя атрибута как количество отличается, и в объекте Car поле, представляющее тип автомобиля, является перечислением, тогда как в CarDTO напрямую Используйте строковое представление.
Car.java
2.1 Интерфейс картографа
Чтобы сгенерировать средство сопоставления, которое преобразует объекты CarDTO и Car, нам необходимо определить интерфейс сопоставителя.
CarMapper.java
1. Аннотация @Mapper отмечает этот интерфейс как интерфейс сопоставления и является точкой входа процессора MapStruct во время компиляции.
2. Метод, который действительно реализует сопоставление, требует исходный объект в качестве параметра и возвращает целевой объект. Название метода сопоставления произвольно. В случае, когда имена атрибутов в исходном и целевом объекте различаются, эти имена можно настроить с помощью аннотации @Mapping. Мы также можем преобразовать параметры разных типов в исходный тип и целевой тип. Здесь тип перечисления преобразуется в строку через атрибут типа. Конечно, в интерфейсе можно определить несколько методов сопоставления. MapStruct сгенерирует для него реализацию.
3. Реализация автоматически сгенерированного интерфейса может быть получена через объект класса Mapper. По соглашению переменная-член INSTANCE объявляется в интерфейсе, чтобы клиент мог получить доступ к реализации интерфейса Mapper.
2.2 Компиляция
Поскольку MapStruct обрабатывает аннотации в виде подключаемого модуля компилятора Java и генерирует реализацию интерфейса сопоставителя. Следовательно, мы должны вручную скомпилировать его перед его использованием (функция автоматической компиляции IDE не будет использовать функцию подключаемого модуля MapStruct).
Выполните команду maven: mvn compile
Как показано на следующем рисунке, в целевом каталоге имеется более одного класса CarMapperImpl.class:
Этот класс на самом деле является классом реализации, который плагин структуры карты автоматически помогает нам создавать на основе интерфейса CarMapper. Мы можем просмотреть исходный код автоматически созданного класса реализации с помощью функции декомпиляции IDE, как показано на следующем рисунке:
Через декомпилированный исходный код мы можем увидеть это для разных имен атрибутов (seatCount и numberOfSeats) и разных типов атрибутов (типа перечисления и типа строки) Все автоматически помогло нам конвертировать. Для преобразований с разными именами атрибутов мы указываем их с помощью аннотации @Mapping, а преобразования с разными типами атрибутов являются конфигурацией по умолчанию MapStruct. Лично это очень хорошо и очень мощно.
2.3 Использование картографа
Запустите этот модульный тест, если ошибок не было, это означает, что мы успешно выполнили этот кейс!
ModelMapper: путешествие туда и обратно
По известным причинам, бэкенд не может отдавать данные из репозитория как есть. Самая известная — сущностные зависимости берутся из базы не в таком виде, в котором их может понять фронт. Сюда же можно добавить и сложности с парсингом enum (если поля enum содержат дополнительные параметры), и многие другие сложности, возникающие при автоматическом приведении типов (или невозможности автоматического их приведения). Отсюда вытекает необходимость в использовании Data Transfer Object — DTO, понятном и для бэка, и для фронта.
Конвертацию сущности в DTO можно решить по-разному. Можно применить библиотеку, можно (если проект маленький) наколхозить что-то вроде такого:
Такие самописные мапперы имеют явные недостатки:
Сразу хочу сказать, что если вам что-то непонятно, Вы можете скачать готовый проект с рабочим тестом, ссылка в конце статьи.
Первый шаг — это, конечно, добавление зависимости. Я использую gradle, но вам не составит труда добавить зависимость в maven-проект.
Этого достаточно, чтобы маппер заработал. Далее, нам необходимо создать бин.
Обычно достаточно просто вернуть new ModelMapper, но не лишним будет настроить маппер для наших нужд. Я задал строгую стратегию соответствия, включил сопоставление полей, пропуск нулловых полей и задал приватный уровень доступа к полям.
Далее, создаём следующую структуру сущностей. У нас будет единорог (Unicorn), у которого в подчинении будет какое-то количество дроидов (Droid), и у каждого дроида будет какое-то количество капкейков (Cupcake).
Эти сущности мы будем конвертировать в DTO. Существует как минимум два подхода к конвертации зависимостей из сущности в DTO. Один подразумевает сохранение только ID вместо сущности, но тогда каждую сущность из зависимости при нужде мы будем дёргать по ID дополнительно. Второй подход подразумевает сохранение DTO в зависимости. Так, при первом подходе мы бы конвертировали List droids в List droids (в новом списке храним только ID), а при втором подходе мы будем сохранять в List droids.
Для тонкой настройки маппера под наши нужды нам будет необходимо создать собственный класс-обёртку и переопределить логику для маппинга коллекций. Для этого мы создаём класс-компонент UnicornMapper, автовайрим туда наш маппер и переопределяем нужные нам методы.
Самый простой вариант класса-обёртки выглядит так:
Теперь нам достаточно заавтовайрить наш маппер в какой-нибудь сервис и дёргать по методам toDto и toEntity. Найденные в объекте сущности маппер будет превращать в DTO, DTO — в сущности.
Но если мы попробуем таким образом законвертировать что-нибудь, а потом вызвать, к примеру, toString, то мы получим StackOverflowException, и вот почему: в UnicornDto находится список DroidDto, в котором находится UnicornDto, в котором находятся DroidDto, и так до того момента, пока не закончится стековая память. Поэтому для обратных зависимостей я обычно использую не UnicornDto unicorn, а Long unicornId. Мы, таким образом, сохраняем связь с Unicorn, но обрубаем циклическую зависимость. Поправим наши DTO таким образом, чтобы вместо обратных DTO они хранили ID своих зависимостей.
Но теперь, если мы вызовём DroidMapper, мы получим unicornId == null. Это происходит потому, что ModelMapper не может определить точно, что такое Long. И просто не сетит его. И нам придётся заняться тонкой настройкой необходимых мапперов, чтобы научить их мапить сущности в ID.
Вспоминаем, что с каждым бином после его инициализации можно поработать вручную.
В @PostConstruct мы зададим правила, в которых укажем, какие поля маппер трогать не должен, потому что для них мы определим логику самостоятельно. В нашем случае, это как определение unicornId в DTO, так и определение Unicorn в сущности (поскольку что делать с Long unicornId, маппер так же не знает).
TypeMap — это и есть правило, в котором мы указываем все нюансы маппинга, а также, задаём конвертер. Мы указали, что для конвертирования из Droid в DroidDto мы пропускаем setUnicornId, а при обратной конвертации — setUnicorn. Конвертировать мы всё будем в конвертере toDtoConverter() для UnicornDto и в toEntityConverter() для Unicorn. Эти конвертеры мы должны описать в нашем компоненте.
Самый простой постконвертер выглядит так:
Нам необходимо расширить его функциональность:
То же самое делаем и с обратным конвертером:
По сути, мы просто вставляем в каждый постконвертер дополнительный метод, в котором пропишем собственную логику для пропущенных полей.
При мапинге в DTO мы сетим ID сущности. При мапинге в DTO достаём сущность из репозитория по ID.
Я показал необходимый минимум для начала работы с modelmapper и особо не рефакторил код. Если у тебя, читатель, есть что добавить к моей статье, я буду рад услышать конструктивную критику.
Проект можно посмотреть тут:
Проект на GitHub.
Любители чистого кода наверняка усмотрели уже возможность загнать многие компоненты кода в абстракции. Если Вы из их числа, предлагаю под кат.
Для начала, определим интерфейс для основных методов класса-обёртки.
Унаследуем от него абстрактный класс.
Постконвертеры и методы заполнения специфичных полей смело отправляем туда. Также, создаём два объекта типа Class и конструктор для их инициализации:
Теперь количество кода в DroidMapper сокращается до следующего:
Маппер без специфичных полей выглядит вообще просто:
Введение в Джексон ОбъектМаппер
В статье обсуждается центральный класс ObjectMapper Джексона, базовая сериализация и дезериализация, а также настройка двух процессов.
1. Обзор
Этот учебник фокусируется на понимании Джексона ОбъектМаппер класс и как сериализировать java-объекты в JSON и дезериализировать строку JSON в java-объекты.
Чтобы узнать больше о библиотеке Джексона в целом, Джексон Tutorial является хорошим местом для начала.
Дальнейшее чтение:
Наследование с Джексоном
Джексон JSON Просмотров
Джексон — пользовательский сериализатор
2. Зависимости
Давайте сначала добавим следующие зависимости к пом.xml :
Эта зависимость также будет временно добавить следующие библиотеки к классу:
3. Чтение и написание с помощью ObjectMapper
Начнем с основных операций чтения и записи.
Простая readValue API ОбъектМаппер является хорошей точкой входа. Мы можем использовать его для разбора или deserialize содержимого JSON в Java-объект.
Кроме того, на письменной стороне, мы можем использовать writeValue API для сериализации любого объекта Java в качестве вывода JSON.
Мы будем использовать следующие Автомобильные класс с двумя полями в качестве объекта для сериализации или дезериализации на протяжении всей этой статьи:
3.1. Объект Java для JSON
Давайте посмотрим первый пример сериализации объекта Java в JSON с помощью writeValue метод ОбъектМаппер класс:
Выход вышеуказанного в файле будет:
Методы writeValueAsString и writeValueAsBytes ОбъектМаппер класс генерирует JSON из объекта Java и возвращает сгенерированный JSON в качестве строки или в качестве массива карт:
3.2. JSON к объекту Java
Ниже приведен простой пример преобразования строки JSON в java-объект с помощью ОбъектМаппер класс:
readValue () функция также принимает другие формы ввода, такие как файл, содержащий строку JSON:
3.3. JSON Джексон JsonNode
Кроме того, JSON можно разобрать на JsonNode объект и используется для извлечения данных из определенного узла:
3.4. Создание списка Java из строки JSON Array
Мы можем разобрать JSON в виде массива в списке объектов Java с помощью ТипРеференс :
3.5. Создание Java-карты из строки JSON
Аналогичным образом, мы можем разобрать JSON на Java- Карта :
4. Расширенные функции
Одной из самых сильных сторон библиотеки Джексона является настраиваемый процесс сериализации и дезируялизации.
В этом разделе мы пройдемся по некоторым расширенным функциям, где входная или выходная реакция JSON может отличаться от объекта, генерируя или потребляя ответ.
4.1. Настройка функции сериализации или дезериализации
При преобразовании объектов JSON в классы Java в случае, если строка JSON имеет несколько новых полей, процесс по умолчанию приведет к исключению:
Строка JSON в приведенном выше примере в процессе разбора по умолчанию на объект Java для Класс автомобильных приведет к НепризнанныйПропертиЭксцепция исключение.
Через настроить метод, мы можем расширить процесс по умолчанию, чтобы игнорировать новые поля :
Аналогичным образом, FAIL_ON_NUMBERS_FOR_ENUM элементы управления, если значения enum разрешены к сериализации/дезериализации в качестве чисел:
4.2. Создание пользовательского сериализатора или дезериализатора
Пользовательские сериализаторы и deserializers очень полезны в ситуациях, когда вход или выход JSON ответ отличается по структуре, чем класс Java, в котором он должен быть сериализован или deserialized.
Ниже приведен пример пользовательского сериала JSON :
Этот пользовательский сериализатор может быть вызван так:
Вот что такое Автомобильные выглядит как (как выход JSON) на стороне клиента:
И вот пример пользовательский JSON deserializer :
Этот пользовательский deserializer может быть вызван таким образом:
4.3. Обработка форматов дат
Сериализация по умолчанию java.util.Date производит число, т.е. эпоха таймштамп (количество миллисекунд с 1 января 1970 года, UTC). Но это не очень читаемый человек и требует дальнейшего преобразования, которые будут отображаться в человеческом читаемом формате.
Давайте завернем Автомобильные экземпляр мы использовали до сих пор внутри Запрос класс с датаПрофилированная свойство:
4.4. Обработка коллекций
Еще одна небольшая, но полезная функция, доступная через ДесериализацияФеатура класс — это возможность создавать коллекцию, которую мы хотим получить из ответа JSON Array.
Например, мы можем создать результат в массиве:
5. Заключение
Джексон является твердой и зрелой библиотекой сериализации/дезериализации JSON для Java. ОбъектМаппер API предоставляет простой способ анализа и генерации объектов реагирования JSON с большой гибкостью. В этой статье обсуждались основные особенности, которые делают библиотеку настолько популярной.
Исходный код, который сопровождает статью, можно найти более чем на GitHub.
Свой mapper или немного про ExpressionTrees
Сегодня мы поговорим про то, как написать свой AutoMapper. Да, мне бы очень хотелось рассказать вам об этом, но я не смогу. Дело в том, что подобные решения очень большие, имеют историю проб и ошибок, а также прошли долгий путь применения. Я лишь могу дать понимание того, как это работает, дать отправную точку для тех, кто хотел бы разобраться с самим механизмом работы «мапперов». Можно даже сказать, что мы напишем свой велосипед.
Отказ от ответственности
Я ещё раз напоминаю: мы напишем примитивный mapper. Если вам вдруг вздумается его доработать и использовать в проде — не делайте этого. Возьмите готовое решение, которое знает стек проблем этой предметной области и уже умеет их решать. Есть несколько более-менее весомых причинам писать и использовать свой вело-mapper:
Что называют словом «mapper»?
Это подсистема, которая отвечает за то, чтобы взять некий объект и преобразовать (скопировать его значения) его в другой. Типичная задача: преобразовать DTO в объект бизнес слоя. Самый примитивный mapper «бежит» по свойствам (property) источника данных и сопоставляет их со свойствами типа данных, который будет на выходе. После сопоставления происходит извлечение значений из источника и их запись в объект, который будет результатом преобразования. Где-то по пути, скорее всего, нужно будет ещё создать этот самый «результат».
Для потребителя mapper — это сервис, который предоставляет следующий интерфейс:
Подчеркиваю: это наиболее примитивный интерфейс, который, с моей точки зрения, удобен для объяснения. В реальности мы, скорее всего, будем иметь дело с более конкретным маппером (IMapper ) или с более общим фасадом (IMapper), который сам подберет конкретный mapper под заданные типы объектов входа-выхода.
Наивная реализация
Ремарка: даже наивная реализация mapper’a требует элементарных знаний в области Reflection и ExpressionTrees. Если вы ещё не прошли по ссылкам или ничего не слышали об этих технологиях — сделайте это, прочтите. Обещаю, мир уже никогда не будет прежним.
Впрочем, мы с вами пишем свой mapper. Для начала давайте получим все свойства (PropertyInfo) того типа данных, который будет на выходе (далее я буду называть его TOut). Это сделать достаточно просто: тип мы знаем, так как пишем имплементацию generic-класса, параметризированного типом TOut. Далее, используя экземпляр класса Type, мы получаем все его свойства.
При получении свойств я опускаю особенности. Например, некоторые из них могут быть без setter-функции, некоторые могут быть помечены аттрибутом как игнорируемые, некоторые могут быть со специальным доступом. Мы рассматриваем самый простой вариант.
Идём далее. Было бы неплохо уметь создавать экземпляр типа TOut, то есть того самого объекта, в который мы «мапим» входящий объект. В C# это можно сделать несколькими способами. Например, мы можем сделать так: System.Activator.CreateInstance(). Или даже просто new TOut(), но для этого вам нужно создать ограничение для TOut, чего в обобщенном интерфейсе делать не хотелось бы. Впрочем, мы с вами что-то знаем об ExpressionTrees, а значит можем сделать вот так:
Почему именно так? Потому что мы знаем, что экземпляр класса Type может дать информацию о том, какие у него есть конструкторы — это весьма удобно для случаев, когда мы решим развить свой mapper настолько, что будем передавать в конструктор какие-либо данные. Также, мы ещё немного узнали про ExpressionTrees, а именно — они позволяют налету создать и скомпилировать код, который потом можно будет многократно использовать. В данном случае это функция, которая на самом деле выглядит как () => new TOut().
Теперь нужно написать основной метод mapper’a, который будет копировать значения. Мы пойдем по самому простому пути: идём по свойствам объекта, который пришёл к нам на вход, и ищем среди свойств исходящего объекта свойство с таким же названием. Если нашли — копируем, если нет — идём дальше.
Таким образом у нас полностью сформировался класс BasicMapper. С его тестами можно ознакомиться вот тут. Обратите внимание, что источником может быть как объект какого-то конкретного типа, так и анонимный объект.
Производительность и boxing
Reflection отличная, но медленная штука. Более того, её частое использование увеличивает memory traffic, а значит нагружает GC, а значит ещё больше замедляет работу приложения. Например, только что мы использовали методы PropertyInfo.SetValue и PropertyInfo.GetValue. Метод GetValue возвращает object, в которой завернуто (boxing) некое значение. Это значит, что мы получили аллокацию на пустом месте.
Mapper’ы обычно находятся там, где нужно превратить один объект в другой… Нет, не один, а множество объектов. Например, когда мы забираем что-то из базы данных. В этом месте хотелось бы видеть нормальную производительность и не терять память на элементарной операции.
Компилируемый mapper
На самом деле, всё относительно просто: мы уже делали new с помощью Expression.New(ConstructorInfo). Наверное вы заметили, что статический метод New называется точно так же, как и оператор. Дело в том, что почти у всего синтаксиса C# есть отражение в виде статических методов класса Expression. Если чего-то нет, то это значит, что вы ищите т.н. «синтаксический сахар».
Вот несколько операций, которые мы будем использовать в нашем mapper’e:
К сожалению, код получается не очень компактный, поэтому предлагаю сразу взглянуть на имплементацию CompiledMapper. Я вынес сюда лишь узловые моменты.
Для начала мы создаем объектное представление параметра нашей функции. Так как она принимает на вход object, то и параметром будет объект типа object.
Далее мы создаем две переменные и список Expression, в который будем последовательно складывать выражения присваивания. Порядок важен, ведь именно так команды будут выполнены, когда мы вызовем скомпилированный метод. Например, мы не можем присвоить значение переменной, которая ещё не объявлена.
Далее мы точно также, как и в случае с наивной имплементацией, идём по списку свойств типов и пытаемся их сопоставить по имени. Однако, вместо того, чтобы немедленно присваивать значения — мы создаем выражения извлечения значений и присваивания значений для каждого сопоставленного свойства.
Важный момент: после того, как мы создали все операции присваивания нам нужно вернуть результат из функции. Для этого последним выражением в списке должно быть Expression, содержащее экземпляр класса, который мы создали. Я оставил комментарий рядом с этой строчкой. Почему поведение, соответствующее ключевому слову return в ExpressionTree выглядит именно так? Боюсь, что это отдельная тема. Сейчас я предлагаю это просто запомнить.
Ну и в самом конце мы должны скомпилировать все выражения, которые мы построили. Что нам тут интересно? Переменная body содержит «тело» функции. У «обычных функций» ведь есть тело, верно? Ну, которое мы заключаем в фигурные скобки. Так вот, Expression.Block — это именно оно. Так как фигурные скобки — это ещё и область видимости, то мы должны передать туда переменные, которые там будут использоваться — в нашем случае sourceInstance и outInstance.
Почему не будет boxing? Потому что скомпилированный ExpressionTree это настоящий IL и для runtime он выглядит также (почти), как и ваш код. Почему «скомпилированный mapper» работает быстрее? Снова: потому что это просто обычный IL. Кстати, скорость мы можем легко подтвердить с помощью библиотеки BenchmarkDotNet, а сам бенчмарк можно посмотреть тут.
Method | Mean | Error | StdDev | Ratio | Allocated |
---|---|---|---|---|---|
AutoMapper | 1,291.6 us | 3.3173 us | 3.1030 us | 1.00 | 312.5 KB |
Velo_BasicMapper | 11,987.0 us | 33.8389 us | 28.2570 us | 9.28 | 3437.5 KB |
Velo_CompiledMapper | 341.3 us | 2.8230 us | 2.6407 us | 0.26 | 312.5 KB |
В колонке Ratio «скомпилированный mapper» (CompiledMapper) показал очень неплохой результат, даже по сравнению с AutoMapper (он baseline, т.е. 1). Впрочем, давайте не будем радоваться: AutoMapper обладает значительно большими возможностями по сравнению с нашим велосипедом. Этой табличкой я лишь хотел показать, что ExpressionTrees значительно быстрее, чем «подход классического Reflection».
Резюме
А что mapper? Mapper — отличный пример, на котором всему этому можно научиться.