[springboot advanced] Elegant use of MapStruct for class replication

Table of contents

 1. Introduction to MapStruct

Two, MapStruct configuration

3. Use of MapStruct

4. Test

Five, the pit encountered

1、java.lang.NoSuchMethodError


Projects often encounter such a situation: the data read from the database is not directly returned to the front end for display, and field processing is required. For example, the timestamp of the record is not needed, and some sensitive data cannot etc. The traditional way is to create a new class, and then write a bunch of get/set methods to assign values. If there are many fields, it is a nightmare, and sometimes I worry about missing them.

 1. Introduction to MapStruct

MapStruct is simply an attribute mapping tool, which is mainly used to solve the incompatibility between data models. Here we mainly talk about its advantage of good performance, like the BeanUtils tool that the author has been using before I contacted MapStruct for conversion. I also knew that the performance was not good at the time, but in order to be lazy, so...

In fact, MapStruct is not mysterious. In fact, the principle of Java program execution is that the compiler first compiles the java file into a class bytecode file, and then the JVM interprets and executes the class file. Mapstruct helps us realize the conversion method at the step of java file to class, that is, preprocessing and compiling the file in advance.

Two, MapStruct configuration

Mapstruct and mapstruct-processor need to be introduced, and the scope is set to provided, that is, it only affects the compilation and testing phases.

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.0.Final</version>
    <scope>provided</scope>
</dependency>

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.0.Final</version>
    <scope>provided</scope>
</dependency>

3. Use of MapStruct

The demonstration here is the process of reading data from the database and returning to the front-end display in a general project.

 Suppose we have a student table, and the entity field information is as follows.

/**
 * <p>
 * 学生表
 * </p>
 *
 * @author Liurb
 * @since 2022-11-13
 */
@Getter
@Setter
@TableName("demo_student")
public class Student implements Serializable {

    private static final long serialVersionUID = 1L;

    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * 学生名称
     */
    @TableField("`name`")
    private String name;

    /**
     * 学生年龄
     */
    @TableField("age")
    private Integer age;

    /**
     * 学生性别
     */
    @TableField("sex")
    private String sex;

    /**
     * 创建时间
     */
    @TableField("created_at")
    private LocalDateTime createdAt;


}

But when the front-end page is displayed, some fields need to be adjusted. For example, student information needs to be displayed on the home page and list page, and their data model field names are inconsistent.

The student's home page display vo needs to adjust the student's id to userId, and the student's name to userName.

/**
 * 学生首页展示vo
 *
 *
 * @Author Liurb
 * @Date 2022/11/13
 */
@Data
public class StudentHomeVo {

    private Integer userId;

    private String userName;

    private Integer age;

    private String sex;

}

The student page display vo needs to adjust the student's gender to gender. 

/**
 * 学生分页展示vo
 *
 *
 * @Author Liurb
 * @Date 2022/11/13
 */
@Data
public class StudentPageVo {

    private Integer id;

    private String name;

    private Integer age;

    private String gender;

}

Create the mapper of the student entity. Since it is necessary to distinguish the underlying mapper of mybatis-plus, the naming here ends with StructMapper, and try to avoid duplication of names. So note that the @Mapper annotation should also be used under the org.mapstruct package.

/**
 * 学生实体转换接口
 *
 * 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *
 * @Author Liurb
 * @Date 2022/11/13
 */
@Mapper
public interface StudentStructMapper {

    /**
     * 获取该类自动生成的实现类的实例
     *
     */
    StudentStructMapper INSTANCES = Mappers.getMapper(StudentStructMapper.class);

    /**
     * 这个方法就是用于实现对象属性复制的方法
     *
     * @Mapping 注解 用于定义属性复制规则
     * source 指定源对象属性
     * target指定目标对象属性
     *
     * @param student 这个参数就是源对象,也就是需要被复制的对象
     * @return 返回的是目标对象,就是最终的结果对象
     */
    @Mappings({
            @Mapping(source = "id", target = "userId"),
            @Mapping(source = "name", target = "userName")
    })
    StudentHomeVo toStudentHomeVo(Student student);

    /**
     * 也可以实现多个复制方法,一般将一个实体源对象的转换写在一起
     *
     * @param student
     * @return
     */
    @Mapping(source = "sex", target = "gender")
    StudentPageVo toStudentPageVo(Student student);
}

4. Test

We create a controller to simulate the interface requests of general projects.

/**
 * mapstruct实例控制器
 *
 * @Author Liurb
 * @Date 2022/11/13
 */
@RestController
@RequestMapping("/demo_api/mapstruct")
public class MapStructController {

    @Resource
    StudentService studentService;

    @GetMapping("/home/{id}")
    public StudentHomeVo home(@PathVariable("id")Integer id) {

        Student student = studentService.getById(id);

        StudentHomeVo studentHomeVo = StudentStructMapper.INSTANCES.toStudentHomeVo(student);

        return studentHomeVo;
    }

    @GetMapping("/page")
    public List<StudentPageVo> page() {
        List<Student> students = studentService.list();

        List<StudentPageVo> studentPageVos = students.stream().map(item -> {

            StudentPageVo studentPageVo = StudentStructMapper.INSTANCES.toStudentPageVo(item);
            return studentPageVo;
        }).collect(Collectors.toList());

        return studentPageVos;
    }

}

The records of the data table are as follows

The situation of calling the home page display interface is as follows. You can see that the returned new field has been successfully assigned.

 Next, look at the paged data, and the new field gender is also successfully assigned.

Five, the pit encountered

1、java.lang.NoSuchMethodError

If we now adjust the age field of the vo class on the student homepage to userAge, run the project, and request the interface once, you will find that an error will be reported at this time, indicating that the setAge method cannot be found.

Why is this so? In fact, the reason lies in the working principle of MapStruct mentioned above. At this time, you can know what is going on by looking at the implementation of the conversion interface.

 The implementation class still calls the setAge method for assignment, but our StudentHomeVo has been modified by us, and there is no such method, so an error will be reported when running.

So how to solve this situation is actually very simple, just recompile the project once.

 Recompile and run, and request the interface again, you can see that the return is successful, and the new field is also assigned. 

If you find that after adjusting the fields or changing the mapper, there are strange situations, you can solve it by recompiling the project.

2. A null value appears in the copy

This is a hidden pit that is so deep that it is discovered that a Null exception occurs after running. Why does the replication fail? Obviously, the field names are the same.

In fact, this is related to our use of lombok. As for what it is and what it is useful for, the author will not elaborate here, but one thing is very important, it is also working in the compilation phase.

Let's take a look at the problematic MapStruct implementation class, as shown in the figure below:

public class StudentStructMapperImpl implements StudentStructMapper {
    public StudentStructMapperImpl() {
    }

    public StudentHomeVo toStudentHomeVo(Student student) {
        if (student == null) {
            return null;
        } else {
            StudentHomeVo studentHomeVo = new StudentHomeVo();
            return studentHomeVo;
        }
    }

    public StudentPageVo toStudentPageVo(Student student) {
        if (student == null) {
            return null;
        } else {
            StudentPageVo studentPageVo = new StudentPageVo();
            return studentPageVo;
        }
    }
}

MapStruct just instantiated Vo, and did not assign a value!

Let's take a look at the normal situation, as shown below:

public class StudentStructMapperImpl implements StudentStructMapper {
    public StudentStructMapperImpl() {
    }

    public StudentHomeVo toStudentHomeVo(Student student) {
        if (student == null) {
            return null;
        } else {
            StudentHomeVo studentHomeVo = new StudentHomeVo();
            studentHomeVo.setUserId(student.getId());
            studentHomeVo.setUserName(student.getName());
            studentHomeVo.setAge(student.getAge());
            studentHomeVo.setSex(student.getSex());
            return studentHomeVo;
        }
    }

    public StudentPageVo toStudentPageVo(Student student) {
        if (student == null) {
            return null;
        } else {
            StudentPageVo studentPageVo = new StudentPageVo();
            studentPageVo.setGender(student.getSex());
            studentPageVo.setId(student.getId());
            studentPageVo.setName(student.getName());
            studentPageVo.setAge(student.getAge());
            return studentPageVo;
        }
    }
}

So why didn't MapStruct help us with the assignment? Because it did not find the get/set method corresponding to the copied field!

Then why didn't I find it, there are clearly compiled Vo classes in it! So here is the issue of work sequence. It is necessary to let lombok work first, let it generate the get/set method of the Vo class, and then let MapStruct help us copy it.

So the final solution is to adjust the dependency loading of the pom file, so that lombok must be in front of MapStruct.

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct</artifactId>
			<scope>compile</scope>
		</dependency>

		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct-processor</artifactId>
			<scope>compile</scope>
		</dependency>

Guess you like

Origin blog.csdn.net/lrb0677/article/details/127838138