MapStruct usage guide

MapStruct usage guide

Introduction

As microservices and distributed applications rapidly occupy the development arena, data integrity and security are more important than ever. Among these loosely coupled systems, secure communication channels and limited data transmission are the most important. Most of the time, end users or services do not need to access all the data in the model, but only need to access certain specific parts.

Data Transfer Objects (DTO) are often used in these applications. DTO is just an object that holds the requested information in another object. Normally, this information is a limited part. For example, there are often conversions between entities defined in the persistence layer and DTOs sent to the client. Since DTO is a reflection of the original object, the mapper between these classes plays a key role in the conversion process.

This is the problem that MapStruct solves: manually creating a bean mapper is very time consuming. But the library can automatically generate Bean mapper classes.

In this article, we will delve into MapStruct .

MapStruct

MapStruct is an open source Java-based code generator used to create an extended mapper that implements conversion between Java Beans. Using MapStruct, we only need to create an interface, and the library will automatically create a specific mapping implementation during the compilation process through annotations, which greatly reduces the amount of sample code that usually needs to be written by hand.

MapStruct dependency

If you use Maven, you can install MapStruct by introducing dependencies:

< Dependencies > < dependency > < the groupId > org.mapstruct </the groupId > < the artifactId > mapstruct </the artifactId > < Version > $ {org.mapstruct.version} </Version > </dependency > </Dependencies > copy the code

This dependency will import the core annotations of MapStruct. Since MapStruct works at compile time and will be integrated into build tools like Maven and Gradle, we must also add a plug-in to the <build in/> tag

maven-compiler-plugin
And add to its configuration
annotationProcessorPaths
, The plug-in will generate the corresponding code when it is built.

< 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 > ${org.mapstruct.version} </version > </path > </annotationProcessorPaths > </configuration > </plugin > </plugins > </Build > copy the code<

If you use Gradle, installing MapStruct will be easier:

plugins { ID 'net.ltgt.apt' Version '0.20' } apply plugin: 'net.ltgt.apt-idea' apply plugin: 'net.ltgt.apt-eclipse' dependencies { compile "org.mapstruct:mapstruct:${mapstructVersion}" annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}" } Copy code

net.ltgt.apt
The plug-in will take care of the annotations. You can enable the plugin according to the IDE you are using
apt-idea
or
apt-eclipse
Plug-in.

The latest stable version of MapStruct and its processors are available from the Maven Central Repository .

Mapping

Basic mapping

Let's start with some basic mapping. We will create a Doctor object and a DoctorDto. For convenience, their attribute fields all use the same name:

public class Doctor { private int id; private String name; //getters and setters or builder } Copy code
public class DoctorDto { private int id; private String name; //getters and setters or builder } Copy code

Now, in order to map between these two, we have to create a

DoctorMapper
interface. Use this interface
@Mapper
Note, MapStruct will know that this is a mapper between the two classes.

@Mapper public interface DoctorMapper { DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); DoctorDto toDto (Doctor doctor) ; } Copy code

This code creates a

DoctorMapper
Instance of type
INSTANCE
, After generating the corresponding implementation code, this is the "entry" we call.

We defined in the interface

toDto()
Method, which receives a
Doctor
The instance is a parameter and returns a
DoctorDto
Instance. This is enough to let MapStruct know that we want to put a
Doctor
Instance maps to a
DoctorDto
Instance.

When we build/compile the application, the MapStruct annotation processor plug-in will recognize the DoctorMapper interface and generate an implementation class for it.

public class DoctorMapperImpl implements DoctorMapper { @Override public DoctorDto toDto (Doctor doctor) { if (doctor == null ) { return null ; } DoctorDtoBuilder doctorDto = DoctorDto.builder(); doctorDto.id(doctor.getId()); doctorDto.name(doctor.getName()); return doctorDto.build(); } } Copy code

DoctorMapperImpl
The class contains a
toDto()
Method, will our
Doctor
The attribute value is mapped to
DoctorDto
In the attribute field. If you want
Doctor
Instance maps to a
DoctorDto
Examples can be written like this:

DoctorDto doctorDto = DoctorMapper.INSTANCE.toDto(doctor); Copy code

Note : You may have also noticed the

DoctorDtoBuilder
. Because the builder code is often relatively long, for the sake of brevity, the implementation code of the builder mode is omitted here. If your class contains Builder, MapStruct will try to use it to create an instance; if not, MapStruct will pass
new
The keyword is instantiated.

Mapping between different fields

Usually, the field names of the model and DTO are not exactly the same. As team members specify their own names, and for different invocation services, developers choose different packaging methods for the returned information, and the names may vary slightly.

MapStruct passed

@Mapping
Annotations provide support for this type of situation.

Different attribute names

We update first

Doctor
Class, add an attribute
specialty
:

public class Doctor { private int id; private String name; private String specialty; //getters and setters or builder } Copy code

in

DoctorDto
Add one to the class
specialization
Attributes:

public class DoctorDto { private int id; private String name; private String specialization; //getters and setters or builder } Copy code

Now we need to make

DoctorMapper
Know the inconsistency here. We can use
@Mapping
Annotation and set its internal
source
with
target
The tags respectively point to two inconsistent fields.

@Mapper public interface DoctorMapper { DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); @Mapping(source = "doctor.specialty", target = "specialization") DoctorDto toDto (Doctor doctor) ; } Copy code

The meaning of this annotation code is:

Doctor
middle
specialty
Field corresponds to
DoctorDto
Category
specialization
.

After compilation, the following implementation code will be generated:

public class DoctorMapperImpl implements DoctorMapper { @Override public DoctorDto toDto (Doctor doctor) { if (doctor == null ) { return null ; } DoctorDtoBuilder doctorDto = DoctorDto.builder(); doctorDto.specialization(doctor.getSpecialty()); doctorDto.id(doctor.getId()); doctorDto.name(doctor.getName()); return doctorDto.build(); } } Copy code

Multiple source classes

Sometimes, a single class is not enough to build a DTO. We may want to aggregate the values in multiple classes into a DTO for end users to use. This can also be done by

@Mapping
Set the appropriate flag in the annotation to complete.

Let's create another object first

Education
:

public class Education { private String degreeName; private String institute; private Integer yearOfPassing; //getters and setters or builder } Copy code

And then to

DoctorDto
Add a new field in:

public class DoctorDto { private int id; private String name; private String degree; private String specialization; //getters and setters or builder } Copy code

Next, change

DoctorMapper
The interface is updated to the following code:

@Mapper public interface DoctorMapper { DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); @Mapping(source = "doctor.specialty", target = "specialization") @Mapping(source = "education.degreeName", target = "degree") DoctorDto toDto (Doctor doctor, Education education) ; } Copy code

We added another

@Mapping
Comment and put it
source
Set as
Education
Category
degreeName
,will
target
Set as
DoctorDto
Category
degree
Field.

in case

Education
Class and
Doctor
The class contains fields with the same name, and we must let the mapper know which one to use, otherwise it will throw an exception. For example, if both models contain a
id
Field, we have to choose which class
id
Mapped to DTO attributes.

Sub-object mapping

In most cases, POJO will not only contain basic data types, which will often contain other types. For example, a

Doctor
There will be multiple patient classes in the class:

public class Patient { private int id; private String name; //getters and setters or builder } Copy code

Add a patient list in Doctor

List
:

public class Doctor { private int id; private String name; private String specialty; private List<Patient> patientList; //getters and setters or builder } Copy code

because

Patient
Need to convert, create a corresponding DTO for it:

public class PatientDto { private int id; private String name; //getters and setters or builder } Copy code

Finally, in

DoctorDto
Add a store in
PatientDto
list of:

public class DoctorDto { private int id; private String name; private String degree; private String specialization; private List<PatientDto> patientDtoList; //getters and setters or builder } Copy code

Modifying

DoctorMapper
Before, we first create a support
Patient
with
PatientDto
Converted mapper interface:

@Mapper public interface PatientMapper { PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class); PatientDto toDto (Patient patient) ; } Copy code

This is a basic mapper that only handles a few basic data types.

Then, let s modify

DoctorMapper
Process the patient list:

@Mapper(uses = {PatientMapper.class}) public interface DoctorMapper { DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); @Mapping(source = "doctor.patientList", target = "patientDtoList") @Mapping(source = "doctor.specialty", target = "specialization") DoctorDto toDto (Doctor doctor) ; } Copy code

Because we have to deal with another class that needs to be mapped, so here is set

@Mapper
Annotated
uses
Logo, so now
@Mapper
You can use another
@Mapper
Mapper. We only added one here, but you can add as many classes/mappers as you want here.

We have added

uses
Logo, so we are
DoctorMapper
When the interface generation mapper is implemented, MapStruct will also
Patient
The model is converted into
PatientDto
Because we have already registered for this task
PatientMapper
.

Compile and view the latest code you want to implement:

public class DoctorMapperImpl implements DoctorMapper { private final PatientMapper patientMapper = Mappers.getMapper( PatientMapper.class ); @Override public DoctorDto toDto (Doctor doctor) { if (doctor == null ) { return null ; } DoctorDtoBuilder doctorDto = DoctorDto.builder(); doctorDto.patientDtoList( patientListToPatientDtoList(doctor.getPatientList())); doctorDto.specialization( doctor.getSpecialty() ); doctorDto.id( doctor.getId() ); doctorDto.name( doctor.getName() ); return doctorDto.build(); } protected List<PatientDto> patientListToPatientDtoList (List<Patient> list) { if (list == null ) { return null ; } List<PatientDto> list1 = new ArrayList<PatientDto>( list.size() ); for (Patient patient: list) { list1.add( patientMapper.toDto( patient) ); } return list1; } } Copy code

Obviously, except

toDto()
In addition to the mapping method, a new mapping method was added to the final implementation
patientListToPatientDtoList()
. This method was added without explicit definition, just because we put
PatientMapper
Added to
DoctorMapper
in.

This method will traverse a

Patient
List, convert each element to
PatientDto
And add the converted object to
DoctorDto
In the list in the object.

Update existing instance

Sometimes, we want to update the attributes in a model with the latest value of DTO for the target object (in our example,

DoctorDto
)use
@MappingTarget
Annotation, you can update the existing instance.

@Mapper(uses = {PatientMapper.class}) public interface DoctorMapper { DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); @Mapping(source = "doctorDto.patientDtoList", target = "patientList") @Mapping(source = "doctorDto.specialization", target = "specialty") void updateModel (DoctorDto doctorDto, @MappingTarget Doctor doctor) ; } Copy code

Regenerate the implementation code, you can get

updateModel()
method:

public class DoctorMapperImpl implements DoctorMapper { @Override public void updateModel (DoctorDto doctorDto, Doctor doctor) { if (doctorDto == null ) { return ; } if (doctor.getPatientList() != null ) { List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList()); if (list != null ) { doctor.getPatientList().clear(); doctor.getPatientList().addAll(list); } else { doctor.setPatientList( null ); } } else { List<Patient> list = patientDtoListToPatientList(doctorDto.getPatientDtoList()); if (list != null ) { doctor.setPatientList(list); } } doctor.setSpecialty(doctorDto.getSpecialization()); doctor.setId(doctorDto.getId()); doctor.setName(doctorDto.getName()); } } Copy code

It is worth noting that since the patient list is a sub-entity in this model, the patient list will also be updated.

Data type conversion

Data type mapping

MapStruct support

source
with
target
Data type conversion between attributes. It also provides automatic conversion between basic types and their corresponding packaging classes.

Automatic type conversion applies to:

  • Between the basic types and their corresponding packaging classes. such as,
    int
    with
    Integer
    ,
    float
    with
    Float
    ,
    long
    with
    Long
    ,
    boolean
    with
    Boolean
    Wait.
  • Between any basic type and any packaging class. Such as
    int
    with
    long
    ,
    byte
    with
    Integer
    Wait.
  • All basic types and packaging
    String
    between. Such as
    boolean
    with
    String
    ,
    Integer
    with
    String
    ,
    float
    with
    String
    Wait.
  • Enum and
    String
    between.
  • Java large number types (
    java.math.BigInteger
    ,
    java.math.BigDecimal
    ) And Java basic types (including its packaging classes) and
    String
    between.
  • For other conditions, please refer to the official MapStruct documentation .

Therefore, in the process of generating mapper code, if the source field and the target field fall into any of the above situations, MapStrcut will handle the type conversion by itself.

We modify

PatientDto
, Add a
dateofBirth
Field:

public class PatientDto { private int id; private String name; private LocalDate dateOfBirth; //getters and setters or builder } Copy code

On the other hand, join

Patient
There is one in the object
String
Type of
dateOfBirth
:

public class Patient { private int id; private String name; private String dateOfBirth; //getters and setters or builder } Copy code

Create a mapper between the two:

@Mapper public interface PatientMapper { @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy") Patient toModel (PatientDto patientDto) ; } Copy code

When converting the date, we can also use

dateFormat
Set the format statement. The generated implementation code is roughly as follows:

public class PatientMapperImpl implements PatientMapper { @Override public Patient toModel (PatientDto patientDto) { if (patientDto == null ) { return null ; } PatientBuilder patient = Patient.builder(); if (patientDto.getDateOfBirth() != null ) { patient.dateOfBirth(DateTimeFormatter.ofPattern( "dd/MMM/yyyy" ) .format(patientDto.getDateOfBirth())); } patient.id(patientDto.getId()); patient.name(patientDto.getName()); return patient.build(); } } Copy code

As you can see, here is used

dateFormat
The date format of the declaration. If we do not declare the format, MapStruct will use
LocalDate
The default format is roughly as follows:

if (patientDto.getDateOfBirth() != null ) { patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE .format(patientDto.getDateOfBirth())); } Copy code

Digital format conversion

As you can see in the above example, when the date is converted, you can pass

dateFormat
The flag specifies the format of the date.

In addition, for digital conversion, you can also use

numberFormat
Specify the display format:

//digital format conversion example @Mapping (source = "price", target = "price", numberFormat = "$ #. 00") Copy the code

Enumeration mapping

Enumeration mapping works in the same way as field mapping. MapStruct will map the enumeration with the same name, this is no problem. However, for enumerations with different names, we need to use

@ValueMapping
annotation. Again, this is the same as the normal type
@Mapping
The annotations are also similar.

We first create two enumerations. the first is

PaymentType
:

public enum PaymentType { CASH, CHEQUE, CARD_VISA, CARD_MASTER, CARD_CREDIT } Copy code

For example, this is a payment method available in the app. Now we have to create a more general and limited image based on these options:

public enum PaymentTypeView { CASH, CHEQUE, CARD } Copy code

Now we create these two

enum
Mapper interface between:

@Mapper public interface PaymentTypeMapper { PaymentTypeMapper INSTANCE = Mappers.getMapper(PaymentTypeMapper.class); @ValueMappings({ @ValueMapping(source = "CARD_VISA", target = "CARD"), @ValueMapping(source = "CARD_MASTER", target = "CARD"), @ValueMapping(source = "CARD_CREDIT", target = "CARD") }) PaymentTypeView paymentTypeToPaymentTypeView (PaymentType paymentType) ; } Copy code

In this example, we set a general

CARD
Value, and more specific
CARD_VISA
,
CARD_MASTER
with
CARD_CREDIT
. The number of enumeration items between the two enumerations does not match-
PaymentType
Has 5 values, and
PaymentTypeView
There are only three.

In order to build a bridge between these enumeration items, we can use

@ValueMappings
Annotation, the annotation can contain multiple
@ValueMapping
annotation. Here we will
source
Set to one of the three specific enumeration items, and set
target
Set as
CARD
.

MapStruct will naturally handle these situations:

public class PaymentTypeMapperImpl implements PaymentTypeMapper { @Override public PaymentTypeView paymentTypeToPaymentTypeView (PaymentType paymentType) { if (paymentType == null ) { return null ; } PaymentTypeView paymentTypeView; switch (paymentType) { case CARD_VISA: paymentTypeView = PaymentTypeView.CARD; break ; case CARD_MASTER: paymentTypeView = PaymentTypeView.CARD; break ; case CARD_CREDIT: paymentTypeView = PaymentTypeView.CARD; break ; case CASH: paymentTypeView = PaymentTypeView.CASH; break ; case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE; break ; default : throw newIllegalArgumentException( "Unexpected enum constant: " + paymentType ); } return paymentTypeView; } } Copy code

CASH
with
CHEQUE
Converted to the corresponding value by default, special
CARD
Value passed
switch
Loop processing.

However, if you want to convert many values to a more general value, this approach is somewhat impractical. In fact, we don't need to manually assign each value, just let MapStruct convert all remaining available enumeration items (the enumeration item with the same name cannot be found in the target enumeration) directly into another corresponding enumeration item.

able to pass

MappingConstants
To achieve this:

@ValueMapping(source = MappingConstants.ANY_REMAINING, target = "CARD") PaymentTypeView paymentTypeToPaymentTypeView (PaymentType paymentType) ; Copy code

In this example, after the default mapping is completed, all remaining (unmatched) enumeration items will be mapped to

CARD
:

@Override public PaymentTypeView paymentTypeToPaymentTypeView (PaymentType paymentType) { if (paymentType == null ) { return null ; } PaymentTypeView paymentTypeView; switch (paymentType) { case CASH: paymentTypeView = PaymentTypeView.CASH; break ; case CHEQUE: paymentTypeView = PaymentTypeView.CHEQUE; break ; default : paymentTypeView = PaymentTypeView.CARD; } return paymentTypeView; } Copy code

Another option is to use

ANY UNMAPPED
:

@ValueMapping(source = MappingConstants.ANY_UNMAPPED, target = "CARD") PaymentTypeView paymentTypeToPaymentTypeView (PaymentType paymentType) ; Copy code

In this way, MapStruct will not process the default mapping as before, and then map the remaining enumeration items to

target
value. Instead, directly remove all failed
@ValueMapping
The values of the annotations that are explicitly mapped are converted to
target
value.

Collection mapping

Simply put, using MapStruct to deal with collection mapping is the same as dealing with simple types.

We create a simple interface or abstract class and declare the mapping method. MapStruct will automatically generate mapping code based on our statement. Usually, the generated code will traverse the source collection, convert each element to the target type, and add each converted element to the target collection.

List mapping

We first define a new mapping method:

@Mapper public interface DoctorMapper { List<DoctorDto> map (List<Doctor> doctor) ; } Copy code

The generated code is roughly as follows:

public class DoctorMapperImpl implements DoctorMapper { @Override public List<DoctorDto> map (List<Doctor> doctor) { if (doctor == null ) { return null ; } List<DoctorDto> list = new ArrayList<DoctorDto>( doctor.size() ); for (Doctor doctor1: doctor) { list.add( doctorToDoctorDto( doctor1) ); } return list; } protected DoctorDto doctorToDoctorDto (Doctor doctor) { if (doctor == null ) { return null ; } DoctorDto doctorDto = new DoctorDto(); doctorDto.setId( doctor.getId() ); doctorDto.setName( doctor.getName() ); doctorDto.setSpecialization( doctor.getSpecialization() ); return doctorDto; } } Copy code

As you can see, MapStruct automatically generated

Doctor
To
DoctorDto
The mapping method.

But it should be noted that if we add a field in the DTO

fullName
, There will be an error when generating the code:

Warning: Unmapped target property: "fullName".Copy code

Basically, this means that MapStruct cannot automatically generate a mapping method for us in the current situation. Therefore, we need to manually define

Doctor
with
DoctorDto
The mapping method between. Refer to the previous section for details.

Set and Map mapping

Set and Map data is handled in a similar way to List. Modify as follows

DoctorMapper
:

@Mapper public interface DoctorMapper { Set<DoctorDto> setConvert (Set<Doctor> doctor) ; Map<String, DoctorDto> mapConvert (Map<String, Doctor> doctor) ; } Copy code

The final implementation code generated is as follows:

public class DoctorMapperImpl implements DoctorMapper { @Override public Set<DoctorDto> setConvert (Set<Doctor> doctor) { if (doctor == null ) { return null ; } Set<DoctorDto> set = new HashSet<DoctorDto>( Math.max( ( int ) (doctor.size()/.75f ) + 1 , 16 ) ); for (Doctor doctor1: doctor) { set.add( doctorToDoctorDto( doctor1) ); } return set; } @Override public Map<String, DoctorDto> mapConvert (Map<String, Doctor> doctor) { if (doctor == null ) { return null ; } Map<String, DoctorDto> map = new HashMap<String, DoctorDto>( Math.max( ( int ) (doctor.size()/.75f ) + 1 , 16 ) ); for (java.util.Map.Entry<String, Doctor> entry: doctor.entrySet()) { String key = entry.getKey(); DoctorDto value = doctorToDoctorDto( entry.getValue() ); map.put( key, value ); } return map; } protected DoctorDto doctorToDoctorDto (Doctor doctor) { if (doctor == null ) { return null ; } DoctorDto doctorDto = new DoctorDto(); doctorDto.setId( doctor.getId() ); doctorDto.setName( doctor.getName() ); doctorDto.setSpecialization( doctor.getSpecialization() ); return doctorDto; } } Copy code

Similar to List mapping, MapStruct automatically generates

Doctor
Convert to
DoctorDto
The mapping method.

Collection mapping strategy

In many scenarios, we need to convert data types that have a parent-child relationship. Generally speaking, there will be a data type (parent) whose field is a collection of another data type (child).

For this situation, MapStruct provides a way to choose how to set or add the subtype to the supertype. Specifically,

@Mapper
In the annotation
collectionMappingStrategy
Attribute, the attribute can take the value
ACCESSOR_ONLY
,
SETTER_PREFERRED
,
ADDER_PREFERRED
or
TARGET_IMMUTABLE
.

These values represent different ways of assigning values to the collection of subtypes. The default value is

ACCESSOR_ONLY
, Which means that only accessors can be used to set sub-collections.

When the Collection field in the parent type

setter
Method is not available, but we have a subtype
add
Method, this option comes in handy; another useful situation is that the Collection field in the parent type is immutable.

We create a new class:

public class Hospital { private List<Doctor> doctors; //getters and setters or builder } Copy code

At the same time define a mapping target DTO class, and define the getter, setter and adder of the subtype collection field at the same time:

public class HospitalDto { private List<DoctorDto> doctors; //subtype collection field getter public List<DoctorDto> getDoctors () { return doctors; } //Setter of subtype collection field public void setDoctors (List<DoctorDto> doctors) { this .doctors = doctors; } // Subtype data adder public void addDoctor (DoctorDto doctorDTO) { if (doctors == null ) { doctors = new ArrayList<>(); } doctors.add(doctorDTO); } } Copy code

Create the corresponding mapper:

@Mapper(uses = DoctorMapper.class) public interface HospitalMapper { HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class); HospitalDto toDto (Hospital hospital) ; } Copy code

The final implementation code generated is:

public class HospitalMapperImpl implements HospitalMapper { @Override public HospitalDto toDto (Hospital hospital) { if (hospital == null ) { return null ; } HospitalDto hospitalDto = new HospitalDto(); hospitalDto.setDoctors( doctorListToDoctorDtoList( hospital.getDoctors()) ); return hospitalDto; } } Copy code

As you can see, the strategy adopted by default is

ACCESSOR_ONLY
, Use the setter method
setDoctors()
to
HospitalDto
Write list data in the object.

Relatively, if you use

ADDER_PREFERRED
As a mapping strategy:

@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = DoctorMapper.class) public interface HospitalMapper { HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class); HospitalDto toDto (Hospital hospital) ; } Copy code

At this time, the adder method will be used to add the converted sub-type DTO objects to the collection field of the parent type one by one.

public class CompanyMapperAdderPreferredImpl implements CompanyMapperAdderPreferred { private final EmployeeMapper employeeMapper = Mappers.getMapper( EmployeeMapper.class ); @Override public CompanyDTO map (Company company) { if (company == null ) { return null ; } CompanyDTO companyDTO = new CompanyDTO(); if (company.getEmployees() != null ) { for (Employee employee: company.getEmployees()) { companyDTO.addEmployee( employeeMapper.map( employee) ); } } return companyDTO; } } Copy code

If there is neither in the target DTO

setter
There is no way
adder
Method, will pass first
getter
The method gets the subtype collection, and then calls the corresponding interface of the collection to add the subtype object.

You can see the different types of DTO definitions (whether it contains a setter method or an adder method) in the reference document , and the way to add subtypes to the collection when using different mapping strategies.

Target set realization type

MapStruct supports the collection interface as the target type of the mapping method.

In this case, some default implementations of collection interfaces will be used in the generated code. For example, in the example above,

List
The default implementation is
ArrayList
.

Common interfaces and their corresponding default implementations are as follows:

Interface typeImplementation type
Collection
ArrayList
List
ArrayList
Map
HashMap
SortedMap
TreeMap
ConcurrentMap
ConcurrentHashMap

You can find a list of all the interfaces supported by MapStruct and the default implementation type corresponding to each interface in the reference document .

Advanced operation

Dependency injection

So far we have been passing

getMapper()
Method to access the generated mapper:

DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); copy the code

However, if you are using Spring, you can inject the mapper just like a regular dependency by simply modifying the mapper configuration.

modify

DoctorMapper
To support the Spring framework:

@Mapper(componentModel = "spring") public interface DoctorMapper {} Copy code

in

@Mapper
Added in comments
(ComponentModel = "spring")
, Is to tell MapStruct that when generating the mapper implementation class, we hope it can support creation through Spring's dependency injection. Now, there is no need to add in the interface
INSTANCE
Field again.

Generated this time

DoctorMapperImpl
Will carry
@Component
annotation:

@Component public class DoctorMapperImpl the implements DoctorMapper {} copy the code

As long as it is marked as

@Component
, Spring can treat it as a bean, and you can pass it in other classes (such as controllers)
@Autowire
Annotate to use it:

@Controller public class DoctorController () { @Autowired private DoctorMapper doctorMapper; } Copy code

If you don't use Spring, MapStruct also supports Java CDI :

@Mapper(componentModel = "cdi") public interface DoctorMapper {} Copy code

Add default value

@Mapping
Annotations have two very useful signs are constants
constant
And default value
defaultValue
. regardless
source
How to get the value, will always use the constant value; if
source
The value is
null
, The default value will be used.

repair it a little

DoctorMapper
,add one
constant
with one
defaultValue
:

@Mapper(uses = {PatientMapper.class}, componentModel = "spring") public interface DoctorMapper { @Mapping(target = "id", constant = "-1") @Mapping(source = "doctor.patientList", target = "patientDtoList") @Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "Information Not Available") DoctorDto toDto (Doctor doctor) ; } Copy code

in case

specialty
Unavailable, we will replace it with
"Information Not Available"
String, in addition, we will
id
Hardcoded as
-1
.

The generated code is as follows:

@Component public class DoctorMapperImpl implements DoctorMapper { @Autowired private PatientMapper patientMapper; @Override public DoctorDto toDto (Doctor doctor) { if (doctor == null ) { return null ; } DoctorDto doctorDto = new DoctorDto(); if (doctor.getSpecialty() != null ) { doctorDto.setSpecialization(doctor.getSpecialty()); } else { doctorDto.setSpecialization( "Information Not Available" ); } doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList())); doctorDto.setName(doctor.getName()); doctorDto.setId(- 1 ); return doctorDto; } } Copy code

You can see if

doctor.getSpecialty()
The return value is
null
, Then
specialization
Set as our default information. No matter what the situation is,
id
Assignment, because this is a
constant
.

Add expression

MapStruct even allows

@Mapping
Enter the Java expression in the comment. You can set
defaultExpression
(
source
The value is
null
Effective when), or a
expression
(Similar to constants, permanent effective).

in

Doctor
with
DoctorDto
Two new attributes have been added to both classes, one is
String
Type of
externalId
,the other is
LocalDateTime
Type of
appointment
, The two classes are roughly as follows:

public class Doctor { private int id; private String name; private String externalId; private String specialty; private LocalDateTime availability; private List<Patient> patientList; //getters and setters or builder } Copy code
public class DoctorDto { private int id; private String name; private String externalId; private String specialization; private LocalDateTime availability; private List<PatientDto> patientDtoList; //getters and setters or builder } Copy code

modify

DoctorMapper
:

@Mapper(uses = {PatientMapper.class}, componentModel = "spring", imports = {LocalDateTime.class, UUID.class}) public interface DoctorMapper { @Mapping(target = "externalId", expression = "java(UUID.randomUUID().toString())") @Mapping(source = "doctor.availability", target = "availability", defaultExpression = "java(LocalDateTime. now())") @Mapping(source = "doctor.patientList", target = "patientDtoList") @Mapping(source = "doctor.specialty", target = "specialization") DoctorDto toDtoWithExpression (Doctor doctor) ; } Copy code

As you can see, here will

externalId
Is set to
java(UUID.randomUUID().toString())
, If there is no
availability
Attribute, the target object
availability
Set as a new
LocalDateTime
Object.

Since the expression is just a string, we must specify the class used in the expression. But the expression here is not the final code to be executed, just the text value of a letter. Therefore, we have to

@Mapper
Add in
imports = {LocalDateTime.class, UUID.class}
.

Add custom method

So far, the strategy we have been using is to add a "placeholder" method and expect MapStruct to implement it for us. In fact, we can also add custom to the interface

default
Method, you can also pass
default
The method directly implements a mapping. Then we can call the method directly through the instance without any problems.

For this, we create a

DoctorPatientSummary
Class, which contains a
Doctor
and
Patient
Summary information of the list:

public class DoctorPatientSummary { private int doctorId; private int patientCount; private String doctorName; private String specialization; private String institute; private List<Integer> patientIds; //getters and setters or builder } Copy code

Next, we are in

DoctorMapper
Add one
default
Method, which will
Doctor
with
Education
Object converted to a
DoctorPatientSummary
:

@Mapper public interface DoctorMapper { default DoctorPatientSummary toDoctorPatientSummary (Doctor doctor, Education education) { return DoctorPatientSummary.builder() .doctorId(doctor.getId()) .doctorName(doctor.getName()) .patientCount(doctor.getPatientList().size()) .patientIds(doctor.getPatientList() .stream() .map(Patient::getId) .collect(Collectors.toList())) .institute(education.getInstitute()) .specialization(education.getDegreeName()) .build(); } } Copy code

Here is created using Builder mode

DoctorPatientSummary
Object.

After MapStruct generates the mapper implementation class, you can use this implementation method just like accessing any other mapper method:

DoctorPatientSummary summary = doctorMapper.toDoctorPatientSummary (dotor, education ); duplicated code

Create a custom mapper

We have always used the interface to design the mapper function. In fact, we can also use a

@Mapper
of
abstract
Class to implement a mapper. MapStruct will also create an implementation for this class, similar to creating an interface implementation.

Let's rewrite the previous example, this time, we will modify it to an abstract class:

@Mapper public abstract class DoctorCustomMapper { public DoctorPatientSummary toDoctorPatientSummary (Doctor doctor, Education education) { return DoctorPatientSummary.builder() .doctorId(doctor.getId()) .doctorName(doctor.getName()) .patientCount(doctor.getPatientList().size()) .patientIds(doctor.getPatientList() .stream() .map(Patient::getId) .collect(Collectors.toList())) .institute(education.getInstitute()) .specialization(education.getDegreeName()) .build(); } } Copy code

You can use this mapper in the same way. Due to fewer restrictions, the use of abstract classes can give us more control and choice when creating custom implementations. Another benefit is that you can add

@BeforeMapping
with
@AfterMapping
method.

@BeforeMapping and @AfterMapping

For further control and customization, we can define

@BeforeMapping
with
@AfterMapping
method. Obviously, these two methods are executed before and after each mapping. In other words, in the final implementation code, these two methods will be added and executed before and after the two objects are actually mapped.

allowable

DoctorCustomMapper
Add two methods:

@Mapper(uses = {PatientMapper.class}, componentModel = "spring") public abstract class DoctorCustomMapper { @BeforeMapping protected void validate (Doctor doctor) { if (doctor.getPatientList() == null ){ doctor.setPatientList( new ArrayList<>()); } } @AfterMapping protected void updateResult ( @MappingTarget DoctorDto doctorDto) { doctorDto.setName(doctorDto.getName().toUpperCase()); doctorDto.setDegree(doctorDto.getDegree().toUpperCase()); doctorDto.setSpecialization(doctorDto.getSpecialization().toUpperCase()); } @Mapping(source = "doctor.patientList", target = "patientDtoList") @Mapping(source = "doctor.specialty", target = "specialization") public abstract DoctorDto toDoctorDto (Doctor doctor) ; } Copy code

Generate a mapper implementation class based on this abstract class:

@Component public class DoctorCustomMapperImpl extends DoctorCustomMapper { @Autowired private PatientMapper patientMapper; @Override public DoctorDto toDoctorDto (Doctor doctor) { validate(doctor); if (doctor == null ) { return null ; } DoctorDto doctorDto = new DoctorDto(); doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor .getPatientList())); doctorDto.setSpecialization(doctor.getSpecialty()); doctorDto.setId(doctor.getId()); doctorDto.setName(doctor.getName()); updateResult(doctorDto); return doctorDto; } } Copy code

can be seen,

validate()
Method will be
DoctorDto
Is executed before the object is instantiated, and
updateResult()
The method will be executed after the mapping is over.

Mapping exception handling

Exception handling is inevitable, and the application will produce an abnormal state at any time. MapStruct provides support for exception handling, which can simplify the work of developers.

Consider a scenario where we want to

Doctor
Mapped to
DoctorDto
Check before
Doctor
The data. We create a new independent
Validator
Class to check:

public class Validator { public int validateId ( int id) throws ValidationException { if (id ==- 1 ){ throw new ValidationException( "Invalid value in ID" ); } return id; } } Copy code

Let's modify

DoctorMapper
To use
Validator
Class, no need to specify the implementation. Same as before, in
@Mapper
Add the class to the list of used classes. All we need to do is tell MapStruct our
toDto()
Will throw
throws ValidationException
:

@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring") public interface DoctorMapper { @Mapping(source = "doctor.patientList", target = "patientDtoList") @Mapping(source = "doctor.specialty", target = "specialization") DoctorDto toDto (Doctor doctor) throws ValidationException ; } Copy code

The final generated mapper code is as follows:

@Component public class DoctorMapperImpl implements DoctorMapper { @Autowired private PatientMapper patientMapper; @Autowired private Validator validator; @Override public DoctorDto toDto (Doctor doctor) throws ValidationException { if (doctor == null ) { return null ; } DoctorDto doctorDto = new DoctorDto(); doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor .getPatientList())); doctorDto.setSpecialization(doctor.getSpecialty()); doctorDto.setId(validator.validateId(doctor.getId())); doctorDto.setName(doctor.getName()); doctorDto.setExternalId(doctor.getExternalId()); doctorDto.setAvailability(doctor.getAvailability()); return doctorDto; } } Copy code

MapStruct automatically

doctorDto
of
id
Set as
Validator
The method return value of the instance. It also adds a throws clause to the method signature.

Note that if the type of a pair of attributes before and after the mapping is the same as

Validator
The method in the input and output parameters are the same type, then the field will be called when the field is mapped
Validator
The method in, so please use this method with caution.

Mapping configuration

MapStruct provides some very useful configurations for writing mapper methods. In most cases, if we have defined a mapping method between two types, when we want to add another mapping method between the same type, we tend to directly copy the mapping configuration of the existing method.

In fact, we don't need to manually copy these annotations, just a simple configuration can create an identical/similar mapping method.

Inherited configuration

Let's review the " update existing instance ". In this scenario, we created a mapper to update the attribute values of the existing Doctor object according to the attributes of the DoctorDto object:

@Mapper(uses = {PatientMapper.class}) public interface DoctorMapper { DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class); @Mapping(source = "doctorDto.patientDtoList", target = "patientList") @Mapping(source = "doctorDto.specialization", target = "specialty") void updateModel (DoctorDto doctorDto, @MappingTarget Doctor doctor) ; } Copy code

Suppose we have another mapper that will

DoctorDto
Convert to
Doctor
:

@Mapper(uses = {PatientMapper.class, Validator.class}) public interface DoctorMapper { @Mapping(source = "doctorDto.patientDtoList", target = "patientList") @Mapping(source = "doctorDto.specialization", target = "specialty") Doctor toModel (DoctorDto doctorDto) ; } Copy code

These two mapping methods use the same annotation configuration,

source
with
target
It's all the same. Actually we can use
@InheritConfiguration
Annotation to avoid duplication of configuration of these two mapper methods.

If you add to a method

@InheritConfiguration
Annotation, MapStruct will retrieve other configured methods, looking for annotation configurations that can be used for the current method. Generally speaking, this annotation is used for
mapping
Behind the method
update
The method is as follows:

@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring") public interface DoctorMapper { @Mapping(source = "doctorDto.specialization", target = "specialty") @Mapping(source = "doctorDto.patientDtoList", target = "patientList") Doctor toModel (DoctorDto doctorDto) ; @InheritConfiguration void updateModel (DoctorDto doctorDto, @MappingTarget Doctor doctor) ; } Copy code

Inherit the reverse configuration

There is another similar scenario, which is to write a mapping function to convert Model to DTO , and to convert DTO to Model . As shown in the code below, we must add the same comment on both functions.

@Mapper(componentModel = "spring") public interface PatientMapper { @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy") Patient toModel (PatientDto patientDto) ; @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy") PatientDto toDto (Patient patient) ; } Copy code

The configuration of the two methods will not be exactly the same, in fact, they should be opposite. Convert Model to DTO , and convert DTO to Model -the fields before and after the mapping are the same, but the source attribute field and the target attribute field are opposite.

We can use on the second method

@InheritInverseConfiguration
Note, avoid writing the mapping configuration twice:

@Mapper(componentModel = "spring") public interface PatientMapper { @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy") Patient toModel (PatientDto patientDto) ; @InheritInverseConfiguration PatientDto toDto (Patient patient) ; } Copy code

The code generated by the two Mappers is the same.

summary

In this article, we explored MapStruct-a library for creating mapper classes. From basic mapping to custom methods and custom mappers, we also introduced some advanced operation options provided by MapStruct, including dependency injection, data type mapping, enumeration mapping, and expression usage.

MapStruct provides a powerful integrated plug-in, which can reduce the workload of developers to write template code and make the process of creating mappers simple and fast.

If you want to explore more and more detailed usage, you can refer to the official reference guide provided by MapStruct .


For more high-quality articles, you can move to your personal blog:

Ma Dao Cheng Gong

or

Follow the public account