目录
一、MapStruct 介绍
二、MapStruct 配置
三、MapStruct 使用
四、测试
五、遇到的坑
1、java.lang.NoSuchMethodError
项目中经常会遇到这样的一个情况:从数据库读取到数据,并不是直接返回给前端做展示的,还需要字段的加工,例如记录的时间戳是不需要的、一些敏感数据更是不能等等。传统的做法就是创建一个新的类,然后写一堆的get/set方法进行赋值,如果字段很多的话,那简直是噩梦,有时候还担心会漏掉等等。
MapStruct 简单来说就是一个属性映射工具,主要用于解决数据模型之间不通用的情况。这里主要说一下它的优点在于性能好,像笔者在未接触 MapStruct 以前一直使用的 BeanUtils 工具进行转换,当时也知道这样性能不好,但是为了能偷懒,所以...
其实 MapStruct 也不是有神秘之处,其实原理在于,Java程序执行的过程,是由编译器先把java文件编译成class字节码文件,然后由JVM去解释执行class文件。Mapstruct正是在java文件到class这一步帮我们实现了转换方法,即做了预处理,提前编译好文件。
需要引入 mapstruct 和 mapstruct-processor,同时 scope 设置为 provided ,即它只影响到编译,测试阶段。
org.mapstruct
mapstruct
1.5.0.Final
provided
org.mapstruct
mapstruct-processor
1.5.0.Final
provided
这边演示的是一般项目中,从数据库读取到数据,到返回前端展示的过程。
假设我们有一个student表,实体字段信息如下。
/**
*
* 学生表
*
*
* @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 page() {
List students = studentService.list();
List studentPageVos = students.stream().map(item -> {
StudentPageVo studentPageVo = StudentStructMapper.INSTANCES.toStudentPageVo(item);
return studentPageVo;
}).collect(Collectors.toList());
return studentPageVos;
}
}
数据表的记录如下
调用首页展示接口的情况如下,可以看到,返回的新字段已经成功赋值。
接下来,看一下分页的数据,新字段 性别 gender 也同样赋值成功。
如果现在我们将学生首页vo类的 age 字段,调整为 userAge,运行项目,在请求一次接口,你会发现这时候会报错,提示找不到 setAge 方法。
为什么会这样呢?其实原因在于上面说的 MapStruct 工作原理,这时候查看转换接口的实现就可以知道是什么情况了。
实现类还是调用的 setAge 方法进行赋值,但是我们的 StudentHomeVo 已经被我们改过,没有这个方法了,所以运行时候就会报错了。
那么这种情况如何解决了,其实也很简单,重新编译一次项目就可以了。
重新编译运行,再请求一次接口,可以看到成功返回,并且新字段也有赋值。
如果发现调整了字段,或者改过转换mapper的东西后,出现奇奇怪怪的情况,一种重新编译一下项目就能解决。
这是一个隐藏很深的坑,以至于运行后出现 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 的前面。
org.projectlombok
lombok
org.mapstruct
mapstruct
compile
org.mapstruct
mapstruct-processor
compile