Предыстория

Всем привет, мои дорогие друзья и читатели! Перед тем, как будем писать статью, немного предыстории… Недавно столкнулся с одной проблемой в работе с библиотекой Mapstruct, которую бегло описал в своем телеграм-канале здесь. В комментариях проблему к записи решили, в этом помог мой коллега по прошлому проекту. Что такое Mapstruct и как правильно настроить его для модульного тестирования в SpringBoot приложениях. Часть 1 - 1После этого я решил написать на эту тему статью, но мы конечно же не будем узко смотреть и постараемся вначале войти в курс дела, понять, что такое Mapstruct и зачем он нужен, и уже на реальном примере разберем возникшую ранее ситуацию и как ее решить. Поэтому настоятельно рекомендую проделать все выкладки параллельно чтению статьи, дабы ощутить все на практике. Перед началом — подпишись на мой телеграм-канал, я там собираю свою деятельность, пишу мысли о разработке на Java и IT в целом. Подписался? Отлично! Ну что ж, теперь поехали!

Mapstruct, чаво?

A code generator for fast type-safe bean mappings. Первая наша задача ​​— разобраться, что такое Mapstruct и зачем он нам. В общем и целом можно почитать о нем на официальном сайте. На главной странице сайта написаны три ответа на вопросы: что это такое? зачем? как? Постараемся и мы так сделать:

Что это такое?

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

Зачем?

В большинстве своем мы разрабатываем многослойные приложения (слой работы с базой, слой бизнес логики, слой взаимодействия приложения с внешним миром) и каждый слой имеет свои объекты для хранения и обработки данных. И эти данные нужно передавать из слоя в слой путем перевода из одной сущности в другую. Для тех, кто не работал с таким подходом, это может показаться несколько сложным. Например, у нас есть сущность к базе данных Student. Когда данные этой сущности переходят в слой бизнес-логики (сервисов), нам нужно перевести данные из класса Student в класс StudentModel. Далее, после всех манипуляций с бизнес логикой, данные нужно выдать наружу. И для этого у нас есть класс StudentDto. Разумеется, нам нужно передать данные из класса StudentModel в StudentDto. Писать руками каждый раз методы, которые будут переносить, трудоемко. Плюс это лишний код в кодовой базе, который нужно поддерживать. Можно допустить ошибку. А Mapstruct такие методы генерирует на этапе компиляции и хранит в generated-sources.

Как?

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

public class Student {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentDTO {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentModel {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}
Для этих классов создаем маппер (здесь и далее так мы будем называть интерфейс, который описывает что мы хотим перевести и куда):

@Mapper
public interface StudentMapper {
   StudentModel toModel(StudentDTO dto);
   Student toEntity(StudentModel model);
   StudentModel toModel(Student entity);
   StudentDTO toDto(StudentModel model);
}
Красота этого подхода в том, что если в разных классах имена и тип полей совпадают (как в нашем случае), то настроек для Mapstruct хватит, чтобы на основании интерфейса StudentMapper на этапе компиляции сгенерировать нужную реализацию, которая будет переводить. Так уже стало понятнее, да? Пойдем дальше, и на реальном примере разберем работу в Spring Boot приложении.

Пример работы Spring Boot и Mapstruct

Первое, что нам нужно — это создать Spring Boot проект и добавить в него Mapstruct. Для этого дела у меня есть организация в GitHub с шаблонами для репозиториев и старт для Spring Boot один из них. На его основе создаем новый проект: Что такое Mapstruct и как правильно настроить его для модульного тестирования в SpringBoot приложениях. Часть 1 - 2Далее получим проект. Да, друзья, ставьте звезду проекту, если нашли его полезным, так я буду знать, что делаю это не зря. В этом проекте раскроем ситуацию, которую я получил на работе и описал в посте у себя в телеграм-канале. Вкратце обрисую ситуацию для тех, кто не в теме: когда пишем тесты на мапперы (то есть на те реализации интерфейсов, о которых мы говорили ранее) хочется, чтобы тесты проходили как можно быстрее. Самый простой вариант с мапперами — это во время запуска теста использовать аннотацию SpringBootTest, которая поднимет весь ApplicationContext Spring Boot приложения и инъектирует нужный для теста маппер внутрь теста. Но этот вариант ресурсоемкий и занимает значительно больше времени, поэтому для нас он не подходит. Нам нужно писать модульный (unit) тест, который бы просто создал нужный маппер и проверил, что его методы работают именно так, как мы ожидаем. Для чего нужно, чтобы быстрее шли тесты? Если тесты проходят долго, то это замедляет весь процесс разработки. Пока тесты не пройдут на новом коде, этот код нельзя считать верным и его не возьмут в тестирование, а значит его не возьмут в продакшн и значит, что работу разработчик не выполнил. Казалось бы зачем писать тест на библиотеку, работа которой не подлежит сомнению? И все же писать тест нужно, потому что мы тестируем то, насколько правильно описали маппер и делает ли он то, что мы ожидаем. Первым делом, чтобы облегчить нам работу, добавим Lombok в наш проект, путем добавления еще одной зависимости в pom.xml:

<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <scope>provided</scope>
</dependency>
В нашем проекте, нам нужно будет перевести из model классов (которые используются для работы с бизнес-логикой) в классы DTO, которые используем для коммуникации с внешним миром. В нашем упрощенном варианте, мы будем предполагать, что поля не изменяются и наши мапперы будут простыми. Но, если будет желание, можно будет написать более развернутую статью о том, как работать с Mapstruct, как его настраивать, как пользоваться его преимуществами. Но потом, так как эта статья выйдет немаленькой. Допустим у нас есть студент со списком лекций и лекторов, которые он посещает. Создадим пакет model. На основе этого создадим простенькую модель:

package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
его лекции

package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
и лекторы

package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
И создадим пакет dto рядом с пакетом model:

package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
лекции

package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
и лекторы

package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
Теперь создадим маппер, который будет переводить коллекцию моделей лекций в коллекцию DTO лекций. Первое, что нужно сделать — это добавить Mapstruct в проект. Для этого воспользуемся их официальным сайтом, там все описано. То есть нам нужно добавить одну зависимость и плагин в наш помник (если есть вопросы о том, что такое помник, — вот, пожалуйста, Статья1 и Статья2):

<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>1.4.2.Final</version>
</dependency>
и в помнике в блок <build/>. которого у нас еще не было:

<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.5.1</version>
           <configuration>
               <source>1.8</source>
               <target>1.8</target>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>1.4.2.Final</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>
Далее создадим пакет mapper рядом с dto и model. На основе тех классов, что мы показали ранее, нужно будет создать еще пять мапперов:
  • Маппер LectureModel <-> LectureDTO
  • Маппер List<LectureModel> <-> List<LectureDTO>
  • Маппер LecturerModel <-> LecturerDTO
  • Маппер List<LecturerModel> <-> List<LecturerDTO>
  • Маппер StudentModel <-> StudentDTO
Поехали:

LectureMapper


package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LecturerDTO dto);
}

LectureListMapper


package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LectureMapper.class)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}

LecturerMapper


package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LectureDTO dto);
}

LecturerListMapper


package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LecturerModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LecturerMapper.class)
public interface LecturerListMapper {
   List<LecturerModel> toModelList(List<LecturerDTO> dloList);
   List<LecturerDTO> toDTOList(List<LecturerModel> modelList);
}

StudentMapper


package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.StudentDTO;
import com.github.romankh3.templaterepository.springboot.model.StudentModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
public interface StudentMapper {
   StudentDTO toDTO(StudentModel model);
   StudentModel toModel(StudentDTO dto);
}
Нужно отдельно отметить, что в мапперах мы ссылаемся на другие мапперы. Делается это посредством поля uses в аннотации Mapper, как это сделано в StudentMapper:

@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
Здесь мы используем два маппера, чтобы правильно замаппить список лекций и список лекторов. Теперь нужно скомпилировать наш код и посмотреть, что там и как. Сделать это можно при помощи команды mvn clean compile. Но, как оказалось, при создании реализаций для Mapstruct наших мапперов реализации мапперов не перезаписывали поля. Почему? Оказалось, что не получилось подхватить аннотацию Data от Lombok. И что-то нужно было делать… Поэтому у нас в статье появился новый раздел.

Связываем Lombok и Mapstruct

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

​​<properties>
   <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
   <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
Добавим недостающую зависимость:

<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok-mapstruct-binding</artifactId>
   <version>${lombok-mapstruct-binding.version}</version>
</dependency>
И обновим наш компайлер-плагин, чтобы он таки смог соединить Lombok и Mapstruct:

<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-compiler-plugin</artifactId>
   <version>3.5.1</version>
   <configuration>
       <source>1.8</source>
       <target>1.8</target>
       <annotationProcessorPaths>
           <path>
               <groupId>org.mapstruct</groupId>
               <artifactId>mapstruct-processor</artifactId>
               <version>${org.mapstruct.version}</version>
           </path>
           <path>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok</artifactId>
               <version>${lombok.version}</version>
           </path>
           <path>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok-mapstruct-binding</artifactId>
               <version>${lombok-mapstruct-binding.version}</version>
           </path>
       </annotationProcessorPaths>
   </configuration>
</plugin>
После этого должно все получиться. Еще раз скомпилируем наш проект. Но где же искать классы, которые сгенерировал Mapstruct? Они лежат в generated-sources: ${projectDir}/target/generated-sources/annotations/ Что такое Mapstruct и как правильно настроить его для модульного тестирования в SpringBoot приложениях. Часть 1 - 3Теперь, когда мы подготовились к тому, чтобы осознать мое разочарование из поста про Mapstruct, попробуем создать тесты на мапперы.

Пишем тесты на наши мапперы

Я создам быстрый и простой тест, который бы протестировал один из мапперов в случае, когда мы создаем интеграционный тест и не заморачиваемся о времени его прохождения:

LectureMapperTest


package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class LectureMapperTest {

   @Autowired
   private LectureMapper mapperUnderTest;

   @Test
   void shouldProperlyMapModelToDto() {
       //given
       LectureModel model = new LectureModel();
       model.setId(11L);
       model.setName("lecture name");

       //when
       LectureDTO dto = mapperUnderTest.toDTO(model);

       //then
       Assertions.assertNotNull(dto);
       Assertions.assertEquals(model.getId(), dto.getId());
       Assertions.assertEquals(model.getName(), dto.getName());
   }

   @Test
   void shouldProperlyMapDtoToModel() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(11L);
       dto.setName("lecture name");

       //when
       LectureModel model = mapperUnderTest.toModel(dto);

       //then
       Assertions.assertNotNull(model);
       Assertions.assertEquals(dto.getId(), model.getId());
       Assertions.assertEquals(dto.getName(), model.getName());
   }
}
Здесь аннотацией SpringBootTest мы запускаем весь applicationContext и уже из него при помощи аннотации Autowired извлекаем необходимый нам класс для тестирования. С точки зрения скорости и легкости написания теста — это очень хорошо. Тест успешно проходит, все хорошо. Но мы пойдем другой дорогой и напишем модульный тест на маппер, например, на LectureListMapper…

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl();

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
Так как реализации, которые генерирует Mapstruct лежат в одном класспасе, что и наш проект, то мы спокойно можем использовать их в наших тестах. Выглядит все прекрасно — никаких аннотаций, создаем самым простым способом класс, что нам нужен и все. Но когда мы запустим тест, то поймем, что он упадет и в консоли будет NullPointerException… Все потому, что реализация маппера LectureListMapper имеет вид:

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T21:46:12+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   @Autowired
   private LectureMapper lectureMapper;

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
Если мы посмотрим, то NPE (сокращение от NullPointerException), мы получаем как раз от переменной lectureMapper, которая оказывается не инициализирована. Но у нас в реализации нет и конструктора, при помощи которого мы бы могли инициализировать переменную. Здесь как раз закопана причина, почему Mapstruct реализовал маппер именно так! В Spring можно несколькими способами добавлять бины в классы, можно инъектировать их через поле вместе с аннотацией Autowired, как сделано выше, а можно инъектировать через конструктор. В такой проблемной ситуации я оказался на работе, когда нужно было оптимизировать время выполнения тестов. Я думал, что сделать с этим ничего нельзя и излил свою боль на своем телеграм-канале. И тут мне в комментариях помогли, сказали, что есть возможность настроить стратегию инъектирования. В интерфейсе Mapper есть поле injectionStrategy, которое как раз и принимает енам InjectionStrategy у которого два значения: FIELD и CONSTRUCTOR. Теперь, зная об этом, добавим в наши мапперы эту настройку, покажу на примере LectureListMapper:

@Mapper(componentModel = "spring", uses = LectureMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}
Выделил жирным часть, что добавил. Добавим эту опцию для всех остальных и перекомпилируем проект, чтобы сгенерировались мапперы уже с новой строкой. Когда это выполним, посмотрим как изменилась реализация маппера для LectureListMapper (выделил жирным ту часть, что нам нужна):

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T22:25:37+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   private final LectureMapper lectureMapper;

   @Autowired
   public LectureListMapperImpl(LectureMapper lectureMapper) {

       this.lectureMapper = lectureMapper;
   }

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
И вот теперь Mapstruct реализовал инъектирование маппера через конструктор. Собственно чего мы и добивались. Теперь наш тест перестанет компилироваться, обновим его и получим:

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl(new LectureMapperImpl());

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
Теперь, если мы запустим тест, то все сработает так как положено, так как в LectureListMapperImpl мы передаем необходимый ему LectureMapper… Победа! Вам не сложно, а мне приятно: Друзья, все как обычно, подписывайтесь на мой гитхаб-аккаунт, на телеграм-аккаунт. Там я выкладываю результат своей деятельности, есть реально полезные вещи) Особенно приглашаю вступить в группу обсуждений телеграм-канала. Так сложилось, что если у кого-то есть технический вопрос, там можно получить ответ. Такой формат интересен для всех, можно почитать, кто что умеет и набраться опыта.

Вывод

В рамках этой статьи мы познакомились с таким нужным и часто используемым продуктом как Mapstruct. Разобрали что это, зачем и как. На реальном примере пощупали, что можно делать и как можно менять. Также, разобрали как настроить инъектирование бинов через конструктор, чтобы была возможность нормально проводить тестирование мапперов. Коллеги из Mapstruct предоставили пользователям их продукта выбирать каким именно способом инъектировать мапперы, за что им несомненно спасибо. НО, несмотря на то, что Spring рекомендует инъектировать бины через конструктор, ребята из Mapstruct поставили по умолчанию инъектирование через поле. Почему так? Нет ответа. Я подозреваю, что могут быть причины о которых мы просто не знаем, и поэтому они так сделали. И чтобы узнать у них, я создал GitHub issue в их официальном репозитории продукта.