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

一、背景

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 源码分析

你还在用BeanUtils进行对象属性拷贝?的更多相关文章

  1. 对象属性拷贝工具类大全==>Bean的属性拷贝从此不用愁

    大家在做java开发时,肯定会遇到api层参数对象传递给服务层,或者把service层的对象传递给dao层,他们之间又不是同一个类型对象,但字段又是一样,如果还是用普通的get.set方式来处理话,比 ...

  2. BeanUtils低依赖属性拷贝测试(一)

    javabean package entity; import java.util.Date; /** * 一个测试用: * student,javaBean * @author mzy * 一个标准 ...

  3. BeanUtils对象属性copy的性能对比以及源码分析

    1. 对象属性拷贝的常见方式及其性能 在日常编码中,经常会遇到DO.DTO对象之间的转换,如果对象本身的属性比较少的时候,那么我们采用硬编码手工setter也还ok,但如果对象的属性比较多的情况下,手 ...

  4. java提高篇(六)-----使用序列化实现对象的拷贝

    我们知道在Java中存在这个接口Cloneable,实现该接口的类都会具备被拷贝的能力,同时拷贝是在内存中进行,在性能方面比我们直接通过new生成对象来的快,特别是在大对象的生成上,使得性能的提升非常 ...

  5. java提高篇(五)-----使用序列化实现对象的拷贝

          我们知道在Java中存在这个接口Cloneable,实现该接口的类都会具备被拷贝的能力,同时拷贝是在内存中进行,在性能方面比我们直接通过new生成对象来的快,特别是在大对象的生成上,使得性 ...

  6. 利用BeanUtils在对象间复制属性

    commons-beanutils是jakarta commons子项目中的一个软件包,其主要目的是利用反射机制对JavaBean的属性进行处理.我们知道,一个JavaBean通常包含了大量的属性,很 ...

  7. getSelection、range 对象属性,方法理解,解释

    网上转了一圈发现没有selection方面的解释,自己捣鼓下 以这段文字为例子.. <p><b>法国国营铁路公司(SNCF)20日承认,</b>新订购的2000列火 ...

  8. js之oop <二> 对象属性

    js中对象属性可以动态添加和删除.删除对象属性用delete关键字. function obj(){ } var oo = new obj(); oo.a = "a"; oo.b ...

  9. JavaScript 中的window.event代表的是事件的状态,jquery事件对象属性,jquery中如何使用event.target

    http://wenda.haosou.com/q/1373868839069215 http://kylines.iteye.com/blog/1660236 http://www.cnblogs. ...

随机推荐

  1. Spring5源码解析-前奏:本地构建Spring5源码

    构建环境 macOS 10.13.6 JDK1.8 IntelliJ IDEA 2018.3.6 (Ultimate Edition) Spring v5.1.9.RELEASE Gradle 5.5 ...

  2. WebGL简易教程(七):绘制一个矩形体

    目录 1. 概述 2. 示例 2.1. 顶点索引绘制 2.2. MVP矩阵设置 2.2.1. 模型矩阵 2.2.2. 投影矩阵 2.2.3. 视图矩阵 2.2.4. MVP矩阵 3. 结果 4. 参考 ...

  3. Linux Shell 基础知识(二)

    1.本文知识结构 2.文件的查询与检索 2.1. cd 目录切换 找到文件/目录位置:cd 切换到上一个工作目录: cd - 切换到home目录: cd or cd ~ 显示当前路径: pwd 更改当 ...

  4. ShaderHelper2 组件升级,支持自动枚举参数!

    ShaderHelper2 组件新体验视频演示: https://www.bilibili.com/video/av69314195/ ShaderHelper2 组件我们已经介绍过两次了,不了解的伙 ...

  5. Vue学习系列(一)——初识Vue.js核心

    前言 vue.js是一套构建用户界面的渐进式框架,vue.js的目标是通过尽可能简单的API实现响应的数据绑定和组合的视图组件. vue通过DOM事件操作和指令来进行视图层和模型层的相互通讯,会为每一 ...

  6. vue3.0的安装使用

    关于旧版本 Vue CLI 的包名称由 vue-cli 改成了 @vue/cli. 如果你已经全局安装了旧版本的 vue-cli (1.x 或 2.x),你需要先通过 npm uninstall vu ...

  7. javascript进阶-《原型对象和原型链》

    原创发布 by @一像素 2015.12 在Javascript中,万物皆对象,但对象也有区别,大致可以分为两类,即:普通对象Object 和 函数对象Function. 一般而言,通过new Fun ...

  8. C#通过对象属性名修改值

    摘自:csdn 给一个对象属性赋值可以通过PropertyInfo.SetValue()方式进行赋值,但要注意值的类型要与属性保持一致.    创建对象实例的两种方法: 1. var obj = As ...

  9. 记录一次Metaspace扩容引发FGC的调优总结

    开始之前 在开始之前先记录一个我碰到的jvm调优的坑.那就是… 为啥我配置到idea64exe.vmoptions中的参数没有生效??? 由于之前一直是在mac上开发,本地开发时当需要优化jvm参数的 ...

  10. python pip源安装模块的一些常见问题

    刷bugku的web时需要用python写脚本,web方面需要安装对应的模块,下面就分享一下我在安装模块时碰到的一些问题以及解决方法 首先找到pip文件所在的位置 打开cmd,cd文件位置,打开文件夹 ...