Mapstruct神器
2024年了你还在用BeanUtil.copy,看看这款神器
1.前言
在项目中,如果我们要遵循分层领域模型规约: 话,肯定避免不了在DTO、VO、BO、AO、VO、Query等实体的转换,我们通常有几种做法:
- 手动一个个字段的赋值
- 通过反序列化的手段,必须先转成JSON字符串,再转回来
- 使用Spring的BeanUtils,提供的克隆方法。
想必上面这几种方式大家都使用过。但是BeanUtils底层是通过反射实现的,复杂场景支持不足,控制 copy 粒度太粗,不易重构。
而前两种方式的话, 重复性工作多,手写代码容易遗漏掉有些字段。
今天我们来介绍关于MapStruct,MapStruct是一个代码生成器,它通过编译时自动生成映射代码,从而避免了手动编写重复且容易出错的映射逻辑。这种方式不仅提高了代码的效率和性能,而且保证了类型的安全性。
2.什么是DTO、DO、BO、AO、VO、Query
在此之前,我们先来认识认识什么是DTO、VO、BO、AO、VO、Query专业名词。
我们项目中会定义各种Object,如下图所示,我把它们分成了三大类
2.1 DTO
Data Transfer Object的简称: 数据传输对象,Service 或 Manager 向外传输的对象。
2.2 DO
Data Object的简称: 此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
2.3 BO
Business Object的简称: 业务对象,由 Service 层输出的封装业务逻辑的对象。
2.4 AO
ApplicationObject的简称: 应用对象,在Web层与Service层之间抽象的复用对象模型, 极为贴近展示层,复用度不高。
2.5 VO
View Object的简称: 数据传输对象,Service向外传输的对象。
2.6 Query
数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止 使用 Map 类来传输。
最难理解的是BO,大致这么理解:
BO这个对象可以包括一个或多个其它的对象。
比如一个简历,有教育经历、工作经历、社会关系等等。
我们可以把教育经历对应一个PO,工作经历对应一个PO,社会关系对应一个PO。
建立一个对应简历的BO对象处理简历,每个BO包含这些PO。这样处理业务逻辑时,我们就可以针对BO去处理。
2.7 示例代码
Controller层
public List getUsers(UserQuery userQuery);
此层常见的转换为:DTO转VO
Service层
// 普通的service层接口
List getUsers(UserQuery userQuery);
然后在Service内部使用UserBO封装中间所需的逻辑对象
// 来自前端的请求
List getUsers(UserAO userAo);
此层常见的转换为:DO转BO、BO转DTO
DAO层
List getUsers(UserQuery userQuery);
3.mapstruct的作用
- 自动生成映射代码: 通过注解配置,MapStruct 可以在编译时自动生成符合映射规则的 JavaBean 映射代码,减少了手动编写代码的工作量。
- 类型转换支持: MapStruct 提供了丰富的类型转换支持,可以处理不同类型之间的映射转换,包括基本类型、集合类型等。
- 易于使用: MapStruct 的注解简单明了,易于理解和配置,同时提供了灵活的定制选项,适应各种复杂映射需求。
- 高性能: 生成的映射代码是高度优化的,性能较高,适用于对性能要求较高的应用场景。
4.mapstruct哪点比BeanUtils好?
- 支持复杂属性赋值
- 效率高,在编译时直接给你生成代码,相当与帮你手动去一个个赋值
- 支持不同字段间的赋值,通过注解实现
假如没有使用 MapStruct 的话,当我们需要把 DO 对象转成一个 VO 对象时,我们需要这样做。
public static UserInfoVO originalCopyItem(UserDTO userDTO){
UserInfoVO userInfoVO = new UserInfoVO();
userInfoVO.setUserName(userDTO.getName());
userInfoVO.setAge(userDTO.getAge());
userInfoVO.setBirthday(userDTO.getBirthday());
userInfoVO.setIdCard(userDTO.getIdCard());
userInfoVO.setGender(userDTO.getGender());
userInfoVO.setIsMarried(userDTO.getIsMarried());
userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
userInfoVO.setAddress(userDTO.getAddress());
return userInfoVO;
}
传统的方法一般是采用硬编码,将每个对象的值都逐一设值。当然为了偷懒也会有采用一些BeanUtil简约代码的方式:
public UserInfoVO utilCopyItem(UserDTO userDTO){
UserInfoVO userInfoVO = new UserInfoVO();
//采用反射、内省机制实现拷贝
BeanUtils.copyProperties(userDTO, userInfoVO);
return userInfoVO;
}
但是,像BeanUtils这类通过反射、内省等实现的框架,在速度上会带来比较严重的影响。尤其是对于一些大字段、大对象而言,这个速度的缺陷就会越明显。针对速度这块我还专门进行了测试,对普通的setter方法、BeanUtils的拷贝以及本次需要介绍的mapperStruct进行了一次对比。得到的耗时结果如下所示:
运行次数 | setter方法耗时 | BeanUtils拷贝耗时 | MapperStruct拷贝耗时 |
---|---|---|---|
1 | 2921528(1) | 3973292(1.36) | 2989942(1.023) |
10 | 2362724(1) | 66402953(28.10) | 3348099(1.417) |
100 | 2500452(1) | 71741323(28.69) | 2120820(0.848) |
1000 | 3187151(1) | 157925125(49.55) | 5456290(1.711) |
10000 | 5722147(1) | 300814054(52.57) | 5229080(0.913) |
100000 | 19324227(1) | 244625923(12.65) | 12932441(0.669) |
以上单位均为毫微秒。括号内的为当前组件同Setter比较的比值。可以看到BeanUtils的拷贝耗时基本为setter方法的十倍、二十倍
以上。而MapperStruct方法拷贝的耗时,则与setter方法相近。由此可见,简单的BeanUtils确实会给服务的性能带来很大的压力。而MapperStruct拷贝则可以很好的解决这个问题。
5.SpringBoot整合mapstruct
5.1 引入pom依赖
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- 不是必要的,哥们只是比较懒 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
它们的作用如下:
- org.mapstruct:mapstruct:
- MapStruct 的核心库。它提供了 MapStruct 所需的主要注解和工具方法,例如 @Mapper, @Mapping 等注解以及 Mappers.getMapper() 方法。
- 在运行时,这个库是必需的,生成的映射代码会依赖它。
- org.mapstruct:mapstruct-processor:
- MapStruct 的注解处理器。它在编译时生成具体的映射实现代码。
- compile 作用域,意味着它只在编译时被使用。
- 当你编译一个使用了 MapStruct 注解的项目时,注解处理器会检测你的代码,然后为你的 @Mapper 注解的接口或抽象类生成实现。
5.2 创建 DO、VO
创建实体类Student跟StudentVo
@Data
@AllArgsConstructor
public class Student {
private String name;
private int age;
}
@Data
public class StudentVO {
private String name;
private Integer age;
}
5.3 创建转换器接口
定义了一个接口 StudentMapper,该接口的主要作用是将 Student对象转换为 StudentVO对象。
@Mapper
public interface StudentMapper {
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
StudentVO tostudentVO(Student student);
@Mappings({
@Mapping(source = "age", target = "ageVo"),
@Mapping(source = "name", target = "nameVo")
})
}
@Mapper
这是 MapStruct 的核心注解之一。它标记了这个接口为一个映射器,并告诉 MapStruct 的注解处理器在编译时为此接口生成实现。
INSTANCE常量
Mappers.getMapper 是 MapStruct 提供的一个工具方法,用于在不使用 Spring 或其他依赖注入框架的情况下获取映射器的实例。
5.4 进行测试
@SpringBootTest(classes = {MapStructApplication.class})
public class StudentTest {
/**
* 用于测试:01
*/
@Test
public void test01() {
Student student = new Student("张三", 18);
StudentVO studentVO = StudentMapper.INSTANCE.tostudentVO(student);
System.out.println("studentVO = " + studentVO);
}
}
我们可以看到,已经成功将student实体类赋值给StudentVO。
6.mapstruct其他操作
6.1 不同字段映射
我们的实际开发中,原实体类跟我们需要转换的VO的字段名可能不太一样,那这个时候我们该怎么办,mpapstruct是否可以帮我们进行转换呢,我们接着往下聊。
我们创建第二个VO,名字叫做StudentVo2
@Data
public class StudentVO2 {
private String nameVo;
private Integer ageVo;
}
编写mapper转换器,使用@Mappings来映射多个字段不一致情况,如果只有一个字段,使用@mapping即可。
@Mappings({
@Mapping(source = "age", target = "ageVo"),
@Mapping(source = "name", target = "nameVo")
})
StudentVO2 tostudentVO2(Student student);
6.2 @Mappings注解
org.mapstruct 包下的 @Mappings 和 @DecoratedWith 注解是 MapStruct 中用于定义映射规则和装饰器的注解。
@Mappings 注解用于定义多个 @Mapping 注解,表示多个字段之间的映射关系。@Mapping 注解用于指定源对象和目标对象之间的字段映射关系,例如:
@Mappings({
@Mapping(source = "age", target = "ageVo"),
@Mapping(source = "name", target = "nameVo")
})
StudentVO2 tostudentVO2(Student student);
上述代码中,使用 @Mappings 注解定义了两个 @Mapping 注解,表示 age映射到 ageVo,name映射到 nameVo。
6.3 List转换
List<StudentVO> studentListVo2Dto(List<StudentVO> vo);
6.4 Spring依赖注入
在前面的案例中,我们一直在使用 Mappers.getMapper 来获取映射器 INSTANCE
StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
如果是在 Spring 环境下,还可以在 @Mapper 注解中添加 componentModel = "spring" 参数来告诉 MapStruct 在生成映射实现类的时候,提供 Spring 依赖注入。
这样我们在使用映射器的时候,可以直接通过 @Autowired 注解来注入 ColumnStructMapper 对象,然后就可以直接这样使用。
@Autowired
private StudentMapper studentMapper;
StudentVO tostudentVO(Student student);
这样我们以后就需要映射器接口中添加 INSTANCE 了。
7.MapStruct的IDEA插件
我们可以通过在Intellij IDEA 中安装MapStruct插件。
直接点击install即可。
安装完成后,可以直接在 @Mapper 接口和它的实现类之间快速导航。
点击之后,就可以到对应的实体类中了。
8.总结
以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是Leo,一个在互联网行业的小白,立志成为更好的自己。
如果你想了解更多关于Leo,可以关注公众号-程序员Leo,后面文章会首先同步至公众号。