最近安琪拉在疯狂卷项目,看了看最近的git 提交记录,非常有成就感,再看看案头上落的头发,又极其悲伤,现在饶头都非常小心,只用指腹轻轻的揉搓,哎。。。。????。
说正题,每次卷项目总会发现一些市面上的一些新东西,比如好用的开源库,新出的特性,这次就又让我用到一个仓库,觉得十分的爽。
这个库就是mapstruct,我在读者群里做了调研,发现还是很多人在用的。我今天才把git源码下载下来,开始尝鲜,out了。。
对象属性拷贝在实际工程中应用场景还是蛮多的,比如从网络或者数据库查询了一个对象,DO对象,dao层往core层传递对象的时候做一层对象转化,DO对象转为DTO对象,core层往视图层返回数据DTO转为VO对象,可能很多字段名和属性都是一样的,一个个写setter方法肯定是要废了,为了提升ctrl+c/v 效率,聪明的工程师们发明了很多好用的对象属性复制工具,那接下来我们看下都有哪些?
BeanUtils
相信大家不会陌生,经常在业务代码里面出现 BeanUtils.copyProperties(source,target)
代码,但是BeanUtils 分为 Apache
和 Spring 实现的,一般实际项目是禁止使用Apache 的,因为有很严重的性能问题,《阿里开发手册》 有这么一条
【强制】避免用Apache Beanutils进行属性的copy。说明:Apache BeanUtils性能较差,可以使用其他方案比如Spring BeanUtils, Cglib BeanCopier,注意均是浅拷贝。反例:[性能提升300%:Apache的BeanUtils的坑]
即使是使用 Spring BeanUtils,也要小心,这里面有深拷贝和浅拷贝的概念,比如原始对象有个List users,赋值到目标对象中,那引用赋值之后,原始数据和目标数据任何一方修改了之后都会互相影响,安琪拉见过之前就因为这个原因出现过线上问题。
下面是源代码,核心是调用setter 和 getter方法(这里的readMethod和 writeMethod)实现属性赋值。
下面顺便给大家撸了一下getter 方法的源码,其实不神秘,就是 get + 属性名(首字母大写)。setter方法类似。如下图:
大家注意到没有,如果是boolean 类型的,处理逻辑不太一样,get方法是加 “is”, 这就是为什么项目开发都会禁止boolean类型变量是“is” 打头,因为框架有规约,boolean 类型的get方法不是getBoolean(), 而是 isXXX(),而且一般boolean 类型属性的获取方法大家都约定 isXXX(),大家可以检查一下自己项目里面boolean类型的变量是不是遵守规范了。
beanCopier
这个工具的性能跟原生的set、get差不多,示例代码:
BeanCopier beanCopier = BeanCopier.create(UserDO.class, UserDTO.class, false);
beanCopier.copy(source, target, null);
为什么BeanCopier 性能好呢?不同于BeanUtils 的反射,BeanCopier 直接通过 BeanCopier.create
生成了一个代理类,通过字节码直接生成 set 和 get 方法,不是运行时反射获取set、get方法,但是 BeanCopier 比较死板,配置化不足,比如要忽略某些属性,属性类型不同需要做一下转化就有点费事。
性能方面:
# Run complete. Total time: 00:10:09
Benchmark Mode Cnt Score Error Units
BeanUtilsBenchmark.testApacheBeanUtils thrpt 20 15.972 ± 0.490 ops/ms
BeanUtilsBenchmark.testCglibBeanCopier thrpt 20 47896.804 ± 535.622 ops/ms
BeanUtilsBenchmark.testCustomizedBeanUtils thrpt 20 72.137 ± 2.519 ops/ms
BeanUtilsBenchmark.testNativeCopy thrpt 20 46414.463 ± 1093.329 ops/ms
BeanUtilsBenchmark.testSpringBeanUtils thrpt 20 273.154 ± 10.125 ops/ms
可以看到BeanCopier 接近Native的性能。
JSON
Fastjson 或者 其他json工具也可以,先把对象序列化,然后反序列化,太傻了,不讲了。
UserDTO target = JSON.parseObject(JSON.toJSONString(userDO), UserDTO.class);
mapstruct
这个就是我们今天的主角。
性能方面和原始的差不多,因为也是根据注解直接生成代码,还非常友好,支持很多个性化场景。
给大家介绍一下用法:
比如我要实现一个Car对象转为CarDto,属性有些差异,需要将numberOfSeats 属性复制到seatCount属性,flagList类型也不一致(String 和 List),其他属性一致,如下图所示:
//Car类
@Data
public class Car {
private Long id;
private String logo;
private int numberOfSeats;
private String flagList;
}
//CarDto类
@Data
public class CarDto {
private Long id;
private String logo;
private int seatCount;
private List<String> flagList;
}
实现转化类,只需要定义一个接口类就ok了,如下图:
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mappings
({
@Mapping(target = "seatCount", source = "numberOfSeats"),
@Mapping(target = "flagList", expression = ("java(convert(source.getFlagList()))"))
})
CarDto convertToDto(Car source);
default List<String> convert(String source) {
return Lists.newArrayList(source);
}
}
实现一个接口,使用@Mapper注解修饰,大家基本一看就懂,@Mapping是用来映射,source 是原始属性,target 是目标属性。
expression 可以实现自定义的属性转化,java代表是java语法,其实原理很简单,就是mapstruct 帮你按照注解生成了一份代码,O(∩_∩)O哈哈~ 还以为是什么牛逼技术,生成的代码如下:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2021-10-18T21:00:37+0800",
comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_151 (Oracle Corporation)"
)
public class CarMapperImpl implements CarMapper {
@Override
public CarDto convertToDto(Car source) {
if ( source == null ) {
return null;
}
CarDto carDto = new CarDto();
carDto.setSeatCount( source.getNumberOfSeats() );
carDto.setId( source.getId() );
carDto.setLogo( source.getLogo() );
carDto.setFlagList( convert(source.getFlagList()) );
return carDto;
}
}
工具还有很多其他属性,比如忽略:ignore,忽略某些属性的赋值。
项目开源地址:
https://github.com/mapstruct/mapstruct?spm=ata.21736010.0.0.36173fc0Ma7qPg#building-from-source
项目使用手册
https://mapstruct.org/documentation/stable/reference/html/#Preface