Всем привет, мои дорогие друзья и читатели! Перед тем, How будем писать статью, немного предыстории… Недавно столкнулся с одной проблемой в работе с библиотекой Mapstruct, которую бегло описал в своем телеграм-канале здесь. В комментариях проблему к записи решor, в этом помог мой коллега по прошлому проекту. После этого я решил написать на эту тему статью, но мы конечно же не будем узко смотреть и постараемся вначале войти в курс дела, понять, что такое Mapstruct и зачем он нужен, и уже на реальном примере разберем возникшую ранее ситуацию и How ее решить. Поэтому настоятельно рекомендую проделать все выкладки параллельно чтению статьи, дабы ощутить все на практике. Перед началом — подпишись на мой телеграм-канал, я там собираю свою деятельность, пишу мысли о разработке на Java и IT в целом. Подписался? Отлично! Ну что ж, теперь поехали!
Mapstruct, чаво?
A code generator for fast type-safe bean mappings. Первая наша задача — разобраться, что такое Mapstruct и зачем он нам. В общем и целом можно почитать о нем на официальном сайте. На главной странице сайта написаны три ответа на вопросы: что это такое? зачем? How? Постараемся и мы так сделать:
What это такое?
Mapstruct — это библиотека, которая помогает сопоставлять (маппить, в целом, так всегда и говорят: маппить, замапить и т.д.) an objectы одних сущностей в an objectы других сущностей при помощи сгенерированного codeа на основе конфигураций, которые описываются через интерфейсы.
Зачем?
В большинстве своем мы разрабатываем многослойные applications (слой работы с базой, слой бизнес логики, слой взаимодействия applications с внешним миром) и каждый слой имеет свои an objectы для хранения и обработки данных. И эти данные нужно передавать из слоя в слой путем перевода из одной сущности в другую. Для тех, кто не работал с таким подходом, это может показаться несколько сложным. Например, у нас есть сущность к базе данных Student. Когда данные этой сущности переходят в слой бизнес-логики (сервисов), нам нужно перевести данные из класса Student в класс StudentModel. Далее, после всех манипуляций с бизнес логикой, данные нужно выдать наружу. И для этого у нас есть класс StudentDto. Разумеется, нам нужно передать данные из класса StudentModel в StudentDto. Писать руками каждый раз методы, которые будут переносить, трудоемко. Плюс это лишний code в codeовой базе, который нужно поддерживать. Можно допустить ошибку. А Mapstruct такие методы генерирует на этапе компиляции и хранит в generated-sources.
Как?
При помощи аннотаций. Нам необходимо просто создать аннотацию, в которой будет главная annotation Mapper, которая скажет библиотеке, что методы в этом интерфейсе можно использовать для перевода из одних an objectов в другие. Как я сказал раньше про студентов, в нашем случае это будет интерфейс StudentMapper, в котором будут несколько методов по перегонке данных из одного слоя в другой:
Красота этого подхода в том, что если в разных классах имена и тип полей совпадают (How в нашем случае), то настроек для Mapstruct хватит, чтобы на основании интерфейса StudentMapper на этапе компиляции сгенерировать нужную реализацию, которая будет переводить. Так уже стало понятнее, да? Пойдем дальше, и на реальном примере разберем работу в Spring Boot приложении.
Пример работы Spring Boot и Mapstruct
Первое, что нам нужно — это создать Spring Boot проект и добавить в него Mapstruct. Для этого дела у меня есть организация в GitHub с шаблонами для репозиториев и старт для Spring Boot один из них. На его основе создаем новый проект: Далее получим проект. Да, друзья, ставьте звезду проекту, если нашли его полезным, так я буду знать, что делаю это не зря. В этом проекте раскроем ситуацию, которую я получил на работе и описал в посте у себя в телеграм-канале. Вкратце обрисую ситуацию для тех, кто не в теме: когда пишем тесты на мапперы (то есть на те реализации интерфейсов, о которых мы говорor ранее) хочется, чтобы тесты проходor How можно быстрее. Самый простой вариант с мапперами — это во время запуска теста использовать аннотацию SpringBootTest, которая поднимет весь ApplicationContext Spring Boot applications и инъектирует нужный для теста маппер внутрь теста. Но этот вариант ресурсоемкий и занимает значительно больше времени, поэтому для нас он не подходит. Нам нужно писать модульный (unit) тест, который бы просто создал нужный маппер и проверил, что его методы работают именно так, How мы ожидаем. Для чего нужно, чтобы быстрее шли тесты? Если тесты проходят долго, то это замедляет весь процесс разработки. Пока тесты не пройдут на новом codeе, этот code нельзя считать верным и его не возьмут в тестирование, а значит его не возьмут в продакшн и значит, что работу разработчик не выполнил. Казалось бы зачем писать тест на библиотеку, работа которой не подлежит сомнению? И все же писать тест нужно, потому что мы тестируем то, насколько правильно описали маппер и делает ли он то, что мы ожидаем. Первым делом, чтобы облегчить нам работу, добавим Lombok в наш проект, путем добавления еще одной зависимости в pom.xml:
В нашем проекте, нам нужно будет перевести из model классов (которые используются для работы с бизнес-логикой) в классы DTO, которые используем для коммуникации с внешним миром. В нашем упрощенном варианте, мы будем предполагать, что поля не изменяются и наши мапперы будут простыми. Но, если будет желание, можно будет написать более развернутую статью о том, How работать с Mapstruct, How его настраивать, How пользоваться его преимуществами. Но потом, так How эта статья выйдет немаленькой. Допустим у нас есть студент со списком лекций и лекторов, которые он посещает. Создадим пакет model. На основе этого создадим простенькую модель:
Теперь создадим маппер, который будет переводить коллекцию моделей лекций в коллекцию DTO лекций. Первое, что нужно сделать — это добавить Mapstruct в проект. Для этого воспользуемся их официальным сайтом, там все описано. То есть нам нужно добавить одну зависимость и плагин в наш помник (если есть вопросы о том, что такое помник, — вот, пожалуйста, Статья1 и Статья2):
Нужно отдельно отметить, что в мапперах мы ссылаемся на другие мапперы. Делается это посредством поля uses в аннотации Mapper, How это сделано в StudentMapper:
Здесь мы используем два маппера, чтобы правильно замаппить список лекций и список лекторов. Теперь нужно скомпorровать наш code и посмотреть, что там и How. Сделать это можно при помощи команды mvn clean compile. Но, How оказалось, при создании реализаций для Mapstruct наших мапперов реализации мапперов не перезаписывали поля. Почему? Оказалось, что не получилось подхватить аннотацию Data от Lombok. И что-то нужно было делать… Поэтому у нас в статье появился новый раздел.
Связываем Lombok и Mapstruct
После нескольких minutes поиска, выяснилось, что нужно определенным образом соединить Lombok и Mapstruct. В documentации Mapstruct есть информация об этом. После исследования примера, который предложor разработчики из Mapstruct, обновим наш pom.xml: Добавим отдельно версии:
После этого должно все получиться. Еще раз скомпorруем наш проект. Но где же искать классы, которые сгенерировал Mapstruct? Они лежат в generated-sources: ${projectDir}/target/generated-sources/annotations/ Теперь, когда мы подготовorсь к тому, чтобы осознать мое разочарование из поста про Mapstruct, попробуем создать тесты на мапперы.
Пишем тесты на наши мапперы
Я создам быстрый и простой тест, который бы протестировал один из мапперов в случае, когда мы создаем интеграционный тест и не заморачиваемся о времени его прохождения:
Здесь аннотацией SpringBootTest мы запускаем весь applicationContext и уже из него при помощи аннотации Autowired извлекаем необходимый нам класс для тестирования. С точки зрения скорости и легкости написания теста — это очень хорошо. Тест успешно проходит, все хорошо. Но мы пойдем другой дорогой и напишем модульный тест на маппер, например, на LectureListMapper…
Так How реализации, которые генерирует Mapstruct лежат в одном класспасе, что и наш проект, то мы спокойно можем использовать их в наших тестах. Выглядит все прекрасно — ниHowих аннотаций, создаем самым простым способом класс, что нам нужен и все. Но когда мы запустим тест, то поймем, что он упадет и в консоли будет NullPointerException… Все потому, что реализация маппера LectureListMapper имеет вид:
Если мы посмотрим, то NPE (сокращение от NullPointerException), мы получаем How раз от переменной lectureMapper, которая оказывается не инициализирована. Но у нас в реализации нет и конструктора, при помощи которого мы бы могли инициализировать переменную. Здесь How раз закопана причина, почему Mapstruct реализовал маппер именно так! В Spring можно несколькими способами добавлять бины в классы, можно инъектировать их через поле вместе с аннотацией Autowired, How сделано выше, а можно инъектировать через конструктор. В такой проблемной ситуации я оказался на работе, когда нужно было оптимизировать время выполнения тестов. Я думал, что сделать с этим ничего нельзя и излил свою боль на своем телеграм-канале. И тут мне в комментариях помогли, сказали, что есть возможность настроить стратегию инъектирования. В интерфейсе Mapper есть поле injectionStrategy, которое How раз и принимает енам InjectionStrategy у которого два значения: FIELD и CONSTRUCTOR. Теперь, зная об этом, добавим в наши мапперы эту настройку, покажу на примере LectureListMapper:
Выделил жирным часть, что добавил. Добавим эту опцию для всех остальных и перекомпorруем проект, чтобы сгенерировались мапперы уже с новой строкой. Когда это выполним, посмотрим How изменилась реализация маппера для LectureListMapper (выделил жирным ту часть, что нам нужна):
И вот теперь Mapstruct реализовал инъектирование маппера через конструктор. Собственно чего мы и добивались. Теперь наш тест перестанет компorроваться, обновим его и получим:
Теперь, если мы запустим тест, то все сработает так How положено, так How в LectureListMapperImpl мы передаем необходимый ему LectureMapper… Победа! Вам не сложно, а мне приятно: Друзья, все How обычно, подписывайтесь на мой гитхаб-аккаунт, на телеграм-аккаунт. Там я выкладываю результат своей деятельности, есть реально полезные вещи) Особенно приглашаю вступить в группу обсуждений телеграм-канала. Так сложилось, что если у кого-то есть технический вопрос, там можно получить ответ. Такой формат интересен для всех, можно почитать, кто что умеет и набраться опыта.
Вывод
В рамках этой статьи мы познакомorсь с таким нужным и часто используемым продуктом How Mapstruct. Разобрали что это, зачем и How. На реальном примере пощупали, что можно делать и How можно менять. Также, разобрали How настроить инъектирование бинов через конструктор, чтобы была возможность нормально проводить тестирование мапперов. Коллеги из Mapstruct предоставor пользователям их продукта выбирать Howим именно способом инъектировать мапперы, за что им несомненно спасибо. НО, несмотря на то, что Spring рекомендует инъектировать бины через конструктор, ребята из Mapstruct поставor по умолчанию инъектирование через поле. Почему так? Нет ответа. Я подозреваю, что могут быть причины о которых мы просто не знаем, и поэтому они так сделали. И чтобы узнать у них, я создал GitHub issue в их официальном репозитории продукта.
GO TO FULL VERSION