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

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

1. обзор

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

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

2. MapStruct и шаблон переносимого объекта

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

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

Таким образом, MapStruct решает проблему. Создание bean-картографов вручную занимает много времени. Библиотекаcan generate bean mapper classes automatically.

3. специалист

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


    org.mapstruct
    mapstruct-jdk8
    1.3.0.Beta2

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

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

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


    org.apache.maven.plugins
    maven-compiler-plugin
    3.5.1
    
        1.8
        1.8
        
            
                org.mapstruct
                mapstruct-processor
                1.3.0.Beta2
            
        
    

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

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

Luckily MapStruct has solid support for both Spring and CDI (Contexts and Dependency Injection).

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

5.1. Измените Mapper

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

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

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

Из нашего предыдущего примера MapStruct смог автоматически отобразить наши 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, который мы будем использовать для добавления атрибута target и source.

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

@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 project.

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. Измените Mapper

Здесь нам нужно добавить метод для преобразования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 reference guide.

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. Измените Mapper

Измените сопоставитель и укажите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
}

Сложная часть здесь - преобразовать сумму долларовBigDecimaltotalв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 toTransactionDTO(
      Collection transactions);
}

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

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

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

Поскольку мы уже реализовали метод сопоставления одиночногоTransactionin сTransactionDTO, мы ожидаем, чтоMapstructперестанет использовать его во втором методе. Будет сгенерировано следующее:

@Generated
class TransactionMapperImpl extends TransactionMapper {

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

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

        return list;
    }
}

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

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

Вот еще один способ настройки возможностей@Mapping с помощью аннотаций@BeforeMapping и@AfterMapping. The annotations are used to mark methods that are invoked right before and after the mapping logic.

Они очень полезны в сценариях, где нам может понадобиться этотbehavior to be applied to all mapped super-types.

Давайте посмотрим на пример, который отображает подтипыCar;ElectricCar, иBioDieselCar наCarDTO.

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

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

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

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

ПодтипыCar:

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 - это аннотация параметра, аннотируемая методомpopulates the target mapping DTO right before the mapping logic is executedin case of @BeforeMapping иright after in case of @AfterMapping.

10.3. Результат

CarsMapper defined above generatestheimplementation:

@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;
    }
}

Обратите внимание, какthe annotated methods invocations surround the mapping logic в реализации.

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

В последней версии MapStruct была анонсирована поддержка Lombok. So we can easily map a source entity and a destination using Lombok. с

Чтобы включить поддержку Lombok, нам нужно добавитьthe dependency в путь процессора аннотаций. Итак, теперь у нас есть mapstruct-процессор и Lombok в плагине компилятора Maven:


    org.apache.maven.plugins
    maven-compiler-plugin
    3.5.1
    
        1.8
        1.8
        
            
                org.mapstruct
                mapstruct-processor
                1.3.0.Beta2
            
            
                org.projectlombok
                lombok
            1.18.4
            
        
    

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

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

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

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

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

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

12. ПоддержкаdefaultExpression

Начиная с версии 1.3.0,we can use the defaultExpression attribute of the @Mapping annotation to specify an expression that determines the value of the destination field if the source field is null. дополняет существующие функции атрибутаdefaultValue.

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

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

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

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

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

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