【springboot进阶】优雅使用 MapStruct 进行类复制

目录

 一、MapStruct 介绍

二、MapStruct 配置

三、MapStruct 使用

四、测试

五、遇到的坑

1、java.lang.NoSuchMethodError


项目中经常会遇到这样的一个情况:从数据库读取到数据,并不是直接返回给前端做展示的,还需要字段的加工,例如记录的时间戳是不需要的、一些敏感数据更是不能等等。传统的做法就是创建一个新的类,然后写一堆的get/set方法进行赋值,如果字段很多的话,那简直是噩梦,有时候还担心会漏掉等等。

 一、MapStruct 介绍

MapStruct 简单来说就是一个属性映射工具,主要用于解决数据模型之间不通用的情况。这里主要说一下它的优点在于性能好,像笔者在未接触 MapStruct 以前一直使用的 BeanUtils 工具进行转换,当时也知道这样性能不好,但是为了能偷懒,所以...

其实 MapStruct 也不是有神秘之处,其实原理在于,Java程序执行的过程,是由编译器先把java文件编译成class字节码文件,然后由JVM去解释执行class文件。Mapstruct正是在java文件到class这一步帮我们实现了转换方法,即做了预处理,提前编译好文件。

二、MapStruct 配置

需要引入 mapstruct 和 mapstruct-processor,同时 scope 设置为 provided ,即它只影响到编译,测试阶段。

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

三、MapStruct 使用

这边演示的是一般项目中,从数据库读取到数据,到返回前端展示的过程。

 假设我们有一个student表,实体字段信息如下。

/**
 * <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;


}

但是前端页面展示的时候,某些字段需要调整。例如,学生信息需要展示在首页和列表页,他们的数据模型字段名称是不一致的。

学生首页展示vo 需要调整学生的 id 为 userId, 学生名称为 userName 。

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

    private Integer userId;

    private String userName;

    private Integer age;

    private String sex;

}

学生分页展示vo 需要调整学生的性别为 gender 。 

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

    private Integer id;

    private String name;

    private Integer age;

    private String gender;

}

创建 学生实体的mapper,由于要区分 mybatis-plus 的底层mapper,所以这里的命名以 StructMapper 结尾,尽量避免重名的情况。所以注意 @Mapper 注解也要使用 org.mapstruct 包下的。

/**
 * 学生实体转换接口
 *
 * 定义这是一个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);
}

四、测试

我们创建一个controller,模拟一般项目的接口请求。

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

}

数据表的记录如下

调用首页展示接口的情况如下,可以看到,返回的新字段已经成功赋值。

 接下来,看一下分页的数据,新字段 性别 gender 也同样赋值成功。

五、遇到的坑

1、java.lang.NoSuchMethodError

如果现在我们将学生首页vo类的 age 字段,调整为 userAge,运行项目,在请求一次接口,你会发现这时候会报错,提示找不到 setAge 方法。

为什么会这样呢?其实原因在于上面说的 MapStruct 工作原理,这时候查看转换接口的实现就可以知道是什么情况了。

 实现类还是调用的 setAge 方法进行赋值,但是我们的 StudentHomeVo 已经被我们改过,没有这个方法了,所以运行时候就会报错了。

那么这种情况如何解决了,其实也很简单,重新编译一次项目就可以了。

 重新编译运行,再请求一次接口,可以看到成功返回,并且新字段也有赋值。 

如果发现调整了字段,或者改过转换mapper的东西后,出现奇奇怪怪的情况,一种重新编译一下项目就能解决。

2、复制出现空值

这是一个隐藏很深的坑,以至于运行后出现 Null 异常才发现,为什么会出现复制失败的情况呢,明明字段名称都是一样的。

其实这跟我们使用了 lombok 有关,至于它是个什么东西有什么用,笔者就不在这里阐述,但是有一点很重要,它也是工作在编译阶段的。

我们先看看出问题的 MapStruct 实现类是怎么样的,如下图:

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 只是做了实例化 Vo 的操作,并没有进行赋值!

我们来看看正常情况,如下图:

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

那为什么 MapStruct 没有帮我们进行赋值呢?因为它并没有找到复制字段对应的 get/set 方法啊!

那为什么没有找到呢,明明编译好的 Vo 类里面有的啊!所以这里就涉及到工作顺序的问题,必要要让 lombok 的工作在前面,让它将 Vo 类的 get/set 方法生成了,再让 MapStruct 帮我们进行复制。

所以最终解决方法,调整 pom 文件的依赖加载,必要让 lombok 在 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>

猜你喜欢

转载自blog.csdn.net/lrb0677/article/details/127838138