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, поэтому его должно быть легко импортировать и запускать как есть.