Bean conversion tool MapStruct is enough to read this article

1.Background

In the layered structure applications we develop daily, in order to decouple each layer from each other, different objects are generally defined to transfer data between different layers. Therefore, there are various XXXDTO, XXXVO, XXXBO and other objects derived from database objects, when transferring data between different layers, it is inevitable that these objects often need to be converted to each other.

At this time, there are generally two processing methods:

  • ① Direct use Setter Sum Getter Method conversion< /span>
  • ② One small tool used 4>.BeanUtil.copyProperties).

The first method requires writing a lot of Getter/Setter code if there are many object properties. Although the second method seems much simpler than the first method, because it uses reflection, its performance is not very good, and there are many pitfalls in use. The protagonist MapStruct to be introduced today solves the shortcomings of these two methods without affecting performance.

2.Introduction to mapstruct

Insert image description here

mapstruct is a entity class mapping framework that can pass Java annotationsSafely assign attributes of one entity class to another entity class. It is based onconvention over configuration method which greatly simplifies the implementation of mapping between Java bean types. With mapstruct, you only need to define a mapper interface. Declare the method that needs to be mapped. During the compilation process, mapstruct will automatically generate the implementation class of the interface to achieve the effect of mapping the source object to the target object.

In general, it has the following three characteristics:

  • Based on annotations
  • Automatically generate mapping conversion code at compile time
  • Type safety, high performance, no dependencies

Source code address: https://github.com/mapstruct/mapstruct

3.Comparison of mapstruct and other mappings

In Chapter 1, we also mentioned that there are roughly two types of entity class mapping frameworks:

  • One is through the runtime java reflection mechanism dynamic mapping;
  • The other is to dynamically generate getters/setters during compilation and directly call the class compiled by the framework during runtime to implement entity mapping.

Since mapstruct mapping is implemented during compilation, it has the following advantages over the runtime mapping framework:

  • 1.High security . Because the mapping of source objects to target objects is implemented at compile time, if the compiler can pass it, no error will be reported at runtime.
  • 2.Fast . Fast speed means that the method of the implementation class is directly called during runtime, and reflection is not used for conversion during runtime.

4. Analysis of the underlying principles of mapstruct

mapstruct is implemented based on JSR 269, which is a specification introduced by JDK. With it, it is possible to process annotations at compile time and read, modify and add content in the abstract syntax tree.

JSR 269 usesAnnotation Processor to process annotations during compilation. The Annotation Processor is equivalent to a plug-in for the compiler, so it is also called < /span>. To implement JSR 269, there are mainly the following steps:Plug-in annotation processing

  • 1.Inherit the AbstractProcessor class , and rewrite the process method to implement your own annotation processing logic in the process method.
  • 2.Create the javax.annotation.processing.Processor file in the META-INF/services directory to register your own Annotation Processor

Speaking of this, I have to mention the process of Java program compilation:
Insert image description here

The process from Java source code to class file in the picture above is actually a relatively complicated process. The process can be described as follows:
Insert image description here

The process in the above figure can be summarized into the following steps:

  • 1.Generate abstract syntax tree . The Java compiler compiles the Java source code and generates an Abstract Syntax Tree (AST).
  • 2.Call the program that implements the JSR 269 API . As long as the program implements the JSR 269 API, the implemented annotation processor will be called during compilation.
  • 3.Modify the abstract syntax tree . In the program that implements the JSR 269 API, you can modify the abstract syntax tree and insert your own implementation logic.
  • 4.Generate bytecode . After modifying the abstract syntax tree, the Java compiler will generate a bytecode file corresponding to the modified abstract syntax tree.

5. Specific use and underlying implementation

5.1 Add maven dependencies

	<properties>
		<org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
	</properties>
		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct</artifactId>
			<version>${org.mapstruct.version}</version>
		</dependency>
		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct-processor</artifactId>
			<version>${org.mapstruct.version}</version>
		</dependency>

5.2 Object conversion

We create two objects directly:
UserA, UserB, and then we convert AB to each other

At this time, UserA and UserB have two scenarios:

1.UserA and UserB fields are the same

@Data
public class UserA {
    
    
    private Integer id;
    private String name;
}

Insert image description here

Create an object converter (Mapper)
It should be noted that the converter does not necessarily have to end with Mapper, but the official example recommends naming the converter name in the XXXMapper format, here The example is the simplest mapping case (field names and types match exactly). You only need to add the @Mapper annotation to the converter class. The converter code is as follows:

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

    UserB toUserB(UserA userA);

}

Call Mapper to convert objects

@SpringBootTest
class DemoApplicationTests {
    
    
	@Test
	void test1(){
    
    
		Integer id = 1;
		String name = "ninesun";

		UserA userA = new UserA();
		userA.setId(id);
		userA.setName(name);

		UserB userB = UserMapper.INSTANCE.toUserB(userA);
		assertEquals(id, userB.getId());
		assertEquals(name, userB.getName());
	}
}

Insert image description here

You can see that the test passed

After compilation, the MapStruct annotation processor plug-in will recognize the DoctorMapper interface and generate an implementation class for it.
, you can see its specific implementation on Mapper
Insert image description here

Insert image description here

You can see that the UserMapperImpl class contains a toUserB() method, which helps us implement field mapping and ultimately realizes the conversion from UserA to UserB.

2.UserA and UserB fields are different

If the two field names are different, MapStuct cannot map them directly, and you need to use @Mappings to map the inconsistent fields
We first create an object UserC

@Data
public class UserC {
    
    
    private Integer id;
    private String userName;
}

Compared with UserA, we found that the name field cannot be mapped.
Insert image description here

Just add the toUserC method to Mapper.

UserC toUserC(UserA userA);

Insert image description here

Do a test

	@Test
	void test2(){
    
    
		Integer id = 1;
		String name = "ninesun";

		UserA userA = new UserA();
		userA.setId(id);
		userA.setName(name);

		UserC userC = UserMapper.INSTANCE.toUserC(userA);
		assertEquals(id, userC.getId());
		assertEquals(name, userC.getUserName());
	}

Insert image description here

It can be found that the name of UserC was not converted successfully. The reason is that the name in UserA cannot be mapped to the userName field in UserBC.

How to do it?

The method is also very simple

    @Mappings({
    
    
            @Mapping(source = "name", target = "userName"),
    })
    UserC toUserC(UserA userA);

We only need to manually implement the mapping, and once again allow the Test2 method to test, we can find that the test can pass
Insert image description here

We have mentioned the source and target in the @Mapping attribute above. In addition, it has several basic attributes:

  • ignore : Indicates that the mapping of the current field is ignored
    • true: ignore this field
    • false: do not ignore, default is false
  • defaultValue 默认值
    @Mapping(source = "UserA.specialty", target = "specialization", defaultValue = "Information Not Available")
  • expressions You can use expressions to construct some simple transformation relationships
    Although the design wants to be compatible with many languages, currently it can only write Java code.

The above are all one-to-one situations, that is, the transfer between one object and one object. Let’s take a look at what the many-to-one scenario looks like.

3. Multiple source classes

We add two new objects UserDto and Education

@Data
public class UserDto {
    
    
    private Integer id;
    private String name;

    private String degree;
}
@Data
public class Education {
    
    
    private String degreeName;
    private String institute;
    private Integer yearOfPassing;
}

What we need to do next is to map the attribute values ​​​​in UserA and Education to UserDto. The corresponding contents in UserMapper are as follows:

    @Mappings({
    
    
            @Mapping(source = "userA.name", target = "userName"),
            @Mapping(source = "education.degreeName", target = "degree"),
    })
    UserDto toUserDto(UserA userA, Education education);

Next continue testing

    @Test
    void test5() {
    
    
        Integer id = 1;
        String name = "ninesun";
        String degreeName = "博士";
        UserA userA = new UserA();
        userA.setId(id);
        userA.setName(name);
        Education education = new Education();
        education.setDegreeName(degreeName);
        UserDto userDto = UserMapper.INSTANCE.toUserDto(userA, education);
        System.out.println(JSON.toJSONString(userDto));
    }

Insert image description here

4. Sub-object mapping

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

@Data
public class UserA {
    
    
    private Integer id;
    private String name;
    private List<Course> courseList;
}
@Data
public class Course {
    
    
    private String name;
    private Integer time;
}
@Data
public class UserDto {
    
    
    private Integer id;
    private String userName;
    private String degree;
    private List<CourseDto> courseDtoList;
}
@Data
public class CourseDto {
    
    
    private String name;
    private Integer time;
}

The corresponding Mapper is as follows:

    @Mappings({
    
    
            @Mapping(source = "name", target = "userName"),
            @Mapping(source = "courseList", target = "courseDtoList"),
    })
    UserDto toUserDto(UserA userA);

Write a unit test to test:

    @Test
    void test6() {
    
    
        Integer id = 1;
        String name = "ninesun";
        UserA userA = new UserA();
        userA.setId(id);
        userA.setName(name);
        Course course = new Course();
        course.setName("高等数学");
        course.setTime(12);
        List<Course> courseList = Arrays.asList(course);
        userA.setCourseList(courseList);
        UserDto userDto = UserMapper.INSTANCE.toUserDto(userA);
        System.out.println(JSON.toJSONString(userDto));
    }

Insert image description here

5. Data type conversion

Data type mapping

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 basic types and their corresponding wrapper classes . For example, int and Integer, float and Float, long and Long, boolean and Boolean, etc.
  • Between any basic 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 .
  • Java big number type (java.math.BigInteger, java.math.BigDecimal) and Java basic types (including their wrapper classes) and String.
  • For other details, seeMapStruct official 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.

We modify UserA and add a birthdate field:

@Data
public class UserA {
    
    
    private Integer id;
    private String name;
    private LocalDate birthdate;
    private List<Course> courseList;
}

Add a birthdate of String type to UserDto;

@Data
public class UserDto {
    
    
    private Integer id;
    private String userName;
    private String degree;
    private String birthdate;
    private List<CourseDto> courseDtoList;
}

Mapper mapping

    @Mappings({
    
    
            @Mapping(source = "name", target = "userName"),
            @Mapping(source = "courseList", target = "courseDtoList"),
            @Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
    })
    UserDto toUserDto(UserA userA);

test:

    @Test
    void test7() {
    
    
        Integer id = 1;
        String name = "ninesun";
        UserA userA = new UserA();
        userA.setId(id);
        userA.setName(name);
        userA.setBirthdate(LocalDate.now());
        Course course = new Course();
        course.setName("高等数学");
        course.setTime(12);
        List<Course> courseList = Arrays.asList(course);
        userA.setCourseList(courseList);
        UserDto userDto = UserMapper.INSTANCE.toUserDto(userA);
        System.out.println(JSON.toJSONString(userDto));
    }

Insert image description here

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 the @ValueMapping annotation . Again, this is similar to the @Mapping annotation for normal types.

Let's start by creating two enumerations.

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

unit test:

    @Test
    void test8() {
    
    
        PaymentType paymentType1=PaymentType.CASH;
        PaymentType paymentType2=PaymentType.CARD_VISA;
        PaymentTypeView paymentTypeView1=PaymentTypeMapper.INSTANCE.paymentTypeToPaymentTypeView(paymentType1);
        PaymentTypeView paymentTypeView2=PaymentTypeMapper.INSTANCE.paymentTypeToPaymentTypeView(paymentType2);
        System.out.println(paymentTypeView1);
        System.out.println(paymentTypeView2);
    }

Insert image description here

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 via MappingConstants:

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

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.

6. Collection mapping

Use MapStruct to work with collection mappings the same way you work with simple types

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

If our Mapper lunch only has the following method

List<UserDto> map(List<UserA> userAList);

test:

    @Test
    void test9() {
    
    
        Integer id = 1;
        String name = "ninesun";
        UserA userA = new UserA();
        userA.setId(id);
        userA.setName(name);
        userA.setBirthdate(LocalDate.now());
        Course course = new Course();
        course.setName("高等数学");
        course.setTime(12);
        List<Course> courseList = Arrays.asList(course);
        userA.setCourseList(courseList);
        List<UserA> userAList = Arrays.asList(userA);
        List<UserDto> userDtoList = UserMapper.INSTANCE.map(userAList);
        System.out.println(JSON.toJSONString(userDtoList));
    }

Insert image description here

It can be found that none of the field names with inconsistent names have been converted. At this time, we add the field mapping between entities.

@Mapper
public interface UserMapper {
    
    
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    @Mappings({
    
    
            @Mapping(source = "name", target = "userName"),
            @Mapping(source = "courseList", target = "courseDtoList"),
            @Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
    })
    UserDto toUserDto(UserA userA);

    List<UserDto> map(List<UserA> userAList);
}

Run the test case just now and you can find that all fields are mapped successfully.
Insert image description here

Set and Map mapping

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

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

    Set<UserDto> setConvert(Set<UserA> doctor);

    Map<String, UserDto> mapConvert(Map<String, UserA> doctor);
}

Similarly, if we only need these mappings, field names with different names still cannot be mapped, so we also need to add

   @Mappings({
    
    
            @Mapping(source = "name", target = "userName"),
            @Mapping(source = "courseList", target = "courseDtoList"),
            @Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
    })
    UserDto toUserDto(UserA userA);

To handle the situation where a single object field is inconsistent

Collection mapping strategy
@Data
public class UserA {
    
    
    private Integer id;
    private String name;
    private LocalDate birthdate;
    private List<Course> courseList;
}
@Data
public class UserDto {
    
    
    private Integer id;
    private String userName;
    private String degree;
    private String birthdate;
    private List<CourseDto> courseDtoList;

    public void setCourseDtoList(List<CourseDto> courseDtoList) {
    
    
        this.courseDtoList = courseDtoList;
    }

    public void addCourseDto(CourseDto courseDto) {
    
    
        if (courseDtoList == null) {
    
    
            courseDtoList = new ArrayList<>();
        }
        courseDtoList.add(courseDto);
    }

}

You can find that there is an additional set method and add method in UserDto

    @Mappings({
    
    
            @Mapping(source = "name", target = "userName"),
            @Mapping(source = "courseList", target = "courseDtoList"),
            @Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
    })
    UserDto toUserDto(UserA userA);

Compiled implementation class:
Insert image description here

You can see that the strategy adopted by default is ACCESSOR_ONLY , and use the setter method setCourseDtoList() to write a list to the UserDto object data.

Relative, if using ADDER_PREFERRED as the mapping strategy:

@Mapper(collectionMappingStrategy = CollectionMappingStrategy.ADDER_PREFERRED)
public interface UserMapper {
    
    }

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.
Insert image description here

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.

5.3 Advanced operations

1. Dependency injection

So far we have been accessing the generated mapper via the getMapper() method:
Insert image description here

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

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

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.

At this time, the generated UserMapperImpl will be annotated with @Component:
Insert image description here

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 @Autowire or @Resourece annotations:

@SpringBootTest
class DemoApplicationTests {
    
    
    @Autowired
    UserMapper userMapper;
    @Test
    void test1() {
    
    
        Integer id = 1;
        String name = "ninesun";

        UserA userA = new UserA();
        userA.setId(id);
        userA.setName(name);

        UserB userB = userMapper.toUserB(userA);
        assertEquals(id, userB.getId());
        assertEquals(name, userB.getName());
    }
}

Even if you don’t use the Spring framework (PS: I guess there are no students who use Java who don’t use Spring now), MapStruct also supports Java CDI

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

2.Default value

@Mapping Annotations have two very useful flags: constantconstant and default value a>defaultValue . No matter how the source value is , the constant value will always be used; if If the source value is null, the default value will be used.

@Mapper(componentModel = "spring")
public interface UserMapper {
    
    
    @Mappings({
    
    
            @Mapping(target = "id",constant = "-1"),
            @Mapping(source = "courseList", target = "courseDtoList"),
            @Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
            @Mapping(target = "name", defaultValue = "Information Not Available"),
    })
    UserDto toUserDto(UserA userA);
}

Ifname is not available, we will replace it with"Information Not Available"String, in addition, we hardcode the id to < a i=8>-1.

Insert image description here

3. Add expression

MapStruct even allows entering Java expressions in @Mapping annotations. You can set defaultExpression ( source with a value of < Effective when a i=7>null), or an expression (similar to constant, effective permanently).

 @Mappings({
    
    
            @Mapping(source = "userA.name", target = "userName"),
            @Mapping(source = "education.degreeName", target = "degree"),
            @Mapping(source = "userA.birthdate", target = "birthdate", defaultExpression = "java(LocalDateTime.now())")
    })
    UserDto toUserDto(UserA userA, Education education);

After compilation is completed, you can find:
Insert image description here

Although it has been converted to what we want, we will find that LocalDate has not been introduced. When we develop locally, we can manually introduce it after compilation, but we cannot add it manually when compiling it into a jar or deploying it to the server. This We can add imports = {LocalDate.class} in @Mapper in advance.
Insert image description here

Compile again and you will find that it is normal.

4. Add custom methods

The strategy we have used so far is to add a "placeholder"method , and hope that MapStruct can 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 directlythrough the instance without any problems.

Next we create a UserCourseSummary class

@Data
@Builder
public class UserCourseSummary {
    
    
    private Integer userId;
    private String userName;
    private Integer courseCount;
    private List<String> courseNames;
    private Integer courseTime;
}

Add in UserMapper:

    default UserCourseSummary toUserCourseSummary(UserA userA, Course course) {
    
    
        return UserCourseSummary.builder()
                .userId(userA.getId())
                .userName(userA.getName())
                .courseCount(userA.getCourseList().size())
                .courseNames(userA.getCourseList()
                        .stream()
                        .map(Course::getName)
                        .collect(Collectors.toList()))
                .courseTime(course.getTime())
                .build();
    }

Later we can directly map UserCourseSummary through this method

    @Test
    void test10() {
    
    
        Integer id = 1;
        String name = "ninesun";
        UserA userA = new UserA();
        userA.setId(id);
        userA.setName(name);
        userA.setBirthdate(LocalDate.now());
        Course course = new Course();
        course.setName("高等数学");
        course.setTime(12);
        List<Course> courseList = Arrays.asList(course);
        userA.setCourseList(courseList);
        UserCourseSummary userCourseSummary = userMapper.toUserCourseSummary(userA, course);
        System.out.println(JSON.toJSONString(userCourseSummary));
    }

5. Create a custom mapper

Previously we have been designing mapper functions through interfaces. In fact, we can also design mapper functions through an interface with @Mapper < a i=3>abstract class to implement a mapper. MapStruct will also create an implementation for this class, similar to creating an interface implementation.

@Mapper(componentModel = "spring")
public abstract class UserCustomMapper {
    
    
    public UserCourseSummary toUserCourseSummary(UserA userA, Course course) {
    
    
        return UserCourseSummary.builder()
                .userId(userA.getId())
                .userName(userA.getName())
                .courseCount(userA.getCourseList().size())
                .courseNames(userA.getCourseList()
                        .stream()
                        .map(Course::getName)
                        .collect(Collectors.toList()))
                .courseTime(course.getTime())
                .build();
    }
}
@BeforeMapping 和 @AfterMapping

For further control and customization, we can define @BeforeMapping and @AfterMappingmethod. 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 UserCustomMapper:

    @BeforeMapping
    protected void validate(UserA user) {
    
    
        if(CollectionUtils.isEmpty(user.getCourseList())){
    
    
            user.setCourseList(new ArrayList<>());
        }
    }

    @AfterMapping
    protected void updateResult(@MappingTarget UserDto userDto) {
    
    
        userDto.setUserName(userDto.getUserName().toUpperCase());
        userDto.setDegree(userDto.getDegree().toUpperCase());
    }

And add a new mapping of user->UserDto

    @Mapping(source = "userA.name", target = "userName")
    public abstract UserDto toDoctorDto(UserA userA);

After compilation:
Insert image description here

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.

6. 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 User's data before mapping it to UserDto. We create a new independent Validator class for verification:

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

and modify userMapper

@Mapper(componentModel = "spring", uses = {
    
    Validator.class},imports = {
    
    LocalDate.class,})
public interface UserMapper {
    
    
    @Mappings({
    
    
            @Mapping(target = "id", constant = "-1"),
            @Mapping(source = "courseList", target = "courseDtoList"),
            @Mapping(source = "birthdate", target = "birthdate", dateFormat = "dd/MMM/yyyy"),
            @Mapping(source = "name", target = "userName", defaultValue = "Information Not Available"),
    })
    UserDto toUserDto(UserA userA);

}

Recompile again:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-11-03T14:13:15+0800",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 1.8.0_371 (Oracle Corporation)"
)
@Component
public class UserMapperImpl implements UserMapper {
    
    

    @Autowired
    private Validator validator;
    @Override
    public UserDto toUserDto(UserA userA) {
    
    
        if ( userA == null ) {
    
    
            return null;
        }

        UserDto userDto = new UserDto();

        userDto.setCourseDtoList( courseListToCourseDtoList( userA.getCourseList() ) );
        userDto.setBirthdate( userA.getBirthdate() );
        if ( userA.getName() != null ) {
    
    
            userDto.setUserName( userA.getName() );
        }
        else {
    
    
            userDto.setUserName( "Information Not Available" );
        }

        try {
    
    
            userDto.setId( validator.validateId( -1 ) );
        }
        catch ( ValidationException e ) {
    
    
            throw new RuntimeException( e );
        }

        return userDto;
    }

}

You can find that the parameter verification after compilation is also very elegant.

It is worth noting 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 the field is mapped, so please use this method with caution.

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

7.1 Inherited configuration

Now let's look at this scenario:

We create a mapper that updates the property values ​​of the existing UserDto object based on the properties of the User object.

    @Mapping(source = "userName", target = "name")
    void updateModel(UserA userA, @MappingTarget UserDto userDto);

Of course, we have another way to convert UserDto to User:

    @Mapping(source = "name", target = "userName", defaultValue = "Information Not Available")
    UserDto toUserDto(UserA userA);

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

    @InheritConfiguration
    void updateModel(UserA userA, @MappingTarget UserDto userDto);

Similarly, after we compile, we can see that the two implementations are consistent. The compiled code is too long and is not given here. You can check it yourself after building.

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

    @Mappings({
    
    
            @Mapping(source = "name", target = "userName"),
    })
    UserC toUserC(UserA userA);

    @Mappings({
    
    
            @Mapping(source = "userName", target = "name"),
    })
    UserA toUserA(UserC userC);

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:

    @Mappings({
    
    
            @Mapping(source = "name", target = "userName"),
    })
    UserC toUserC(UserA userA);


    @InheritInverseConfiguration
    UserA toUserA(UserC userC);

After compilation:
Insert image description here

6. Conversion of objects and Maps

We can use the MapStruct tool to easily realize the conversion between objects, and also easily realize the conversion from Map to objects.

6.1 Map to object

As follows:

UserA map2User(Map<String, String> map);

After compilation, we are prompted:
Insert image description here

This sentence means that the String type cannot be converted into a List type Course object. At this time, we need to use the default method to convert it:
Add in UserMapper :

    UserA map2User(Map<String, String> map);

    default List<Course> str2List(String str) {
    
    
        return JSON.parseArray(str, Course.class);
    }

Compile again and you can see:
Insert image description here

6.2 Object to Map

Add in UserMapper

Map<String, String> user2String(UserA userA);

After compiling, I found:
Insert image description here

This error prompts us that the object returned cannot be an abstract class or interface, and needs to return a specific instantiated object. Then we simply modify it:

HashMap<String, String> user2String(UserA userA);

Compile again and find that although no error is reported, the specific implementation is as follows:
Insert image description here

It is equivalent to returning an empty HashMap without doing anything. So what should we do to convert the object into a Map easily and elegantly?

Of course, there is far more than one way to achieve it. I will write down the common ones below, and you can choose one yourself.

  • Implementation method 1: Implementation using ObjectMapper
    default Map<String, String> toMap1(Object object) {
    
    
        if (Objects.isNull(object)) {
    
    
            return new HashMap<>();
        }
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.registerModule(new JavaTimeModule());
        Map<String, String> targetMap = objectMapper.convertValue(object, HashMap.class);
        return targetMap;

    }
  • Implementation method 2: Use reflection to implement. The bottom layer of Map conversion in BeanUtils uses the reflection mechanism.
    default Map<String, String> toMap2(Object object) {
    
    
        Map<String, String> map = new HashMap<>(36);
        ReflectionUtils.doWithFields(object.getClass(), field -> {
    
    
            field.setAccessible(true);
            // 不处理自动化测试的插码字段
            if (field.isSynthetic()) {
    
    
                return;
            }
            Object fieldValue = null;
            try {
    
    
                fieldValue = field.get(object);
            } catch (IllegalAccessException e) {
    
    
                throw new RuntimeException(e);
            }
            if (null != fieldValue) {
    
    
                if (String.class.equals(field.getType())) {
    
    
                    map.put(field.getName(), fieldValue.toString());
                } else {
    
    
                    map.put(field.getName(), JSON.toJSONString(fieldValue));
                }
            }
        });
        return map;
    }
  • Implementation method 3: Implementation using JSON serialization
    default Map<String, String> toMap3(Object object) {
    
    
        if (Objects.isNull(object)) {
    
    
            return new HashMap<>();
        }
        String str = JSON.toJSONString(object);
        Map<String, String> map = JSON.parseObject(str, HashMap.class);
        return map;
    }

Each method has been self-tested, you can try it yourself

6. Summary

In this article, we explored MapStruct, a library for creating mapper classes. From basic mapping to custom methods and custom mappers, in addition, 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 that reduces the developer's effort in writing template code, making the process of creating mappers quick and easy.

If you want to know more about the use and operation of this tool class, you can go to:MapStruct official reference guide

Guess you like

Origin blog.csdn.net/zhiyikeji/article/details/134019776