你还在用BeanUtils进行对象属性拷贝

38次阅读

共计 8726 个字符,预计需要花费 22 分钟才能阅读完成。

在做业务的时候,为了隔离变化,我们会将 DAO 查询出来的 DO 和对前端提供的 DTO 隔离开来。大概 90% 的时候,它们的结构都是类似的;但是我们很不喜欢写很多冗长的 b.setF1(a.getF1()) 这样的代码,于是我们需要简化对象拷贝方式。

大多时候时候使用的是 ApacheSpring`BeanUtils,今天,我们来看一下一个更高效的属性拷贝方式:BeanCopier`。

一、背景

1.1 对象拷贝概念

Java中,数据类型分为值类型(基本数据类型)和引用类型,值类型包括 intdoublebytebooleanchar 等简单数据类型,引用类型包括类、接口、数组等复杂类型。

对象拷贝分为 浅拷贝 (浅克隆)深拷贝(深克隆)

  • 浅拷贝与深拷贝差异
分类 浅拷贝 深拷贝
区别 创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的话,则复制 引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。 创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都 复制独立的一份。当你修改其中一个对象的任何内容时,都不会影响另一个对象的内容。

参考文章

1.2 示例前准备

  • 源对象属性类UserDO.class(以下示例,源对象都用这个)
@Data
public class UserDO {

    private int id;
    private String userName;
    /**
     * 以下两个字段用户模拟自定义转换
     */
    private LocalDateTime gmtBroth;
    private BigDecimal balance;

    public UserDO(Integer id, String userName, LocalDateTime gmtBroth, BigDecimal balance) {
        this.id = id;
        this.userName = userName;
        this.gmtBroth = gmtBroth;
        this.balance = balance;
    }
}
  • 造数据工具类DataUtil.class
public class DataUtil {

    /**
     * 模拟查询出一条数据
     * @return
     */
    public static UserDO createData() {return new UserDO(1, "Van", LocalDateTime.now(),new BigDecimal(100L));
    }

    /**
     * 模拟查询出多条数据
     * @param num 数量
     * @return
     */
    public static List<UserDO> createDataList(int num) {List<UserDO> userDOS = new ArrayList<>();
        for (int i = 0; i < num; i++) {UserDO userDO = new UserDO(i+1, "Van", LocalDateTime.now(),new BigDecimal(100L));
            userDOS.add(userDO);
        }
        return userDOS;
    }
}

二、对象拷贝之 BeanUtils

ApacheSpring 均有 BeanUtils 工具类,Apache BeanUtils 稳定性与效率都不行;SpringBeanUtils 比较稳定,不会因为量大了,耗时明显增加,故一般都使用 SpringBeanUtils

2.1 源码解读

Spring中的BeanUtils,其中实现的方式很简单,就是对两个对象中相同名字的属性进行简单get/set,仅检查属性的可访问性。

BeanUtils 源码

可以看到, 成员变量赋值是基于目标对象的成员列表, 并且会跳过 ignore 的以及在源对象中不存在的, 所以这个方法是安全的, 不会因为两个对象之间的结构差异导致错误, 但是必须保证同名的两个成员变量类型相同。

2.2 示例

@Slf4j
public class BeanUtilsDemo {public static void main(String[] args) {long start = System.currentTimeMillis();
        UserDO userDO = DataUtil.createData();
        log.info("拷贝前,userDO:{}", userDO);
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(userDO,userDTO);
        log.info("拷贝后,userDO:{}", userDO);
    }
}
  • 结果
18:12:11.734 [main] INFO cn.van.parameter.bean.copy.demo.BeanUtilsDemo - 拷贝前,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:12:11.730, balance=100)
18:12:11.917 [main] INFO cn.van.parameter.bean.copy.demo.BeanUtilsDemo - 拷贝后,userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:12:11.730, balance=100)

三、对象拷贝之 BeanCopier

BeanCopier是用于在两个 bean 之间进行属性拷贝的。BeanCopier支持两种方式:

  1. 一种是不使用 Converter 的方式,仅对两个 bean 间属性名和类型完全相同的变量进行拷贝;
  2. 另一种则引入Converter,可以对某些特定属性值进行特殊操作。

3.1 基本使用

  • 依赖
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib-nodep</artifactId>
    <version>3.3.0</version>
</dependency>

注意:该依赖非必须,因为 Spring 中已经集成了cglib, 博主使用的就是org.springframework.cglib.beans.BeanCopier

3.1.1 属性名称、类型都相同

  • 目标对象属性类
@Data
public class UserDTO {
    private int id;
    private String userName;
}
  • 测试方法
/**
 * 属性名称、类型都相同(部分属性不拷贝)
 */
private static void normalCopy() {
    // 模拟查询出数据
    UserDO userDO = DataUtil.createData();
    log.info("拷贝前:userDO:{}", userDO);
    // 第一个参数:源对象,第二个参数:目标对象,第三个参数:是否使用自定义转换器(下面会介绍),下同
    BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false);
    UserDTO userDTO = new UserDTO();
    b.copy(userDO, userDTO, null);
    log.info("拷贝后:userDTO:{}", userDTO);
}
  • 结果:拷贝成功
18:24:24.080 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T18:24:24.077, balance=100)
18:24:24.200 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝后:userDTO:UserDTO(id=1, userName=Van)

3.1.2 属性名称相同、类型不同

  • 目标对象属性类
@Data
public class UserEntity {
    private Integer id;
    private String userName;
}
  • 测试方法
/**
 * 属性名称相同、类型不同
 */
private static void sameNameDifferentType() {
    // 模拟查询出数据
    UserDO userDO = DataUtil.createData();
    log.info("拷贝前:userDO:{}", userDO);

    BeanCopier b = BeanCopier.create(UserDO.class, UserEntity.class, false);
    UserEntity userEntity = new UserEntity();
    b.copy(userDO, userEntity, null);
    log.info("拷贝后:userEntity:{}", userEntity);
}
  • 结果
19:43:31.645 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:43:31.642, balance=100)
19:43:31.748 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝后:userEntity:UserEntity(id=null, userName=Van)
  • 分析

通过日志可以发现:UserDOint 类型的 id 无法拷贝到 UserEntityIntegerid

3.1.3 小节

BeanCopier只拷贝名称和类型都相同的属性。

即使源类型是原始类型 (int, shortchar等),目标类型是其包装类型 (Integer, ShortCharacter等),或反之:都不会被拷贝。

3.2 自定义转换器

通过 3.1.2 可知,当源和目标类的属性类型不同时,不能拷贝该属性,此时我们可以通过实现 Converter 接口来自定义转换器

3.2.1 准备

  • 目标对象属性类
@Data
public class UserDomain {
    private Integer id;
    private String userName;
    
    /**
     * 以下两个字段用户模拟自定义转换
     */
    private String gmtBroth;
    private String balance;
}

3.2.2 不使用Converter

  • 测试方法
/**
 * 类型不同, 不使用 Converter
 */
public static void noConverterTest() {
    // 模拟查询出数据
    UserDO userDO = DataUtil.createData();
    log.info("拷贝前:userDO:{}", userDO);
    BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, false);
    UserDomain userDomain = new UserDomain();
    copier.copy(userDO, userDomain, null);
    log.info("拷贝后:userDomain:{}", userDomain);
}
  • 结果
19:49:19.294 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:49:19.290, balance=100)
19:49:19.394 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝后:userDomain:UserDomain(id=null, userName=Van, gmtBroth=null, balance=null)
  • 分析

通过打印日志的前后对比,属性类型不同的字段 id,gmtBroth,balance 未拷贝。

3.2.3 使用Converter

  • 实现 Converter 接口来自定义属性转换
public  class UserConverter implements Converter {

    /**
     * 时间转换的格式
     */
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    /**
     * 自定义属性转换
     * @param value 源对象属性类
     * @param target 目标对象里属性对应 set 方法名,eg.setId
     * @param context 目标对象属性类
     * @return
     */
    @Override
    public Object convert(Object value, Class target, Object context) {if (value instanceof Integer) {return value;} else if (value instanceof LocalDateTime) {LocalDateTime date = (LocalDateTime) value;
            return dtf.format(date);
        } else if (value instanceof BigDecimal) {BigDecimal bd = (BigDecimal) value;
            return bd.toPlainString();}
        // 更多类型转换请自定义
        return value;
    }
}
  • 测试方法
/**
 * 类型不同, 使用 Converter
 */
public static void converterTest() {
    // 模拟查询出数据
    UserDO userDO = DataUtil.createData();
    log.info("拷贝前:userDO:{}", userDO);
    BeanCopier copier = BeanCopier.create(UserDO.class, UserDomain.class, true);
    UserConverter converter = new UserConverter();
    UserDomain userDomain = new UserDomain();
    copier.copy(userDO, userDomain, converter);
    log.info("拷贝后:userDomain:{}", userDomain);
}
  • 结果:全部拷贝
19:51:11.989 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝前:userDO:UserDO(id=1, userName=Van, gmtBroth=2019-11-02T19:51:11.985, balance=100)
19:51:12.096 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopierDemo - 拷贝后:userDomain:UserDomain(id=1, userName=Van, gmtBroth=2019-11-02 19:51:11, balance=100)

3.2.4 小节

  1. 一旦使用 ConverterBeanCopier 只使用 Converter 定义的规则去拷贝属性,所以在 convert() 方法中要考虑所有的属性。
  2. 但,使用 Converter 会使对象拷贝速度变慢。

3.3 BeanCopier总结

  1. 当源类和目标类的属性名称、类型都相同,拷贝没问题。
  2. 当源对象和目标对象的属性名称相同、类型不同, 那么名称相同而类型不同的属性不会被拷贝。注意,原始类型(intshortchar)和 他们的包装类型,在这里都被当成了不同类型,因此不会被拷贝。
  3. 源类或目标类的 settergetter少,拷贝没问题,此时 setter 多余,但是不会报错。
  4. 源类和目标类有相同的属性(两者的 getter 都存在),但是目标类的 setter 不存在,此时会抛出NullPointerException

四、BeanUtils 与 BeanCopier 速度对比

废话不多说,我这里直接演示两种工具 10000 条数据拷贝的耗时对比

4.1 BeanUtils

  • 测试代码
private static void beanUtil() {List<UserDO> list = DataUtil.createDataList(10000);
    long start = System.currentTimeMillis();
    List<UserDTO> dtoList = new ArrayList<>();
    list.forEach(userDO -> {UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(userDO,userDTO);
        dtoList.add(userDTO);
    });
    log.info("BeanUtils cotTime: {}ms", System.currentTimeMillis() - start);
}
  • 结果(耗时:232ms)
20:14:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanUtils cotTime: 232ms

4.2 BeanCopier

  • 测试代码
private static void beanCopier() {
    // 工具类生成 10w 条数据
    List<UserDO> doList = DataUtil.createDataList(10000);
    long start = System.currentTimeMillis();
    List<UserDTO> dtoList = new ArrayList<>();
    doList.forEach(userDO -> {BeanCopier b = BeanCopier.create(UserDO.class, UserDTO.class, false);
        UserDTO userDTO = new UserDTO();
        b.copy(userDO, userDTO, null);
        dtoList.add(userDTO);
    });
    log.info("BeanCopier costTime: {}ms", System.currentTimeMillis() - start);
}
  • 结果(耗时:116ms)
20:15:24.380 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanCopier costTime: 116ms

4.3 缓存 BeanCopier 实例提升性能

BeanCopier拷贝速度快,性能瓶颈出现在创建 BeanCopier 实例的过程中。所以,把创建过的 BeanCopier 实例放到缓存中,下次可以直接获取,提升性能。

  • 测试代码
private static void beanCopierWithCache() {List<UserDO> userDOList = DataUtil.createDataList(10000);
    long start = System.currentTimeMillis();
    List<UserDTO> userDTOS = new ArrayList<>();
    userDOList.forEach(userDO -> {UserDTO userDTO = new UserDTO();
        copy(userDO, userDTO);
        userDTOS.add(userDTO);
    });
    log.info("BeanCopier 加缓存后 costTime: {}ms", System.currentTimeMillis() - start);

}

public static void copy(Object srcObj, Object destObj) {String key = genKey(srcObj.getClass(), destObj.getClass());
    BeanCopier copier = null;
    if (!BEAN_COPIERS.containsKey(key)) {copier = BeanCopier.create(srcObj.getClass(), destObj.getClass(), false);
        BEAN_COPIERS.put(key, copier);
    } else {copier = BEAN_COPIERS.get(key);
    }
    copier.copy(srcObj, destObj, null);

}
private static String genKey(Class<?> srcClazz, Class<?> destClazz) {return srcClazz.getName() + destClazz.getName();}
  • 结果(耗时:6ms)
20:32:31.405 [main] INFO cn.van.parameter.bean.copy.demo.BeanCopyComparedDemo - BeanCopier 加缓存后 costTime: 6ms

五、总结及源码

场景 耗时(10000 次调用) 原理
BeanUtils 232ms 反射
BeanCopier 116ms 修改字节码
BeanCopier(加缓存) 6ms 修改字节码

Github 示例代码

推荐:BeanCopier 源码分析

技术交流

  1. 风尘博客
  2. 风尘博客 - 博客园
  3. 风尘博客 - 掘金

关注公众号,了解更多:

正文完
 0