Кратко ръководство за MapStruct

1. Общ преглед

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

Този API съдържа функции, които автоматично картографират между два Java Bean. С MapStruct трябва само да създадем интерфейс и библиотеката автоматично ще създаде конкретна реализация по време на компилация.

2. MapStruct и прехвърляне на модел на обект

За повечето приложения ще забележите много типови кодове, конвертиращи POJO в други POJO.

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

Така че това е проблемът, който MapStruct решава - ръчното създаване на картографиращи компоненти отнема много време. Библиотеката може да генерира класове боб Mapper автоматично .

3. Мейвън

Нека добавим зависимостта по-долу в нашия Maven pom.xml :

 org.mapstruct mapstruct 1.3.1.Final 

Последната стабилна версия на Mapstruct и неговият процесор се предлагат от Централното хранилище на Maven.

Нека също добавим секцията annotationProcessorPaths към конфигурационната част на приставката maven-compiler- plugin.

В mapstruct-процесор се използва за генериране на изпълнението на картограф по време на строеж:

 org.apache.maven.plugins maven-compiler-plugin 3.5.1  1.8 1.8   org.mapstruct mapstruct-processor 1.3.1.Final    

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

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

Забележете, че не създадохме клас на внедряване за нашия SimpleSourceDestinationMapper - защото MapStruct го създава за нас.

4.3. Новият картограф

Можем да задействаме обработката на MapStruct, като изпълним mvn чиста инсталация .

Това ще генерира клас на внедряване под / 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. Картиране с инжектиране на зависимост

След това нека да получим екземпляр на mapper в MapStruct, като просто извикаме Mappers.getMapper (YourClass.class).

Разбира се, това е много ръчен начин за получаване на екземпляра - много по-добра алтернатива би било инжектирането на mapper директно там, където имаме нужда (ако нашият проект използва решение за инжектиране на зависимост).

За щастие MapStruct има солидна поддръжка както за Spring, така и за CDI ( Contexts and Dependency Injection ).

За да използваме Spring IoC в нашия картограф, трябва да добавим атрибута componentModel към @Mapper със стойността spring и за CDI ще бъде cdi .

5.1. Променете Mapper

Добавете следния код към SimpleSourceDestinationMapper :

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

6. Картиране на полета с различни имена на полета

От нашия предишен пример MapStruct успя да картографира нашите зърна автоматично, тъй като те имат еднакви имена на полета. И така, ако боб, който предстои да картографираме, има различно име на поле?

За нашия пример ще създадем нов боб, наречен Employee and 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. Интерфейсът на Mapper

Когато картографираме различни имена на полета, ще трябва да конфигурираме неговото поле източник към целевото поле и за това ще трябва да добавим анотация @Mappings . Тази анотация приема масив от анотация @Mapping, който ще използваме за добавяне на атрибута target и source.

В 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

Нека добавим нова препратка към боб към обекта 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.

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

Modify the mapper and provide the dateFormat for our start date:

@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. Modify the Test Case

Let’s add a few more test case to verify the conversion is correct:

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 With an Abstract Class

Sometimes, we may want to customize our mapper in a way which exceeds @Mapping capabilities.

For example, in addition to type conversion, we may want to transform the values in some way as in our example below.

In such case, we can create an abstract class and implement methods we want to have customized and leave abstract those, that should be generated by MapStruct.

9.1. Basic Model

In this example, we'll use the following class:

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

and a matching DTO:

public class TransactionDTO { private String uuid; private Long totalInCents; // standard getters and setters }

The tricky part here is converting the BigDecimaltotalamount of dollars into a Long totalInCents.

9.2. Defining a Mapper

We can achieve this by creating our Mapper as an abstract class:

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

Here, we've implemented our fully customized mapping method for a single object conversion.

On the other hand, we left the method which is meant to map Collectionto a Listabstract, so MapStruct will implement it for us.

9.3. Generated Result

As we have already implemented the method to map single Transactioninto a TransactionDTO, we expect Mapstructto use it in the second method. The following will be generated:

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

As we can see in line 12, MapStruct uses our implementation in the method, that it generated.

10. Before-mapping and After-mapping Annotations

Here's another way of customizing @Mapping capabilities by using @BeforeMapping and @AfterMapping annotations. The annotations are used to mark methods that are invoked right before and after the mapping logic.

They are quite useful in scenarios where we might want this behavior to be applied to all mapped super-types.

Let's take a look at an example that maps the sub-types of Car; ElectricCar, and BioDieselCar, to CarDTO.

While mapping we would like to map the notion of types to the FuelType enum field in the DTO, and after the mapping is done we'd like to change the name of the DTO to uppercase.

10.1. Basic Model

In this example, we’ll use the following classes:

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

Sub-types of Car:

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

The CarDTO with an enum field type FuelType:

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

10.2. Defining the Mapper

Now let's proceed and write our abstract mapper class, that maps Car to 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 is a parameter annotation that populates the target mapping DTO right before the mapping logic is executedin case of @BeforeMapping and right after in case of @AfterMapping annotated method.

10.3. Result

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

Notice how the annotated methods invocations surround the mapping logic in the implementation.

11. Support for Lombok

In the recent version of MapStruct, Lombok support was announced. So we can easily map a source entity and a destination using Lombok.

To enable Lombok support we need to add the dependency in the annotation processor path. So now we have the mapstruct-processor as well as Lombok in the Maven compiler plugin:

 org.apache.maven.plugins maven-compiler-plugin 3.5.1  1.8 1.8   org.mapstruct mapstruct-processor 1.3.1.Final   org.projectlombok lombok 1.18.4    

Let's define the source entity using Lombok annotations:

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

And the destination data transfer object:

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

The mapper interface for this remains similar to our previous example:

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

12. Support for defaultExpression

Starting with version 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. This is in addition to the existing defaultValue attribute functionality.

The source entity:

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

The destination data transfer object:

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

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

@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, така че трябва да е лесно да се импортира и да се изпълнява както е.