一. 背景
由于每个人对对象映射的使用工具不一致,导致各个应用存在对象映射工具使用混杂多样,没有一个统一对象映射工具去规范使用,各个对象映射的工具性能也参差不齐;有的通过反射实现,比较损耗性能;有的会进行类型自动转换,容易导致数据问题,针对以上的一些问题,对对象映射框架进行一次调研。
二. 对象映射框架性能对比
| 工具 | 实现方式 | star | 最近更新时间 | 说明 |
|---|---|---|---|---|
| Getter/Setter | getter/setter方法 | 属性很多时不好维护 | ||
| Mapstruct | getter/setter方法 | 6k | 2023-02-05 | 基于JSR269,在在编译期生成对象映射代码 |
| Orika | 动态生成字节码 | 1.2k | 2022-07-13 | 基于javassist生成对象映射字节码,并加载生成的字节码文件 |
| Spring BeanUtils | 反射机制 | 基于Spring反射工具类 | ||
| BeanCopier | 基于ASM的MethodVisitor为field赋值 | 4.6k | 2022-02-08 | 使用ASM的MethodVisitor直接编写各属性的get/set方法 |
| Dozer | 反射机制 | 2k | 2023-01-06 | 大量反射,主要基于Field.set(obj, obj)为field赋值 |
| Apache BeanUtils | 反射机制 | 236 | 2023-02-25 | |
| ModelMapper | 反射机制 | 2.1k | 2022-12-08 |
1、压测
机器处理器:M1 Pro (10 核中央处理器和16 核图形处理器)
机器内存:32G
平均耗时/吞吐量(JMH基准测试预热3次,每次3秒,真正执行5次,每次3次)
| 工具 | 5个线程拷贝1w次 | 5个线程拷贝10w次 | 5个线程拷贝100w次 | 5个线程拷贝500w次 | 排名 | ||||
|---|---|---|---|---|---|---|---|---|---|
| 吞吐量(ops/s) | 平均耗时(ms) | 吞吐量(ops/s) | 平均耗时(ms) | 吞吐量(ops/s) | 平均耗时(ms) | 吞吐量(ops/s) | 平均耗时(ms) | ||
| Mapstruct | 226.243 | 4 | 27.801 | 35 | 2.481 | 387 | 0.289 | 4168 | 1 |
| Orika | 21.139 | 45 | 4.714 | 206 | 0.695 | 1423 | - | - | 4 |
| Spring BeanUtils | 125.025 | 7 | 15.377 | 63 | 1.234 | 770 | 0.147 | 6745 | 3 |
| BeanCopier | 188.295 | 5 | 22.215 | 46 | 1.956 | 523 | 0.214 | 4332 | 2 |
| Dozer | 22.829 | 46 | 2.225 | 457 | 0.204 | 4872 | - | - | 6 |
| Apache BeanUtils | 29.679 | 33 | 3.020 | 29511 | - | - | - | - | 7 |
| ModelMapper | 21.575 | 46 | 2.279 | 434 | 0.208 | 4897 | - | - | 5 |
综上比较可知性能比较好的对象映射框架为MapStruct和cglib下的BeanCopier,但BeanCopier只拷贝名称和类型都相同的属性;即便基本类型与其对应的包装类型也不能相互转换, 而且BeanCopier社区活跃度没有MapStruct好,最近更新时间也22年2月8号,几乎不怎么更新了,所以最佳的选择是MapStruct。
三. MapStruct用法
1、pom.xml
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.2.Final</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.2.Final</version>
</dependency>
2、定义Mapper
@Mapper(componentModel="spring")
public interface SourceToTargetMapper {
SourceToTargetMapper INSTANCE = Mappers.getMapper(SourceToTargetMapper.class);
Target to(Source source);
}
3、使用
Target target=SourceToTargetMapper.INSTANCE.to(buildSource());
四. MapStruct问题
1、手动编写mapper太麻烦
2、mapper太多记不住,使用不够方便,没Util来的方便
五. MapStruct原理
APT:APT(编译时注解处理器)即为Annotation Processing Tool,是JSR269规范提供的一套API。APT可以用来在编译时扫描和处理注解。通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。APT的核心是AbstractProcessor类
六. MapStruct增强方案
技术栈:APT+JavaPoet+SPI
JavaPoet:JavaPoet是square推出的开源java代码生成框架,提供Java Api生成.java源文件。
1、手动编写mapper太麻烦
解决方案:编译阶段自动生成Mapper,减少手动编写Mapper
2、mapper太多记不住,使用不够方便
解决方案:封装MapStructUtils代替Mapper调用,使用更方便,但有一定的性能损耗
七. MapStruct增强后使用以及性能压测
1、使用
引入pom
<dependency>
<groupId>com.xxx.xxx</groupId>
<artifactId>mapstruct</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
DTO上定义注解
@Data
@Builder
@AutoMap(targets = {Target.class},componentModel = "spring")
public class Source {
private Integer id;
private String name;
}
@Data
@Builder
@AutoMap(targets = {Source.class})
public class Target {
private Integer id;
private String name;
private Integer shorName;
}
使用:
方式一(推荐)
Target target= SourceTargetMapper.INSTANCE.convert(buildSource());
方式二
Target target= MapStructUtils.convert(buildSource(), Target.class);
2、性能压测
| 工具 | 5个线程拷贝1w次 | 5个线程拷贝10w次 | 5个线程拷贝100w次 | 5个线程拷贝500w次 | 排名 | ||||
|---|---|---|---|---|---|---|---|---|---|
| 吞吐量(ops/s) | 平均耗时(ms) | 吞吐量(ops/s) | 平均耗时(ms) | 吞吐量(ops/s) | 平均耗时(ms) | 吞吐量(ops/s) | 平均耗时(ms) | ||
| Mapstruct | 226.243 | 4 | 27.801 | 35 | 2.481 | 387 | 0.289 | 4168 | 1 |
| BeanCopier | 188.295 | 5 | 22.215 | 46 | 1.956 | 523 | 0.214 | 4332 | 2 |
| MapStructUtil | 110.439 | **7 ** | 13.536 | 76 | 1.250 | **837 ** | **0.150 ** | 6669 |
八. MapStruct增强后优缺点
优点:
1、无需再定义Mapper,编译阶段自动生成
2、封装了Util,使用简单
3、增强后依旧在编译阶段运行,不影响原生MapStruct的功能使用,不会影响程序性能
4、解决了Mapstruct原生默认不支持lombok的@Builder注解导致父类不生成set方法的问题
缺点:
1、封装的Util比原生的Mapper性能差一点,但比其他对象拷贝框架(非MapStruct)性能还是强很多
2、若开发人员手动在dto目标新建apt目录,且新增跟生成的Mapper名称一样的文件的话会不兼容,概率较低
3、增强后能解决90%的使用场景,但一些自定义的注解在自动生成的Mapper里未集成,比如不同名称字段映射等