MapStruct Usage Guide

MapStruct Usage Guide

Hello! MapStruct usage guide, basic grammar knowledge. MapStruct official website

Maven dependency

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

P.S. This dependency will import MapStruct’s core annotations. Since MapStruct works at compile time and is integrated with build tools like Maven and Gradle, we must also add a plugin maven-compiler-plugin in the <build/> tag and add annotationProcessorPaths in its configuration. The plugin will generate the corresponding code at build time.

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

Reference: https://mapstruct.org/documentation/installation/

Core annotations of MapStruct

import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
import org.mapstruct.factory.Mappers;

@Mapping
@MappingTarget
//Ignore the conversion of categoryId
@Mapping(target = “categoryId”,ignore = true),
//Collection application in the source data class --> Data reference in the target class – conversion List
@Mapping(target = "trees", source = "colors"),
//The attributes of the nested class are simply transferred and converted
@Mapping(target = "run", source = "cart.animal.run"),
time conversion and formatting
@Mapping(source = “birthday”, target = “birthDateFormat”, dateFormat = “yyyy-MM-dd HH:mm:ss”), @Mapping: Attribute mapping,
if the source object attribute is consistent with the target object name, the corresponding attribute
source will be automatically mapped: Source attribute
target: Target attribute
dateFormat: Convert between String and Date dates through SimpleDateFormat. The value is the date format of SimpleDateFormat.
ignore: Ignore this field.
@Mappings: Configure multiple @Mappings.
@MappingTarget is used to update existing objects.
@InheritConfiguration Used to inherit configuration

basic mapping

Let's start with some basic mappings. 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
}

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

To map between the two, we create a DoctorMapper interface. Annotate the interface with @Mapper and MapStruct will know that this is a mapper between two classes.

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

This code creates an instance INSTANCE of the DoctorMapper type. After generating the corresponding implementation code, this is the "entry" we call.

We defined the toDto() method in the interface, which receives a Doctor instance as a parameter and returns a DoctorDto instance. This is enough to let MapStruct know that we want to map a Doctor instance to a DoctorDto instance.

When we build/compile the application, the MapStruct annotation processor plugin 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());

        return doctorDto.build();
    }
}

The DoctorMapperImpl class contains a toDto() method that maps our Doctor attribute values ​​to DoctorDto's attribute fields. If you want to map a Doctor instance to a DoctorDto instance, you can write:

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

Note: You may also notice DoctorDtoBuilder in the implementation code above. Because builder code is often relatively long, the implementation code of builder mode is omitted here for the sake of brevity. If your class contains a Builder, MapStruct will try to use it to create an instance; if not, MapStruct will instantiate it through the new keyword.

Mapping between different fields

We need to let DoctorMapper know about the inconsistency. We can use the @Mapping annotation and set its internal source and target tags to point to two inconsistent fields respectively.

@Mapper
public interface DoctorMapper {
    
    
    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
	//将目标对象的 doctor.specialty属性 拷贝给 specialization 
    @Mapping(source = "doctor.specialty", target = "specialization")
    DoctorDto toDto(Doctor doctor);
}

Multiple source classes & different attribute names

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

@Mapper
public interface DoctorMapper {
    
    
    DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
	/*  
	    1:将源对象doctor的specialty赋值给目标对象的specialization属性 
	    2:将源对象education的degreeName赋值给目标对象的degree属性
	    3: 其他属性默认按同名拷贝
	*/
    @Mapping(source = "doctor.specialty", target = "specialization")
    @Mapping(source = "education.degreeName", target = "degree")
    DoctorDto toDto(Doctor doctor, Education education);
}

Sub-object mapping [can refer to other Mapper methods]

In most cases, POJO does not only contain basic data types, but also other classes. For example, there will be multiple patient classes in a Doctor class:

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

Add a list of patients to Doctor:

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

Because the Patient needs to be converted, create a corresponding DTO for it:

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

Finally, add a list to store PatientDto 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
}

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

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

This is a basic mapper and will only handle a few basic data types.

Then, let's modify DoctorMapper to 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);
}

Because we have to deal with another class that needs to be mapped, the uses flag of the @Mapper annotation is set here so that the current @Mapper can use another @Mapper mapper. We only added one here, but you can add as many classes/mappers here as you want.

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

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

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

This method will traverse a Patient list, convert each element to a PatientDto, and add the converted object to the list in the DoctorDto object.

Update existing instance

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

Regenerate the implementation code and you can get 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());
    }
}

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

Data type conversion

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

Automatic type conversion applies to:

Between primitive types and their corresponding wrapper classes. For example, int and Integer, float and Float, long and Long, boolean and Boolean, etc.
Between any primitive type and any wrapper class. Such as int and long, byte and Integer, etc.
Between all basic types and wrapper classes and String. Such as boolean and String, Integer and String, float and String, etc.
Between enumeration and String.
Between Java large number types (java.math.BigInteger, java.math.BigDecimal) and Java basic types (including their wrapper classes) and String.
For other details, see the official MapStruct documentation .
Therefore, during the process of generating mapper code, if any of the above conditions exists between the source field and the target field, MapStrcut will handle the type conversion itself.

date conversion

When converting dates, we can also use dateFormat to set the format statement.

@Mapper
public interface PatientMapper {
    
    

    @Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
    Patient toModel(PatientDto patientDto);
}

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

As you can see, the date format declared by dateFormat is used here. If we do not declare a format, MapStruct will use the default format of LocalDate, which is roughly as follows:

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

digital format conversion

As you can see from the above example, when performing date conversion, you can specify the date format through the dateFormat flag.

In addition, for number conversion, you can also use numberFormat to specify the display format:

// 数字格式转换示例
   @Mapping(source = "price", target = "price", numberFormat = "$#.00")

enum mapping

Enum mapping works the same as field mapping. MapStruct will map enums with the same name , no problem. However, for enumeration items with different names, we need to use @ValueMapping annotation. Again, this is similar to the @Mapping annotation for normal types.

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

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

For example, here are the payment methods available within the app. Now we want to create a more general, limited image based on these options:

public enum PaymentTypeView {
    
    
    CASH,
    CHEQUE,
    CARD
}

Now, we create the mapper interface between these 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);
}

In this example, we set the general CARD value, and the more specific CARD_VISA, CARD_MASTER and CARD_CREDIT. There is a mismatch in the number of enumeration items between the two enumerations - PaymentType has 5 values, while PaymentTypeView has only 3.

In order to build a bridge between these enumeration items, we can use the @ValueMappings annotation, which can contain multiple @ValueMapping annotations. Here, we set the source to one of three specific enumeration items and the target to CARD.

MapStruct handles these cases naturally:

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 );
        }
        return paymentTypeView;
    }
}

CASH and CHEQUE are converted to corresponding values ​​by default, and special CARD values ​​are processed through a switch loop.

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

This can be achieved through MappingConstants:

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

In this example, after the default mapping is completed, all remaining (unmatched) enumeration items 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;
}

Another option is to use ANY UNMAPPED:

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

When using this method, MapStruct will not process the default mapping first and then map the remaining enumeration items to the target value as before. Instead, all values ​​that are not explicitly mapped through the @ValueMapping annotation are directly converted to the target value.

Collection mapping

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

We create a simple interface or abstract class and declare mapping methods. MapStruct will automatically generate mapping code based on our declarations. 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 mapping

Let's start by defining a new mapping method:

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

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

        return doctorDto;
    }
}

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

However, it should be noted that if we add a new field fullName to the DTO , an error will occur when generating code :

警告: Unmapped target property: "fullName".

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

Set and Map mapping

Set and Map type data are processed similarly to List. Modify DoctorMapper as follows:

@Mapper
public interface DoctorMapper {
    
    

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

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

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

        return doctorDto;
    }
}

Similar to List mapping, MapStruct automatically generates a mapping method for converting Doctor to DoctorDto.

Collection mapping strategy

In many scenarios, we need to convert data types with parent-child relationships. Typically, there will be a data type (parent) whose fields are a collection of another data type (child).

For this case, MapStruct provides a way to choose how the subtype is set or added to the parent type. Specifically, it is the collectionMappingStrategy attribute in the @Mapper annotation, which can be ACCESSOR_ONLY, SETTER_PREFERRED, ADDER_PREFERRED or TARGET_IMMUTABLE.

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

This option comes in handy when the Collection field setter method in the parent type is not available, but we have an add method in the subtype; another useful situation is when the Collection field in the parent type is immutable.

Let's create a new class:

public class Hospital {
private List doctors;
// getters and setters or builder
}

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

public class HospitalDto {
    
    

    private List<DoctorDto> doctors;

  // 子类型集合字段getter
    public List<DoctorDto> getDoctors() {
    
    
        return doctors;
    }
  // 子类型集合字段setter
    public void setDoctors(List<DoctorDto> doctors) {
    
    
        this.doctors = doctors;
    }
  // 子类型数据adder
    public void addDoctor(DoctorDto doctorDTO) {
    
    
        if (doctors == null) {
    
    
            doctors = new ArrayList<>();
        }

        doctors.add(doctorDTO);
    }
}

Create the corresponding mapper:

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

    HospitalDto toDto(Hospital hospital);
}

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

As you can see, the strategy adopted by default is ACCESSOR_ONLY, and the setter method setDoctors() is used 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);
}

At this time, the adder method will be used to add the converted subtype 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;
    }
}

If there is neither a setter method nor an adder method in the target DTO, the subtype collection will be obtained first through the getter method, and then the corresponding interface of the collection will be called to add the subtype object.

You can see the different types of DTO definitions (whether they contain setter methods or adder methods) in the reference documentation , and the ways to add subtypes to the collection when using different mapping strategies.

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

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

Common interfaces and their corresponding default implementations are as follows:

Interface type Implementation type

You can find a list of all interfaces supported by MapStruct in the reference documentation , as well as the default implementation type corresponding to each interface.

Advanced operations

dependency injection

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

DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);

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 Spring framework:

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

Adding (componentModel = "spring") to the @Mapper annotation is to tell MapStruct that when generating the mapper implementation class, we hope that it can support creation through Spring's dependency injection. Now, there is no need to add the INSTANCE field to the interface.

The DoctorMapperImpl generated this time will be annotated with @Component:

@Component
public class DoctorMapperImpl implements DoctorMapper {
    
    

}

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

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

If you're not using Spring, MapStruct also supports Java CDI :

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

Add default value

The @Mapping annotation has two very practical flags: constant and defaultValue. No matter how the source value is used, a constant value will always be used; if the source value is null, the default value will be used.

Modify DoctorMapper and add a constant and a 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);
}

If the specialty is not available, we replace it with the "Information Not Available" string. Additionally, 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);

        return doctorDto;
    }
}

As you can see, if the return value of doctor.getSpecialty() is null, specialization is set to our default information. In any case, a value will be assigned to id because it is a constant.

Add expression

MapStruct even allows Java expressions to be entered in the @Mapping annotation. You can set defaultExpression (effective when source value is null), or an expression (similar to a constant, effective permanently).

Two new attributes have been added to both the Doctor and DoctorDto classes, one is the externalId of the String type, and the other is the appointment of the LocalDateTime type. 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
}

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
}

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

As you can see, the value of externalId is set to java(UUID.randomUUID().toString()). If there is no availability attribute in the source object, the availability in the target object will be set to a new LocalDateTime object.

Since expressions are just strings, we must specify the class used in the expression. But the expression here is not the final executed code, just a text value of a letter. Therefore, we need to add imports = {LocalDateTime.class, UUID.class} in @Mapper.

Add custom method

The strategy we have used so far is to add a "placeholder" method and expect MapStruct to implement it for us. In fact, we can also add a custom default method to the interface, or directly implement a mapping through the default method. We can then call the method directly through the instance without any problems.

To do this, we create a DoctorPatientSummary class that contains summary information for 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
}

Next, we add a default method in DoctorMapper, which will convert 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();
    }
}

The Builder mode is used here to create the 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);

Create a custom mapper

Previously, we have been designing mapper functions through interfaces. In fact, we can also implement a mapper through an abstract class with @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 modify it into 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();
    }
}

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 @BeforeMapping and @AfterMapping methods.

@BeforeMapping 和 @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 to say, in the final implementation code, these two methods will be added and executed before and after the two objects are actually mapped.

Two methods can be added to DoctorCustomMapper:

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

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

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

Mapping exception handling

Exception handling is inevitable, and the application will generate an abnormal state at any time. MapStruct provides support for exception handling, which can simplify the developer's work.

Consider a scenario where we want to verify the Doctor's data before mapping it to DoctorDto. We create a new independent Validator class for verification:

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

Let's modify DoctorMapper to use the Validator class without specifying an 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() will 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;
}

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

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 type of a pair of attributes before and after mapping is consistent with the method input and output parameter types in Validator, then the method in Validator will be called when mapping the field, 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 already defined a mapping method between two types, when we want to add another mapping method between the same types, we tend to directly copy the mapping configuration of the existing method.

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

Inherit configuration

Let's review "Updating an existing instance". 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);
}

Suppose we have another mapper that converts DoctorDto 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);
}

These two mapping methods use the same annotation configuration, and the source and target are the same. In fact, we can use the @InheritConfiguration annotation to avoid repeated configuration of these two mapper methods.

If you add the @InheritConfiguration annotation to a method, MapStruct will search other configured methods to find annotation configurations that can be used for the current method. Generally speaking, this annotation is used in the update method after the mapping method, as shown below:

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

Inherit reverse configuration

There is another similar scenario, which is to write a mapping function to convert Model to DTO, and convert DTO to Model. As shown in the code below, we have to add the same annotation 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);
}

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

We can use the @InheritInverseConfiguration annotation on the second method to 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);
}

The code generated by these two Mappers is the same.

. . . . . . . . . . . . . Insert image description here

Guess you like

Origin blog.csdn.net/qq_28344049/article/details/122237921