在日常开发中,我们经常需要给对象进行赋值,通常会调用其set/get方法,有些时候,如果我们要转换的两个对象之间属性大致相同,会考虑使用属性拷贝工具进行。

如我们经常在代码中会对一个数据结构封装成DO、SDO、DTO、VO等,而这些Bean中的大部分属性都是一样的,所以使用属性拷贝类工具可以帮助我们节省大量的set和get操作。

市面上有很多类似的工具类,比较常用的有

1、Spring BeanUtils 2、Cglib BeanCopier 3、Apache BeanUtils 4、Apache PropertyUtils 5、Dozer 6、MapStucts

这里面我比较建议大家使用的是MapStructs,我在《丢弃掉那些BeanUtils工具类吧,MapStruct真香!!!》中介绍过原因。这里就不再赘述了。

最近我们有个新项目,要创建一个新的应用,因为我自己分析过这些工具的效率,也去看过他们的实现原理,比较下来之后,我觉得MapStruct是最适合我们的,于是就在代码中引入了这个框架。

另外,因为Spring的BeanUtils用起来也比较方便,所以,代码中对于需要beanCopy的地方主要在使用这两个框架。

我们一般是这样的,如果是DO和DTO/Entity之间的转换,我们统一使用MapStruct,因为他可以指定单独的Mapper,可以自定义一些策略。

如果是同对象之间的拷贝(如用一个DO创建一个新的DO),或者完全不相关的两个对象转换,则使用Spring的BeanUtils。

刚开始都没什么问题,但是后面我在写单测的时候,发现了一个问题。

问题

先来看看我们是在什么地方用的Spring的BeanUtils

我们的业务逻辑中,需要对订单信息进行修改,在更改时,不仅要更新订单的上面的属性信息,还需要创建一条变更流水。

而变更流水中同时记录了变更前和变更后的数据,所以就有了以下代码:

//从数据库中查询出当前订单,并加锁
OrderDetail orderDetail = orderDetailDao.queryForLock();//copy一个新的订单模型
OrderDetail newOrderDetail = new OrderDetail();
BeanUtils.copyProperties(orderDetail, newOrderDetail);//对新的订单模型进行修改逻辑操作
newOrderDetail.update();//使用修改前的订单模型和修改后的订单模型组装出订单变更流水
OrderDetailStream orderDetailStream = new OrderDetailStream();
orderDetailStream.create(orderDetail, newOrderDetail);

大致逻辑是这样的,因为创建订单变更流水的时候,需要一个改变前的订单和改变后的订单。所以我们想到了要new一个新的订单模型,然后操作新的订单模型,避免对旧的有影响。

但是,就是这个BeanUtils.copyProperties的过程其实是有问题的。

因为BeanUtils在进行属性copy的时候,本质上是浅拷贝,而不是深拷贝。

浅拷贝?深拷贝?

什么是浅拷贝和深拷贝?来看下概念。

1、浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。

2、深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

我们举个实际例子,来看下为啥我说BeanUtils.copyProperties的过程是浅拷贝。

先来定义两个类:

public class Address {private String province;private String city;private String area;//省略构造函数和setter/getter
}class User {private String name;private String password;private HomeAddress address;//省略构造函数和setter/getter
}

然后写一段测试代码:

User user = new User("Hollis", "hollischuang");
user.setAddress(new HomeAddress("zhejiang", "hangzhou", "binjiang"));User newUser = new User();
BeanUtils.copyProperties(user, newUser);
System.out.println(user.getAddress() == newUser.getAddress());

以上代码输出结果为:true

即,我们BeanUtils.copyProperties拷贝出来的newUser中的address对象和原来的user中的address对象是同一个对象。

可以尝试着修改下newUser中的address对象:

    newUser.getAddress().setCity("shanghai");System.out.println(JSON.toJSONString(user));System.out.println(JSON.toJSONString(newUser));

输出结果:

{"address":{"area":"binjiang","city":"shanghai","province":"zhejiang"},"name":"Hollis","password":"hollischuang"}
{"address":{"area":"binjiang","city":"shanghai","province":"zhejiang"},"name":"Hollis","password":"hollischuang"}

可以发现,原来的对象也受到了修改的影响。

这就是所谓的浅拷贝!

如何进行深拷贝

发现问题之后,我们就要想办法解决,那么如何实现深拷贝呢?

1、实现Cloneable接口,重写clone()

在Object类中定义了一个clone方法,这个方法其实在不重写的情况下,其实也是浅拷贝的。

如果想要实现深拷贝,就需要重写clone方法,而想要重写clone方法,就必须实现Cloneable,否则会报CloneNotSupportedException异常。

将上述代码修改下,重写clone方法:

public class Address implements Cloneable{private String province;private String city;private String area;//省略构造函数和setter/getter@Overridepublic Object clone() throws CloneNotSupportedException {return super.clone();}
}class User implements Cloneable{private String name;private String password;private HomeAddress address;//省略构造函数和setter/getter@Overrideprotected Object clone() throws CloneNotSupportedException {User user = (User)super.clone();user.setAddress((HomeAddress)address.clone());return user;}
}

之后,在执行一下上面的测试代码,就可以发现,这时候newUser中的address对象就是一个新的对象了。

这种方式就能实现深拷贝,但是问题是如果我们在User中有很多个对象,那么clone方法就写的很长,而且如果后面有修改,在User中新增属性,这个地方也要改。

那么,有没有什么办法可以不需要修改,一劳永逸呢?

2、序列化实现深拷贝

我们可以借助序列化来实现深拷贝。先把对象序列化成流,再从流中反序列化成对象,这样就一定是新的对象了。

序列化的方式有很多,比如我们可以使用各种JSON工具,把对象序列化成JSON字符串,然后再从字符串中反序列化成对象。

如使用fastjson实现:

User newUser = JSON.parseObject(JSON.toJSONString(user), User.class);

也可实现深拷贝。

除此之外,还可以使用Apache Commons Lang中提供的SerializationUtils工具实现。

我们需要修改下上面的User和Address类,使他们实现Serializable接口,否则是无法进行序列化的。

class User implements Serializable
class Address implements Serializable

然后在需要拷贝的时候:

User newUser = (User) SerializationUtils.clone(user);

同样,也可以实现深拷贝啦~!

使用各类BeanUtils的时候,切记注意这个坑!相关推荐

  1. 这样的烂代码,我实习的时候都写不出来!

    ‍‍ 来源 | Hollis(ID:hollischuang) 本文的内容是最近我刚刚遇到的一个问题,问题代码是我自己写的,也是我自己写单元测试的时候发现的,也是我自己修复的,修复完之后,我反思了一下 ...

  2. 这样的问题代码,我实习的时候都写不出来!

    △Hollis, 一个对Coding有着独特追求的人△ 这是Hollis的第 365 篇原创分享 作者 l Hollis 来源 l Hollis(ID:hollischuang) 本文的内容是最近我刚 ...

  3. 干货 | 大数据人工智能领域从菜鸟到高手晋级指南

    作者 | 王明哲.王存光 校对 | 丁楠雅 本文长度为5600字,建议阅读10分钟 本文为你剖析当下的时代背景,为在大数据江湖中修炼的行者提供升级建议. 我们身处一个"技术爆炸"和 ...

  4. 干货丨人工智能、大数据领域从菜鸟到高手的晋级指南(经典长文,值得收藏)

    本文试图帮助各位读者用好各类"共享.开源"的学习工具以及学习渠道,躲过各类新手容易误入的"深坑",以最小时间成本和经济成本,优质地完成目标技术的学习和掌握. 一 ...

  5. linux mysql卸载_Linux环境下安装Mysql8.0数据库

    今年的双11既没有光棍节的气氛也没有购物的欲望,不过还是大出血了,买了一台2核4G的云服务器3年699元.就算用来督促自己学习吧!话不多说,今天先来体验Linux安装Mysql数据库? 步骤一:Mys ...

  6. Python中的logging模块

    http://python.jobbole.com/86887/ 最近修改了项目里的logging相关功能,用到了python标准库里的logging模块,在此做一些记录.主要是从官方文档和stack ...

  7. Gink掉过的坑(一):将CCTableView导入到lua中

    环境: 系统:win7 64位 cocos2dx:cocos2d-2.1rc0-x-2.1.3 Visual Studio: 2012 由于项目是用lua写的,需要将cocos2dx中的方法导入到lu ...

  8. 独家 | 大数据人工智能领域从菜鸟到高手晋级指南

    作者:王明哲.王存光 校对:丁楠雅 本文长度为5600字,建议阅读10分钟 本文为你剖析当下的时代背景,为在大数据江湖中修炼的行者提供升级建议. 我们身处一个"技术爆炸"和&quo ...

  9. 大数据人工智能领域从菜鸟到高手晋级指南

    我们身处一个"技术爆炸"和"共享.开源"的时代,先进技术的更新迭代速率超过了历史上任何一个时期,而且这些技术也不再闭塞,人人都可以接触并学习.终身学习已经是我们 ...

最新文章

  1. 2022-2028全球与中国跨临界二氧化碳系统市场现状及未来发展趋势报告
  2. Python生物信息学⑤DNA转录RNA
  3. 以网络安全为例的大数据可视化设计
  4. VC++之自定义消息
  5. Pycharm社区版配置Django
  6. springMVCs下载
  7. 1006 换个格式输出整数(C语言)
  8. 智能图形开发板SmartGLCD
  9. 2019年初,我收到人生中的第一笔稿费
  10. react怎么引入jquery_在react里面使用jquery插件
  11. ORA-12514: TNS:listener does not currently know of service requested in connect descriptor
  12. 尚硅谷大数据技术之Hadoop(入门)
  13. 网络工程师(软考中级-华为认证)
  14. 微信开发者工具 文件删除操作 二次确认框 设置
  15. niosii 把程序固化到epcs中的步骤
  16. 斑马网络招聘汽车安全软件工程师
  17. 超级简单的HTML圆形头像css
  18. 教你文本聚类(参考http://www.kuqin.com/searchengine/20080511/8323.html)
  19. android sqlite 分词,sqlite3自定义分词器
  20. 【专题5: 硬件设计】 之 【9.案例一:门控开关,制作BOM表和成本核算】

热门文章

  1. php post aspx,PHP模拟POST aspx页面不成功
  2. lisp提取长方形坐标_语义SLAM | 深度学习用于特征提取 : SuperPoint(一)
  3. 微信能远程控制电脑吗_神器分享:用微信就能远程控制电脑,这款神器有些厉害...
  4. php设计的意义,PHP设计模式
  5. java static method_java 中static的几种用法
  6. raid ahci模式哪个好_比群晖好用?威联通TR-004磁盘阵列外接盒使用详解
  7. java aes pbe_JAVA对称加密算法PBE定义与用法实例分析
  8. mysql服务等待应答超时_从mysql备份报错来看net_read_timeout 和net_write_timeout参数
  9. qq空间登陆 cookie_把这篇 Session、Cookie、Token看完,和面试官随便谈人生
  10. (王道408考研操作系统)第四章文件管理-第一节5:文件存储空间管理