最近安琪拉在疯狂卷项目,看了看最近的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)实现属性赋值。

发现一个好东西-对象复制库_apache


下面顺便给大家撸了一下getter 方法的源码,其实不神秘,就是 get + 属性名(首字母大写)。setter方法类似。如下图:

大家注意到没有,如果是boolean 类型的,处理逻辑不太一样,get方法是加 “is”, 这就是为什么项目开发都会禁止boolean类型变量是“is” 打头,因为框架有规约,boolean 类型的get方法不是getBoolean(), 而是 isXXX(),而且一般boolean 类型属性的获取方法大家都约定 isXXX(),大家可以检查一下自己项目里面boolean类型的变量是不是遵守规范了。

发现一个好东西-对象复制库_spring_02

发现一个好东西-对象复制库_apache_03

发现一个好东西-对象复制库_json_04


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