Перейти к основному содержимому

Краткое руководство по MapStruct

· 12 мин. чтения

1. Обзор

В этом руководстве мы рассмотрим использование MapStruct , который, проще говоря, является картографом Java Bean.

Этот API содержит функции, которые автоматически сопоставляются между двумя компонентами Java Bean. С MapStruct нам нужно только создать интерфейс, и библиотека автоматически создаст конкретную реализацию во время компиляции.

2. MapStruct и передача шаблона объекта

В большинстве приложений вы заметите много стандартного кода, преобразующего POJO в другие POJO.

Например, общий тип преобразования происходит между сущностями, поддерживаемыми постоянством, и DTO, которые выходят на сторону клиента.

Итак, вот проблема, которую решает MapStruct: ручное создание модулей сопоставления компонентов требует много времени. Но библиотека может автоматически генерировать классы сопоставления компонентов.

3. Мавен

Давайте добавим следующую зависимость в наш Maven pom.xml :

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>

Последний стабильный выпуск MapStruct и его процессор доступны в центральном репозитории Maven.

Давайте также добавим раздел annotationProcessorPaths в часть конфигурации плагина maven-compiler- plugin.

Обработчик mapstruct-processor используется для генерации реализации картографа во время сборки:

<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>

4. Базовое картографирование

4.1. Создание POJO

Давайте сначала создадим простой Java POJO:

public class SimpleSource {
private String name;
private String description;
// getters and setters
}

public class SimpleDestination {
private String name;
private String description;
// getters and setters
}

4.2. Интерфейс картографа

@Mapper
public interface SimpleSourceDestinationMapper {
SimpleDestination sourceToDestination(SimpleSource source);
SimpleSource destinationToSource(SimpleDestination destination);
}

Обратите внимание, что мы не создавали класс реализации для нашего SimpleSourceDestinationMapper, потому что MapStruct создает его за нас.

4.3. Новый картограф

Мы можем запустить обработку MapStruct, выполнив mvn clean install .

Это сгенерирует класс реализации в /target/generated-sources/annotations/ .

Вот класс, который автоматически создает для нас MapStruct:

public class SimpleSourceDestinationMapperImpl
implements SimpleSourceDestinationMapper {
@Override
public SimpleDestination sourceToDestination(SimpleSource source) {
if ( source == null ) {
return null;
}
SimpleDestination simpleDestination = new SimpleDestination();
simpleDestination.setName( source.getName() );
simpleDestination.setDescription( source.getDescription() );
return simpleDestination;
}
@Override
public SimpleSource destinationToSource(SimpleDestination destination){
if ( destination == null ) {
return null;
}
SimpleSource simpleSource = new SimpleSource();
simpleSource.setName( destination.getName() );
simpleSource.setDescription( destination.getDescription() );
return simpleSource;
}
}

4.4. Прецедент

Наконец, со всем сгенерированным, давайте напишем тестовый пример, показывающий, что значения в SimpleSource соответствуют значениям в SimpleDestination :

public class SimpleSourceDestinationMapperIntegrationTest {
private SimpleSourceDestinationMapper mapper
= Mappers.getMapper(SimpleSourceDestinationMapper.class);
@Test
public void givenSourceToDestination_whenMaps_thenCorrect() {
SimpleSource simpleSource = new SimpleSource();
simpleSource.setName("SourceName");
simpleSource.setDescription("SourceDescription");
SimpleDestination destination = mapper.sourceToDestination(simpleSource);

assertEquals(simpleSource.getName(), destination.getName());
assertEquals(simpleSource.getDescription(),
destination.getDescription());
}
@Test
public void givenDestinationToSource_whenMaps_thenCorrect() {
SimpleDestination destination = new SimpleDestination();
destination.setName("DestinationName");
destination.setDescription("DestinationDescription");
SimpleSource source = mapper.destinationToSource(destination);
assertEquals(destination.getName(), source.getName());
assertEquals(destination.getDescription(),
source.getDescription());
}
}

5. Сопоставление с внедрением зависимостей

Затем давайте получим экземпляр преобразователя в MapStruct, просто вызвав Mappers.getMapper(YourClass.class) .

Конечно, это очень ручной способ получения экземпляра. Тем не менее, гораздо лучшей альтернативой является внедрение картографа непосредственно туда, где он нам нужен (если в нашем проекте используется какое-либо решение для внедрения зависимостей).

К счастью, MapStruct имеет надежную поддержку как Spring, так и CDI ( контексты и внедрение зависимостей ).

Чтобы использовать Spring IoC в нашем картографе, нам нужно добавить атрибут componentModel в @Mapper со значением spring , а для CDI это будет cdi .

5.1. Изменить картограф

Добавьте следующий код в SimpleSourceDestinationMapper :

@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper

5.2. Внедрить компоненты Spring в Mapper

Иногда нам нужно использовать другие компоненты Spring внутри нашей логики сопоставления. В этом случае мы должны использовать абстрактный класс вместо интерфейса :

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService

Затем мы можем легко внедрить нужный компонент, используя известную аннотацию @Autowired, и использовать его в нашем коде:

@Mapper(componentModel = "spring")
public abstract class SimpleDestinationMapperUsingInjectedService {

@Autowired
protected SimpleService simpleService;

@Mapping(target = "name", expression = "java(simpleService.enrichName(source.getName()))")
public abstract SimpleDestination sourceToDestination(SimpleSource source);
}

Мы должны помнить, что не следует делать инжектируемый bean-компонент приватным! Это связано с тем, что MapStruct должен получить доступ к объекту в сгенерированном классе реализации.

6. Сопоставление полей с разными именами полей

В нашем предыдущем примере MapStruct смог автоматически сопоставить наши bean-компоненты, потому что они имеют одинаковые имена полей. Итак, что, если bean-компонент, который мы собираемся сопоставить, имеет другое имя поля?

В этом примере мы создадим новый компонент с именами Employee и EmployeeDTO .

6.1. Новые POJO

public class EmployeeDTO {
private int employeeId;
private String employeeName;
// getters and setters
}
public class Employee {
private int id;
private String name;
// getters and setters
}

6.2. Интерфейс картографа

При сопоставлении разных имен полей нам нужно будет настроить его исходное поле на целевое поле, и для этого нам нужно будет добавить аннотацию @Mappings . Эта аннотация принимает массив аннотаций @Mapping , которые мы будем использовать для добавления целевого и исходного атрибутов.

В MapStruct мы также можем использовать точечную нотацию для определения члена компонента:

@Mapper
public interface EmployeeMapper {
@Mappings({
@Mapping(target="employeeId", source="entity.id"),
@Mapping(target="employeeName", source="entity.name")
})
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mappings({
@Mapping(target="id", source="dto.employeeId"),
@Mapping(target="name", source="dto.employeeName")
})
Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

6.3. Прецедент

Опять же, нам нужно проверить, совпадают ли значения исходного и целевого объекта:

@Test
public void givenEmployeeDTOwithDiffNametoEmployee_whenMaps_thenCorrect() {
EmployeeDTO dto = new EmployeeDTO();
dto.setEmployeeId(1);
dto.setEmployeeName("John");

Employee entity = mapper.employeeDTOtoEmployee(dto);

assertEquals(dto.getEmployeeId(), entity.getId());
assertEquals(dto.getEmployeeName(), entity.getName());
}

Больше тестовых случаев можно найти в проекте GitHub .

7. Сопоставление бинов с дочерними бинами

Далее мы покажем, как сопоставить компонент со ссылками на другие компоненты.

7.1. Изменить POJO

Давайте добавим новую ссылку на bean-компонент к объекту Employee :

public class EmployeeDTO {
private int employeeId;
private String employeeName;
private DivisionDTO division;
// getters and setters omitted
}
public class Employee {
private int id;
private String name;
private Division division;
// getters and setters omitted
}
public class Division {
private int id;
private String name;
// default constructor, getters and setters omitted
}

7.2. Изменить картограф

Здесь нам нужно добавить метод для преобразования Division в DivisionDTO и наоборот; если MapStruct обнаружит, что тип объекта необходимо преобразовать, а метод для преобразования существует в том же классе, он будет использовать его автоматически.

Давайте добавим это в маппер:

DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

7.3. Изменить тестовый пример

Давайте изменим и добавим несколько тестовых случаев к существующему:

@Test
public void givenEmpDTONestedMappingToEmp_whenMaps_thenCorrect() {
EmployeeDTO dto = new EmployeeDTO();
dto.setDivision(new DivisionDTO(1, "Division1"));
Employee entity = mapper.employeeDTOtoEmployee(dto);
assertEquals(dto.getDivision().getId(),
entity.getDivision().getId());
assertEquals(dto.getDivision().getName(),
entity.getDivision().getName());
}

8. Сопоставление с преобразованием типов

MapStruct также предлагает несколько готовых неявных преобразований типов, и для нашего примера мы попытаемся преобразовать дату String в фактический объект Date .

Дополнительные сведения о неявном преобразовании типов см . в справочном руководстве по MapStruct .

8.1. Изменить бобы

Добавляем дату начала для нашего сотрудника:

public class Employee {
// other fields
private Date startDt;
// getters and setters
}
public class EmployeeDTO {
// other fields
private String employeeStartDt;
// getters and setters
}

8.2. Изменить картограф

Мы модифицируем маппер и предоставляем dateFormat для нашей даты начала:

@Mappings({
@Mapping(target="employeeId", source = "entity.id"),
@Mapping(target="employeeName", source = "entity.name"),
@Mapping(target="employeeStartDt", source = "entity.startDt",
dateFormat = "dd-MM-yyyy HH:mm:ss")})
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mappings({
@Mapping(target="id", source="dto.employeeId"),
@Mapping(target="name", source="dto.employeeName"),
@Mapping(target="startDt", source="dto.employeeStartDt",
dateFormat="dd-MM-yyyy HH:mm:ss")})
Employee employeeDTOtoEmployee(EmployeeDTO dto);

8.3. Изменить тестовый пример

Давайте добавим еще несколько тестовых случаев, чтобы убедиться в правильности преобразования:

private static final String DATE_FORMAT = "dd-MM-yyyy HH:mm:ss";
@Test
public void givenEmpStartDtMappingToEmpDTO_whenMaps_thenCorrect() throws ParseException {
Employee entity = new Employee();
entity.setStartDt(new Date());
EmployeeDTO dto = mapper.employeeToEmployeeDTO(entity);
SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);

assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
entity.getStartDt().toString());
}
@Test
public void givenEmpDTOStartDtMappingToEmp_whenMaps_thenCorrect() throws ParseException {
EmployeeDTO dto = new EmployeeDTO();
dto.setEmployeeStartDt("01-04-2016 01:00:00");
Employee entity = mapper.employeeDTOtoEmployee(dto);
SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);

assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
entity.getStartDt().toString());
}

9. Сопоставление с абстрактным классом

Иногда мы можем захотеть настроить наш преобразователь таким образом, чтобы он превышал возможности @Mapping.

Например, в дополнение к преобразованию типов мы можем каким-то образом преобразовать значения, как в нашем примере ниже.

В таких случаях мы можем создать абстрактный класс и реализовать методы, которые мы хотим настроить, и оставить абстрактными те, которые должны быть сгенерированы MapStruct.

9.1. Базовая модель

В этом примере мы будем использовать следующий класс:

public class Transaction {
private Long id;
private String uuid = UUID.randomUUID().toString();
private BigDecimal total;

//standard getters
}

и соответствующий DTO:

public class TransactionDTO {

private String uuid;
private Long totalInCents;

// standard getters and setters
}

Сложная часть здесь заключается в преобразовании общей суммы BigDecimal в долларах в Long totalInCents .

9.2. Определение картографа

Мы можем добиться этого, создав наш Mapper как абстрактный класс:

@Mapper
abstract class TransactionMapper {

public TransactionDTO toTransactionDTO(Transaction transaction) {
TransactionDTO transactionDTO = new TransactionDTO();
transactionDTO.setUuid(transaction.getUuid());
transactionDTO.setTotalInCents(transaction.getTotal()
.multiply(new BigDecimal("100")).longValue());
return transactionDTO;
}

public abstract List<TransactionDTO> toTransactionDTO(
Collection<Transaction> transactions);
}

Здесь мы реализовали наш полностью настраиваемый метод сопоставления для преобразования одного объекта.

С другой стороны, мы оставили метод, предназначенный для сопоставления коллекции с абстрактным списком , поэтому MapStruct реализует его за нас.

9.3. Сгенерированный результат

Поскольку мы уже реализовали метод для сопоставления одной транзакции с TransactionDTO , мы ожидаем, что MapStruct будет использовать его во втором методе.

Будет сгенерировано следующее:

@Generated
class TransactionMapperImpl extends TransactionMapper {

@Override
public List<TransactionDTO> toTransactionDTO(Collection<Transaction> transactions) {
if ( transactions == null ) {
return null;
}

List<TransactionDTO> list = new ArrayList<>();
for ( Transaction transaction : transactions ) {
list.add( toTransactionDTO( transaction ) );
}

return list;
}
}

Как видно из строки 12, MapStruct использует нашу реализацию в сгенерированном методе.

10. Аннотации до и после картирования

Вот еще один способ настроить возможности @Mapping с помощью аннотаций @BeforeMapping и @AfterMapping . Аннотации используются для обозначения методов, которые вызываются непосредственно перед и после логики сопоставления.

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

Давайте рассмотрим пример, который сопоставляет подтипы Car ElectricCar и BioDieselCar с CarDTO .

При сопоставлении мы хотели бы сопоставить понятие типов с полем перечисления FuelType в DTO. Затем, после завершения сопоставления, мы хотели бы изменить имя DTO на верхний регистр.

10.1. Базовая модель

Мы будем использовать следующие классы:

public class Car {
private int id;
private String name;
}

Подтипы автомобилей :

public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}

CarDTO с типом поля перечисления FuelType :

public class CarDTO {
private int id;
private String name;
private FuelType fuelType;
}
public enum FuelType {
ELECTRIC, BIO_DIESEL
}

10.2. Определение картографа

Теперь давайте продолжим и напишем наш абстрактный класс преобразователя, который сопоставляет Car с CarDTO :

@Mapper
public abstract class CarsMapper {
@BeforeMapping
protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
if (car instanceof ElectricCar) {
carDto.setFuelType(FuelType.ELECTRIC);
}
if (car instanceof BioDieselCar) {
carDto.setFuelType(FuelType.BIO_DIESEL);
}
}

@AfterMapping
protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
carDto.setName(carDto.getName().toUpperCase());
}

public abstract CarDTO toCarDto(Car car);
}

@MappingTarget — это аннотация параметра, которая заполняет DTO целевого сопоставления непосредственно перед выполнением логики сопоставления в случае @BeforeMapping и сразу после в случае `` аннотированного метода @AfterMapping .

10.3. Результат

Определенный выше CarsMapper генерирует реализацию : ****

@Generated
public class CarsMapperImpl extends CarsMapper {

@Override
public CarDTO toCarDto(Car car) {
if (car == null) {
return null;
}

CarDTO carDTO = new CarDTO();

enrichDTOWithFuelType(car, carDTO);

carDTO.setId(car.getId());
carDTO.setName(car.getName());

convertNameToUpperCase(carDTO);

return carDTO;
}
}

Обратите внимание, как вызовы аннотированных методов окружают логику сопоставления в реализации.

11. Поддержка Ломбока

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

Чтобы включить поддержку Lombok, нам нужно добавить зависимость в пути процессора аннотаций. Начиная с версии Lombok 1.18.16, мы также должны добавить зависимость от lombok-mapstruct-binding . Теперь у нас есть mapstruct-processor, а также Lombok в плагине компилятора Maven:

<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>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>

Давайте определим исходный объект, используя аннотации Lombok:

@Getter
@Setter
public class Car {
private int id;
private String name;
}

И целевой объект передачи данных:

@Getter
@Setter
public class CarDTO {
private int id;
private String name;
}

Интерфейс картографа для этого остается таким же, как в нашем предыдущем примере:

@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
CarDTO carToCarDTO(Car car);
}

12. Поддержка выражения по умолчанию

Начиная с версии 1.3.0, мы можем использовать атрибут defaultExpression аннотации @Mapping , чтобы указать выражение, определяющее значение поля назначения, если исходное поле имеет значение null . Это в дополнение к существующей функциональности атрибута defaultValue .

Исходный объект:

public class Person {
private int id;
private String name;
}

Объект передачи данных назначения:

public class PersonDTO {
private int id;
private String name;
}

Если поле id исходного объекта равно null , мы хотим сгенерировать случайный идентификатор и назначить его получателю, сохраняя другие значения свойств как есть:

@Mapper
public interface PersonMapper {
PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

@Mapping(target = "id", source = "person.id",
defaultExpression = "java(java.util.UUID.randomUUID().toString())")
PersonDTO personToPersonDTO(Person person);
}

Давайте добавим тестовый пример для проверки выполнения выражения:

@Test
public void givenPersonEntitytoPersonWithExpression_whenMaps_thenCorrect()
Person entity = new Person();
entity.setName("Micheal");
PersonDTO personDto = PersonMapper.INSTANCE.personToPersonDTO(entity);
assertNull(entity.getId());
assertNotNull(personDto.getId());
assertEquals(personDto.getName(), entity.getName());
}

13. Заключение

В этой статье представлено введение в MapStruct. Мы представили большую часть основ библиотеки сопоставления и способы ее использования в наших приложениях.

Реализацию этих примеров и тестов можно найти в проекте GitHub . Это проект Maven, поэтому его должно быть легко импортировать и запускать как есть.