introduce

With microservices and distributed applications rapidly taking over the development landscape, data integrity and security are more important than ever. Secure communication channels and limited data transfer between these loosely coupled systems are Paramount. Most of the time, end users or services do not need to access all of the data in the model, but only specific parts.

Data Transfer Objects (Dtos) are often used in these applications. A DTO is simply an object that holds the requested information in another object. Often, this information is a limited part. For example, there are often transitions between entities defined at the persistence layer and Dtos sent to the client. Because dtos are reflections of the original objects, the mapper between these classes plays a key role in the transformation process.

This is the problem MapStruct addresses: manually creating a bean mapper is time-consuming. However, 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 for creating extended mapper that implements transformations between Java beans. With MapStruct, we only need to create interfaces, and the library automatically creates a concrete mapping implementation during compilation through annotations, greatly reducing the amount of boilerplate code that would normally have to be written by hand.

MapStruct rely on

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

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
Copy the code

This dependency imports the core annotation of MapStruct. Since MapStruct works at compile time and is integrated into build tools like Maven and Gradle, we also have to add a plugin maven-Compiler-plugin to the
tag, And add annotationProcessorPaths to its configuration, which generates the corresponding code at build time.

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1 track of</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

Installing MapStruct is easier if you use Gradle:

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

The net.ltgt.apt plugin takes care of the comments. You can enable the plugin apt-idea or apt-Eclipse depending on the IDE you are using.

The latest stable versions of MapStruct and its processors are available from Maven’s central repository.

mapping

The basic mapping

Let’s start with some basic mappings. We will create a Doctor object and a DoctorDto. For convenience, they all use the same name for their property fields:

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

Now, to map between the two, we will create a DoctorMapper interface. Using the @mapper annotation on this interface, MapStruct knows that this is a Mapper between two classes.

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

This code creates an INSTANCE of DoctorMapper type, INSTANCE, which is the “entry” we call after generating the corresponding implementation code.

We define the toDto() method in the interface, which takes an instance of Doctor as an argument and returns an instance of DoctorDto. This is enough to let MapStruct know that we want to map an instance of Doctor to an instance of DoctorDto.

When we build/compile the application, the MapStruct annotation processor plug-in recognizes the DoctorMapper interface and generates 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());

        returndoctorDto.build(); }}Copy the code

The DoctorMapperImpl class contains a toDto() method that maps the value of our Doctor property to the DoctorDto property field. To map an instance of Doctor to an instance of DoctorDto, write:

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

Note: You may also have noticed the DoctorDtoBuilder in the implementation code above. Because Builder code tends to be long, the builder pattern implementation code has been omitted for brevity. If your class contains Builder, MapStruct will try to use it to create instances; If not, the MapStruct is instantiated with the new keyword.

Mapping between different fields

Typically, the field names of the model and DTO will not be exactly the same. The names may change slightly, as team members specify different names and developers choose to package the returned information differently for different calling services.

MapStruct provides support for such cases through the @Mapping annotation.

Different attribute names

We’ll start by updating the Doctor class and adding a property specialty:

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

Add a Specialization attribute to the DoctorDto class:

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

Now, we need to let the DoctorMapper know about the inconsistency. We can use the @Mapping annotation and set the source and target tags inside it to point to the two inconsistent fields, respectively.

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

    @Mapping(source = "doctor.specialty", target = "specialization")
    DoctorDto toDto(Doctor doctor);
}
Copy the code

The annotation code says that the Specialty field in Doctor corresponds to the Specialization of the DoctorDto class.

After compilation, the following implementation code is 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());

        returndoctorDto.build(); }}Copy the code

Multiple source class

Sometimes, a single class is not enough to build a DTO, and we might want to aggregate values from multiple classes into a SINGLE DTO for use by the end user. This can also be done by setting the appropriate flag in the @Mapping annotation.

Let’s start with another new object Education:

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

Then add a new field to DoctorDto:

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

Next, update the DoctorMapper interface 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 the code

We added another @Mapping annotation and set its source to degreeName of the Education class and target to degree field of the DoctorDto class.

If the Education and Doctor classes contain fields with the same name, we must let the mapper know which to use or it will throw an exception. For example, if both models contain an ID field, we have to choose which class id to map to the DTO attribute.

Subobject mapping

In most cases, poJOs don’t just contain basic data types; they often contain other classes. For example, a Doctor class might have multiple patient classes:

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

Add a patient List to Doctor:

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

Since Patient needs the transformation, create a corresponding DTO for it:

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

Finally, add a list of PatientDto stores in DoctorDto:

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

Before modifying DoctorMapper, we create a mapper interface that supports Patient and PatientDto transformations:

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

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

Then, we can modify the DoctorMapper to handle 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 the code

Because we’re dealing with another class that needs to be mapped, we set the uses flag for the @mapper annotation so that the current @Mapper can use another @Mapper Mapper. We’ve only added one here, but you can add as many classes/Mapper as you want.

We have added the USES flag, so when we generate the mapper implementation for the DoctorMapper interface, MapStruct will also convert the Patient model to PatientDto — because we have registered PatientMapper for this task.

Compile to 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 ) );
        }

        returnlist1; }}Copy the code

Obviously, in addition to toDto () mapping method, the final implementation, also added a new method for mapping patientListToPatientDtoList (). This method was added without an explicit definition, simply because we added PatientMapper to the DoctorMapper.

The method iterates over a Patient list, converts each element to PatientDto, and adds the converted object to the list within the DoctorDto object.

Updating an existing instance

Sometimes we want to update a property in a model with the latest value of the DTO, using the @mappingTarget annotation on the target object (DoctorDto in our case) to update an 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 the code

Regenerating the implementation code yields the 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 the code

It is important to note that since the patient list is a child entity in the model, the patient list is also updated.

Data type conversion

Data type mapping

MapStruct supports data type conversions between source and target attributes. It also provides automatic conversions between base types and their corresponding wrapper classes.

Automatic type conversion applies to:

  • Between the base type and its corresponding wrapper class. For instance,intInteger.floatFloat.longLong.booleanBooleanAnd so on.
  • Between any base type and any wrapper class. Such asintlong.byteIntegerAnd so on.
  • All basic types and packaging classes withStringIn between. Such asbooleanString.IntegerString.floatStringAnd so on.
  • The enumeration andStringIn between.
  • Java large number types (java.math.BigInteger.java.math.BigDecimal) and Java primitive types (including their wrapper classes)StringIn between.
  • For other cases, see the official MapStruct documentation.

Thus, MapStrcut takes care of the type conversion itself when generating mapper code if any of these conditions exist between the source and target fields.

We modified the PatientDto to add a dateofBirth field:

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

On the other hand, adding the Patient object has a dateOfBirth of type String:

public class Patient {
    private int id;
    private String name;
    private String dateOfBirth;
    // getters and setters or builder
}
Copy the 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 the code

When converting dates, we can also use dateFormat to set format declarations. The generated implementation code looks like this:

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());

        returnpatient.build(); }}Copy the code

As you can see, the dateFormat declared by dateFormat is used. If we do not declare a format, MapStruct will use the LocalDate default format, which looks like this:

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

Digital format conversion

As you can see in the example above, the dateFormat flag can be used to specify the format of the date during date conversion.

In addition, for numeric conversions, you can also specify the display format using numberFormat:

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

Enumeration mapping

Enumeration maps work in the same way as field maps. There is no problem with MapStruct mapping enumerations with the same name. However, for enumerations with different names, we need to use the @valuemapping annotation. Again, this is similar to a normal @mapping annotation.

Let’s start by creating two enumerations. The first is PaymentType:

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

For example, here’s an in-app payment method, and now we’re going to create a more general, limited map based on these options:

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

Now we create a mapper interface between the two enUms:

@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 the code

So in this example, we’re setting the general CARD value, and the more specific CARD_VISA, CARD_MASTER, and CARD_CREDIT. The number of enumerations does not match between the two enumerations — PaymentType has five values and PaymentTypeView has only three.

To build Bridges between these enumerated items, we can use the @valuemappings annotation, which can contain multiple @valuemapping annotations. Here, we set source to one of three concrete enumerations and target to CARD.

MapStruct naturally handles these cases:

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 new IllegalArgumentException( "Unexpected enum constant: " + paymentType );
        }
        returnpaymentTypeView; }}Copy the code

CASH and retailer are converted to the corresponding value by default, and special CARD value is cycled through switch.

However, if you want to convert many values to a more general value, this approach may be impractical. Instead of assigning each value manually, we just need MapStruct to convert all remaining available enumerations (no enumerations with the same name are found in the target enumeration) directly into the corresponding enumeration.

This can be achieved with MappingConstants:

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

In this example, after the default mapping is done, all remaining (unmatched) enumerations are 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 the code

Another option is to use ANY UNMAPPED:

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

In this way, MapStruct does not first process the default mapping and then map the remaining enumeration items to the target value as it did earlier. Instead, all values that are not explicitly mapped by the @Valuemapping annotation are directly converted to target values.

Set the mapping

Simply put, using MapStruct works with collection maps in the same way as it does with simple types.

We create a simple interface or abstract class and declare mapping methods. MapStruct will automatically generate the mapping code based on our declaration. Typically, the generated code iterates through the source collection, converts each element to the target type, and adds each converted element to the target collection.

List the mapping

Let’s define a new mapping method:

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

The generated code looks like this:

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() );

        returndoctorDto; }}Copy the code

As you can see, MapStruct automatically generates a mapping method for us from Doctor to DoctorDto.

Note, however, that if we add a new field fullName to the DTO, we will get an error:

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

Basically, this means that MapStruct cannot automatically generate mapping methods for us in its current case. Therefore, we need to manually define the mapping method between Doctor and DoctorDto. Refer to the previous section for details.

Set and Map mapping

Sets and maps are handled similarly to lists. Modify DoctorMapper as follows:

@Mapper
public interface DoctorMapper {

    Set<DoctorDto> setConvert(Set<Doctor> doctor);

    Map<String, DoctorDto> mapConvert(Map<String, Doctor> doctor);
}
Copy the code

The resulting implementation code 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() );

        returndoctorDto; }}Copy the code

Like the List map, MapStruct automatically generates a mapping method that converts Doctor to DoctorDto.

Set mapping strategy

In many scenarios, we need to convert data types that have parent-child relationships. Typically, there is a data type (parent) whose fields are a collection of another data type (children).

In this case, MapStruct provides a way to choose how to set or add a subtype to a parent type. Specifically, the @ Mapper collectionMappingStrategy attributes of annotation, the attribute value can be as ACCESSOR_ONLY, SETTER_PREFERRED, ADDER_PREFERRED or TARGET_IMMUTABLE.

These values represent different ways of assigning values to collections of subtypes. The default value is ACCESSOR_ONLY, which means that subcollections can only be set using accessors.

This option is useful when the setter method for the Collection field in the parent type is not available, but we have a subtype add method; Another useful case is when the Collection field in the parent type is immutable.

Let’s create a new class:

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

Define a mapping target DTO class as well as getters, setters, and adders for subtype collection fields:

public class HospitalDto {

    private List<DoctorDto> doctors;

		Getter for subtype collection field
    public List<DoctorDto> getDoctors(a) {
        return doctors;
    }
		// Subtype set field setter
    public void setDoctors(List<DoctorDto> doctors) {
        this.doctors = doctors;
    }
		// Subtype data adder
    public void addDoctor(DoctorDto doctorDTO) {
        if (doctors == null) {
            doctors = newArrayList<>(); } doctors.add(doctorDTO); }}Copy the code

Create the corresponding mapper:

@Mapper(uses = DoctorMapper.class)
public interface HospitalMapper {
    HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class);

    HospitalDto toDto(Hospital hospital);
}
Copy the code

The resulting implementation code 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() ) );

        returnhospitalDto; }}Copy the code

As you can see, the default policy is ACCESSOR_ONLY, using setter method setDoctors() to write list data to the HospitalDto object.

In contrast, if ADDER_PREFERRED is used as the mapping strategy:

@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED, uses = DoctorMapper.class)
public interface HospitalMapper {
    HospitalMapper INSTANCE = Mappers.getMapper(HospitalMapper.class);

    HospitalDto toDto(Hospital hospital);
}
Copy the code

At this point, the converted subtype DTO objects are added to the collection field of the parent type one by one using the Adder method.

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 ) ); }}returncompanyDTO; }}Copy the code

If the target DTO has neither setter nor Adder methods, the collection of subtypes is first obtained through getter methods, and then the corresponding interface of the collection is called to add subtype objects.

You can see in the reference documentation how different types of DTO definitions (whether setter methods or Adder methods are included) are used to add subtypes to collections with different mapping strategies.

Target collection implementation type

MapStruct supports collection interfaces as target types for mapping methods.

In this case, some collection interface default implementation is used in the generated code. For example, in the example above, the default implementation of List is ArrayList.

Common interfaces and their corresponding default implementations are as follows:

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

You can find a list of all the interfaces supported by MapStruct, along with the default implementation type for each interface, in the Reference documentation.

Advanced operation

Dependency injection

So far, we have been accessing the generated mapper through the getMapper() method:

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

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

Modify DoctorMapper to support the Spring framework:

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

We add (componentModel = “Spring”) to the @mapper annotation to tell MapStruct that when we generate the Mapper implementation class, we want it to support creation through Spring’s dependency injection. At this point, you don’t need to add the INSTANCE field to the interface.

The DoctorMapperImpl generated this time will have the @Component annotation:

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

As long as it is marked @Component, Spring can treat it as a bean, and you can use it in other classes (such as controllers) via the @Autowire annotation:

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

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

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

Add default Values

The @mapping annotation has two useful flags: constant and defaultValue. Constant values are always used regardless of the source value; If source is null, the default value is used.

Modify DoctorMapper to add constant and 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 the code

If Specialty is Not Available, we replace it with the “Information Not Available” string, and we hardcode the ID to -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);

        returndoctorDto; }}Copy the code

As you can see, if doctor.getSpecialty() returns null, we set specialization to our default information. In any case, id is assigned because it’s constant.

Add expression

MapStruct even allows you to enter Java expressions in the @Mapping annotation. You can set defaultExpression (which works when source is null) or an expression (which is like a constant and lasts forever).

Add two new properties to both the Doctor and DoctorDto classes: externalId (String) and Appointment (LocalDateTime).

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

As you can see, set externalId to Java (uuID.randomuuid ().tostring ()). If no availability attribute exists in the source object, Sets Availability in the target object to a new LocalDateTime object.

Since the expression is just a string, we must specify the class to use in the expression. But the expression here is not the final code executed, just the text value of a letter. So we’ll add imports = {localDatetime. class, uuID.class} to @mapper.

Add custom methods

So far, the strategy we’ve been using is to add a “placeholder” method and expect MapStruct to implement it for us. You can also add a custom default method to the interface, or implement a mapping directly using the default method. We can then call the method directly from the instance without any problems.

To do this, we create a DoctorPatientSummary class that contains a summary of a Doctor and its Patient 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 the code

Next, we add a default method to DoctorMapper that converts the Doctor and Education objects into 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 the code

The Builder pattern is used to create the DoctorPatientSummary object.

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

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

Create a custom mapper

We have been designing Mapper functionality through interfaces, but we can also implement a Mapper through an Abstract class with @mapper. MapStruct also creates an implementation for this class, similar to creating an interface implementation.

Let’s rewrite the previous example, this time changing 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 the code

You can use this mapper in the same way. With fewer restrictions, using abstract classes gives us more control and choice when creating custom implementations. Another benefit is that you can add the @beforeMapping and @AfterMapping methods.

@ BeforeMapping and @ AfterMapping

For further control and customization, we can define @beforeMapping and @AfterMapping methods. Obviously, these two methods are executed before and after each mapping. That is, in the final implementation code, the two methods are added and executed before and after the objects are actually mapped.

You can add two methods to DoctorCustomMapper:

@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public abstract class DoctorCustomMapper {

    @BeforeMapping
    protected void validate(Doctor doctor) {
        if(doctor.getPatientList() == null){
            doctor.setPatientList(newArrayList<>()); }}@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 the 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);

        returndoctorDto; }}Copy the code

As you can see, the validate() method is executed before the DoctorDto object is instantiated, and the updateResult() method is executed after the mapping ends.

Mapping exception Handling

Exception handling is inevitable, and an application can generate exception states at any time. MapStruct provides support for exception handling to simplify the developer’s life.

Consider a scenario where we want to validate Doctor’s data before mapping Doctor to DoctorDto. Let’s create a separate Validator class for validation:

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

Let’s modify DoctorMapper to use the Validator class without specifying the implementation. As before, add this class to the list of classes used by @Mapper. All we need to do is tell MapStruct that our toDto() throws throws throws 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 the code

The resulting 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());

        returndoctorDto; }}Copy the code

MapStruct automatically sets the ID of doctorDto to the method return value of the Validator instance. It also adds a throws clause to the method signature.

Note that if the mapping pair of attributes has the same type as the method input parameter in the Validator, the mapping will call the method in the Validator, so use this method with caution.

Mapping configuration

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

There is no need to copy these annotations manually, and a simple configuration is required to create an identical/similar mapping method.

Inherit the configuration

In this scenario, we create a mapper that updates the property values of the existing Doctor object based on the properties 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 the code

Suppose we have another mapper that converts DoctorDto 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 the code

The two mapping methods use the same annotation configuration, and both source and target are the same. We can use the @inheritConfiguration annotation to avoid reconfiguring the two mapper methods.

If you add @inheritConfiguration annotations to a method, MapStruct retrieves other configured methods to find annotation configurations available for the current method. In general, this annotation is used for the update method after the mapping method, 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 the code

Inheritance reverse configuration

Another similar scenario involves writing mapping functions to convert Model to DTOS and dtos to Model. As shown in the following code, we must add the same comment to 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 the code

The configuration of the two methods will not be exactly the same; in fact, they should be opposite. Converting Model to DTO, and converting DTO to Model — the fields are the same before and after the mapping, but the source property fields are reversed from the target property fields.

We can use @ InheritInverseConfiguration comment on the second method, avoid writing twice mapping configuration:

@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 the code

The code generated by both Mappers is the same.

conclusion

In this article, we explored MapStruct, a library for creating mapper classes. From basic mapping to custom methods and custom mapmers, we also looked at some of the advanced manipulation options offered by MapStruct, including dependency injection, data type mapping, enumeration mapping, and expression usage.

MapStruct provides a powerful integration plug-in that reduces the developer’s effort to write template code and makes the process of creating a mapper simple and quick.

If you want to explore more detailed ways to use MapStruct, you can refer to the reference guide provided by MapStruct.


For more quality articles, you can go to personal blog:

Code that is male

or

Follow public account