Bald 发表于 2021-10-17 21:08:06

还在用BeanUtils拷贝对象?MapStruct才是王者!「附源码」

前几天,远在北京的小同伴在群里抛出了“MapStruct”的概念。对于只闻其名,未见其人的我来说,决定对其研究一番。本文我们就从 MapStruct 的概念出发,通过具体的代码示例来研究它的使用环境,末了与“市面上”的其它工具来做个对比!
官方介绍

首先我们打开 MapStruct 的官网地址,映入眼帘的就是下边的三步曲:
What is it?
MapStruct 是一个代码生成器,它基于约定优先于配置的方法大大简化了 JavaBean 类型之间映射的实现。生成的映射代码使用平凡方法调用,因此速度快、类型安全且易于理解。
Why?
多层应用步伐通常需要在不同的对象模型(例如实体和 DTO)之间进行映射。编写这样的映射代码是一项乏味且容易出错的使命。MapStruct 旨在通过尽可能自动化来简化这项工作。
与其他映射框架不同,MapStruct 在编译时生成 bean 映射,这确保了高性能,允许快速的开发人员反馈和彻底的错误检查。
How?
MapStruct 是插入 Java 编译器的注释处理器,可以在命令行构建(Maven、Gradle等)中使用,也可以在首选 IDE 中使用。它使用合理的默认值,但在配置或实现特殊举动时,用户可以自定义实现。
官网的表明总是咬文嚼字,艰涩难明的,看到这你只需要记住 MapStruct 是用来做实体类映射——实体类拷贝 的就可以了。
源码地址:https://github.com/mapstruct/mapstruct
官网推荐的 Demo: https://github.com/mapstruct/mapstruct-examples
简朴实现

我们留意到官网中有涉及到简朴样例的实现,我们用2分钟来分析一波:
1. 引入依赖
        org.mapstruct        mapstruct-jdk8        1.3.0.Final//注解处理器,根据注解自动生成mapper的实现        org.mapstruct        mapstruct-processor        1.2.0.Final我们在编译时会报 java: No property named "numberOfSeats" exists in source parameter(s). Did you mean "null"? 错误,经过查阅资料发现 mapstruct-processor 和 Lombok 的版本需要同逐一下:mapstruct-processor:1.2.0.Final , Lombok:1.16.14。
2. 准备实体类 Car.java 和 数据传输类 CarDto.java
@NoArgsConstructor@AllArgsConstructor@Datapublic class Car {    private String make;    private int numberOfSeats;    private CarType type;}@Data@NoArgsConstructor@AllArgsConstructorpublic class CarDto {    private String make;    private int seatCount;    private String type;}3. 创建映射器接口,里边定义映射方法
@Mapperpublic interface CarMapper {   CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );    @Mapping(source = "numberOfSeats", target = "seatCount")    CarDto carToCarDto(Car car);    }解析分析:

[*]@Mapper 将接口标志为映射接口,并允许 MapStruct 处理器在编译期间启动。这里的 @Mapper 注解不是 mybatis 的注解,而是 org.mapstruct.Mapper 的;
[*]实际映射方法 carToCarDto() 期望源对象 Car 作为参数,并返回目标对象 CarDto ,方法名可以自由选择;
[*]对于源对象和目标对象中具有不同名称的属性,可以使用 @Mapping 注释来配置名称;
[*]对于源对象和目标对象中具有不同类型的属性,也可以使用 @Mapping 注释来进行转换,比如:类型属性将从枚举类型转换为字符串;
[*]一个接口中可以有多个映射方法,对于全部的这些方法,MapStruct 将生成一个实现;
[*]该接口的实现实例可以从 Mappers 中获得,接口声明一个 INSTANCE,为客户端提供对映射器实现的访问。
4. 实现类
我们可以将代码进行编译,然后会发现在 target 文件中生成了 CarMapperImpl.class 文件:
https://p3.toutiaoimg.com/large/pgc-image/4bab427203254dccaebdefcf80bbe3cb
从代码中可以看出 MapStruct 为我们自动生成了 set/get 代码,并且对枚举类进行了特殊处理。
5. 客户端
@Testpublic void shouldMapCarToDto() {    Car car = new Car( "Morris", 5, CarType.SEDAN );    CarDto carDto = CarMapper.INSTANCE.carToCarDto( car );    System.out.println(carDto);    }实验结果:
https://p26.toutiaoimg.com/large/pgc-image/68d28f1551fa4d06b4f86e2da9a4f034
小结: MapStruct 基于 mapper 接口,在编译期动态生成 set/get 代码的 class 文件 ,在运行时直接调用该 class 文件。
MapStruct 配置

@Mapper

我们翻开上边提到的 Mapper 注释的源码,该注释的表明是:将接口或抽象类标志为映射器,并通过 MapStruct 激活该类型实现的生成。我们找到其中的 componentModel 属性,默认值为 default,它有四种值供我们选择:

[*]default:映射器不使用组件模型,实例通常通过 Mappers.getMapper(java.lang.Class)获取;
[*]cdi:生成的映射器是 application-scoped 的CDI bean,可以通过 @Inject 获取;
[*]spring:生成的映射器是 Spring bean,可以通过 @Autowired 获取;
[*]jsr330:生成的映射器被 @javax.inject.Named 和 @Singleton 注释,可以通过 @inject 获取;
上边我们用的就是默认的方法,当然我们也可以用 @Autowired 来引入接口依赖,此处不再举例,有兴趣的小同伴可以本身试试!
另外我们可以看下 uses 属性:可以通过定义其他类来完成字段转换,接下来我们来个小例子演示一下:
1. 定义一个 CarVo.java 类
@Data@NoArgsConstructor@AllArgsConstructorpublic class CarVo {    private String make;    private int seatCount;    private boolean type;}2. 在 mapper 中定义一个 vo 转为 dto 的方法 CarDto carVoToCarDto(CarVo carVo);
当不加 uses 属性时,查看编译后生成的实现类
public CarDto carVoToCarDto(CarVo carVo) { if (carVo == null) {return null; } else {CarDto carDto = new CarDto();carDto.setMake(carVo.getMake());carDto.setSeatCount(carVo.getSeatCount());carDto.setType(String.valueOf(carVo.isType()));return carDto; }}
[*]在 mapper 上增加 uses 属性,并指定自定义的处理类,代码如下:
@Mapper(uses = {BooleanStrFormat.class})public interface CarMapper {    ......}/*** 自定义的转换类*/@Componentpublic class BooleanStrFormat {    public String toStr(boolean type) {      if(type){            return "Y";      }else{            return "N";      }    }    public boolean toBoolean(String type) {      if (type.equals("Y")) {            return true;      } else {            return false;      }    }}/*** 查看编译后生成的实现类*/public CarDto carVoToCarDto(CarVo carVo) { if (carVo == null) {return null; } else {CarDto carDto = new CarDto();carDto.setMake(carVo.getMake());carDto.setSeatCount(carVo.getSeatCount());      //调用自定义的类中的方法carDto.setType(this.booleanStrFormat.toStr(carVo.isType()));return carDto; }}4.客户端代码
@Testpublic void shouldMapCarVoToDto() { CarVo carVo = new CarVo( "Morris", 5, false ); CarDto carDto = CarMapper.INSTANCE.carVoToCarDto( carVo ); System.out.println(carDto);}实验结果:
https://p3.toutiaoimg.com/large/pgc-image/8d997f6338b24306b70203380bc62557
@Mapping

@Mapping 可以用来配置一个 bean 属性或枚举常量的映射,默认是将具有相同名称的属性进行映射,当然也可以用 source、expression 大概 constant 属性手动指定,接下来我们来分析下常用的属性值。

[*]target:属性的目标名称,同一目标属性不能映射多次。如果用于映射枚举常量,则将给出常量成员的名称,在这种环境下,源枚举中的多个值可以映射到目标枚举的相同值。
[*]source:属性的源名称,


[*]如果带注释的方法有多个源参数,则属性名称必须使用参数名称限定,例如“addressParam.city";
[*]当找不到匹配的属性时,MapStruct 将查找匹配的参数名称;
[*]当用于映射枚举常量时,将给出常量成员的名称;
[*]该属性不能与 constant 或 expression 一起使用;

[*]dateFormat:通过 SimpleDateFormat 实现 String 到 Date 日期之间相互转换。
[*]numberFormat:通过 DecimalFormat 实现 Number 与 String 的数值格式化。
[*]constant:设置指定目标属性的常量字符串,当指定的目标属性的类型为:primitive 或 boxed(例如 Long)时,MapStruct 检查是否可以将该 primitive 作为有效的文天职配给 primitive 或 boxed 类型。如果可能,MapStruct 将分配为文字;如果不可能,MapStruct 将实验应用用户定义的映射方法。 另外,MapStruct 将常量作为字符串处理,将通过应用匹配方法、类型转换方法或内置转换来转换该值。此属性不能与 source、defaultValue、defaultExpression 或 expression 一起使用。
[*]expression:是一个表达式,根据该表达式设置指定的目标属性。他的属性不能与 source、 defaultValue、defaultExpression、constant 一起使用。
[*]ignore: 忽略这个字段。
我们用 expression 这个属性来实现一下上边用 uses 实现的案例:
1. 在 mapper 中定义方法
@Mapping(target = "type", expression = "java(new com.ittest.controller.BooleanStrFormat().toStr(carVo.isType()))")CarDto carVoToDtoWithExpression(CarVo carVo);2. 生成的实现类
@Overridepublic CarDto carVoToDtoWithExpression(CarVo carVo) { if ( carVo == null ) {return null; } CarDto carDto = new CarDto(); carDto.setMake( carVo.getMake() ); carDto.setSeatCount( carVo.getSeatCount() ); carDto.setType( new com.ittest.controller.BooleanStrFormat().toStr(carVo.isType()) ); return carDto;}3. 客户端
@Testpublic void mapCarVoToDtoWithExpression() { CarVo carVo = new CarVo( "Morris", 5, false ); CarDto carDto = CarMapper.INSTANCE.carVoToDtoWithExpression( carVo ); System.out.println(carDto);}运行结果:
https://p3.toutiaoimg.com/large/pgc-image/1eee695c9ac245df9faa4d94f92ea1ce
至于其他的用法各人可以多多探索。
重要提示:枚举映射功能已被弃用,并被 ValueMapping 取代。它将在后续版本中删除。
@Mappings

可以配置多个 @Mapping,例如
@Mappings({    @Mapping(source = "id", target = "carId"),    @Mapping(source = "name", target = "carName"),    @Mapping(source = "color", target = "carColor")})@MappingTarget

用于更新已有对象,还是用例子来说明吧:
1. 创建 BMWCar.java 类
@NoArgsConstructor@AllArgsConstructor@Datapublic class BMWCar {    private String make;    private int numberOfSeats;    private CarType type;    private String color;    private String price;}2. mapper 中创建更新方法,并查看实现类
// 更新方法void updateBwmCar(Car car, @MappingTarget BMWCar bwmCar);// 实现类public void updateBwmCar(Car car, BMWCar bwmCar) { if (car != null) {bwmCar.setMake(car.getMake());bwmCar.setNumberOfSeats(car.getNumberOfSeats());bwmCar.setType(car.getType()); }}3. 客户端代码
@Testpublic void updateBwmCar() { Car car = new Car( "Morris", 5, CarType.SEDAN ); BMWCar bwmCar = new BMWCar("BWM", 5, CarType.SPORTS, "RED", "50w"); System.out.println("更新前 car:"+car.toString()); System.out.println("更新前 BWMCar:"+bwmCar.toString()); CarMapper.INSTANCE.updateBwmCar(car, bwmCar); System.out.println("更新后 car:"+car.toString()); System.out.println("更新后 BWMCar:"+bwmCar.toString());}实验结果:
https://p9.toutiaoimg.com/large/pgc-image/616a3a8f2d644022af16a47ecb906e5c
扩展:多个对象映射一个对象

1. 准备实体类 Benz4SMall.java 和 Mall4S.java
@NoArgsConstructor@AllArgsConstructor@Datapublic class Mall4S {    private String address;    private String mobile;}@Data@NoArgsConstructor@AllArgsConstructorpublic class Benz4SMall {    private String address;    private String mobile;    private String make;    private int numberOfSeats;}2. mapper 创建转换方法并查看生成的实现类
Benz4SMall mallCarToBenzMall(Car car, Mall4S mall4S);/*** 实现类*/public Benz4SMall mallCarToBenzMall(Car car, Mall4S mall4S) { if (car == null && mall4S == null) {return null; } else {Benz4SMall benz4SMall = new Benz4SMall();if (car != null) {   benz4SMall.setMake(car.getMake());   benz4SMall.setNumberOfSeats(car.getNumberOfSeats());}if (mall4S != null) {   benz4SMall.setAddress(mall4S.getAddress());   benz4SMall.setMobile(mall4S.getMobile());}return benz4SMall; }}3. 客户端
@Testpublic void mallCarToBenzMall() { Car car = new Car( "Morris", 5, CarType.SEDAN ); Mall4S mall4S = new Mall4S("北京市", "135XXXX4503"); Benz4SMall benz4SMall = CarMapper.INSTANCE.mallCarToBenzMall(car, mall4S); System.out.println(benz4SMall.toString());}实验结果
https://p5.toutiaoimg.com/large/pgc-image/a26b6183c27a499abc18174a56bf783f
深拷贝与浅拷贝

深拷贝和浅拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是引用。
假设 B 复制了 A ,修改 A 的时候,看 B 是否发生变化:如果 B 跟着也变了,说明是浅拷贝,拿人手短!(修改堆内存中的同一个值);如果 B 没有改变,说明是深拷贝,自食其力!(修改堆内存中的不同的值)
MapStruct 中是创建新的对象,也就是深拷贝。
MapStruct 与其他 Copy 的对比

我们在平常的项目中经常会使用到拷贝的功能,本日我们就将他们做一下对比,直接抛出 ZhaoYingChao88 大佬的实验结果:
https://p3.toutiaoimg.com/large/pgc-image/53e7bd6671094ea6b4c6f863daaf48e7
输出结果:手动Copy >Mapstuct>= cglibCopy > springBeanUtils > apachePropertyUtils > apacheBeanUtils 可以理解为: 手工复制 > cglib > 反射 > Dozer。
https://p9.toutiaoimg.com/large/pgc-image/265f958d10304a12912df1a7eedaf70c
根据测试结果,我们可以得出在速度方面,MapStruct 是最好的,实验速度是 Apache BeanUtils 的10倍、Spring BeanUtils 的 4-5倍、和 BeanCopier 的速度差不多。
总结:在大数据量级的环境下,MapStruct 和 BeanCopier 都有着较高的性能上风,其中 MapStruct 尤为优秀。如果你仅是在日常处理少量的对象时,选取哪个其实变得并不重要,但数据量大时建议还是使用 MapStruct 或 BeanCopier 的方式,进步接口性能。
页: [1]
查看完整版本: 还在用BeanUtils拷贝对象?MapStruct才是王者!「附源码」