这篇文章的内容是我最近刚遇到的一个问题。问题代码是自己写的,是我写单元测试的时候发现的,也是自己修复的。修好后反思:自己实习期间连这样的问题代码都写不出来。但是我为什么要写出来呢?其实是因为有些知识没有那么扎实~很容易被忽略,所以在团队群里强调了这个问题:所以这篇文章主要讲的是BeanUtils工具的属性拷贝和深拷贝,浅拷贝副本等问题。好了,下面开始正文,介绍一下问题代码是什么,为什么会出现问题,是否符合修改?在日常开发中,我们经常需要给对象赋值,通常会调用它们的set/get方法。有时候,如果我们要转换两个对象之间的属性大致相同,就会考虑使用属性复制工具。比如我们在代码中经常会把一个数据结构封装成DO、SDO、DTO、VO等,而这些bean中的属性大部分都是相同的,所以使用属性拷贝工具可以帮助我们节省大量的set和开始运作。市面上类似的工具很多,比较常用的有1.SpringBeanUtils2、CglibBeanCopier3、ApacheBeanUtils4、ApachePropertyUtils5、Dozer6、MapStucts。其中,我推荐大家使用MapStructs。我在《丢弃掉那些BeanUtils工具类吧,MapStruct真香!!!》介绍了原因。这里我就不细说了。最近我们有一个新项目,我们想创建一个新的应用程序,因为我自己分析过这些工具的效率,也看到了它们的实现原理。经过比较,我觉得MapStruct是最适合我们的,所以在代码中引入了这个框架。另外由于Spring的BeanUtils使用起来也比较方便,所以这两个框架主要用在需要beanCopy的代码中。我们通常这样做。如果是DO和DTO/Entity之间的转换,我们统一使用MapStruct,因为他可以单独指定一个Mapper,自定义一些策略。如果是同一个对象之间的拷贝(比如用一个DO创建一个新的DO),或者两个对象之间完全不相关的转换,使用Spring的BeanUtils。一开始没问题,后来写单元测试的时候发现问题了。问题我们先来看看我们在哪里使用了Spring的BeanUtils。在我们的业务逻辑中,我们需要修改订单信息。变更时,我们不仅需要更新订单的上述属性信息,还需要创建一个变更流程。变更流同时记录变更前后的数据,所以有如下代码//从数据库中查询当前订单并锁定OrderDetailorderDetail=orderDetailDao.queryForLock();//复制一个新的订单ModelOrderDetailnewOrderDetail=newOrderDetail();BeanUtils.copyProperties(orderDetail,newOrderDetail);//修改新订单模型逻辑操作newOrderDetail.update();//使用修改前的订单模型和修改后的订单模型组装订单更改流OrderDetailStreamorderDetailStream=newOrderDetailStream();orderDetailStream.create(orderDetail,newOrderDetail);大体逻辑是这样的,因为在创建订单变更流程时,需要变更前的订单和变更后的订单。所以我们想到了创建一个新的订单模型,然后操作新的订单模型,避免影响旧的。然而,真正有问题的是BeanUtils.copyProperties这个过程。因为BeanUtils在复制属性的时候本质上是浅拷贝,不是深拷贝。浅拷贝?深拷贝?什么是浅拷贝和深拷贝?看概念。1、浅拷贝:基本数据类型按值传递,引用数据类型按引用复制。这是浅拷贝。2、深拷贝:传递基本数据类型的值,为引用数据类型新建一个对象,并复制其内容,为深拷贝。下面通过一个实际的例子来看看为什么我说BeanUtils.copyProperties的过程是浅拷贝。先定义两个类:publicclassAddress{privateStringprovince;privateStringcity;privateStringarea;//省略构造函数和setter/getter}classUser{privateStringname;privateStringpassword;privateAddressaddress;/省略构造函数和setter/getter}并编写测试代码:Useruser=newUser("Hollis","hollishuang");user.setAddress(newAddress("浙江","杭州","滨江"));UsernewUser=newUser();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":"hollishuang"}可以发现原始对象也受到了修改的影响。这就是所谓的浅拷贝!如何进行深拷贝找到问题后,我们就得想办法解决,那么深拷贝怎么实现呢?1、实现Cloneable接口,重写clone(),在Object类中定义一个clone方法,其实这个方法其实就是一个浅拷贝,没有重写。如果要实现深拷贝,需要重写clone方法,如果要重写clone方法,必须实现Cloneable,否则会报CloneNotSupportedException。修改以上代码,重写clone方法:publicclassAddressimplementsCloneable{privateStringprovince;privateStringcity;privateStringarea;//省略构造函数和setter/getter@OverridepublicObjectclone()throwsCloneNotSupportedException{returnsuper.clone();}}classUserimplementsCloneable{privateStringname;privateStringaddsword;privateStringAddpassword//省略构造函数和setter/getter@OverrideprotectedObjectclone()throwsCloneNotSupportedException{Useruser=(User)super.clone();user.setAddress((Address)address.clone());returnuser;}}执行完上面的测试代码,可以发现此时newUser中的address对象是一个新的对象。这个方法可以实现深拷贝,但是问题是如果我们User里面的对象很多,那么clone方法会写的很长,而且如果后面有修改,如果User里面增加了一个新的属性,这个地方必须改变。那么,有没有什么办法一劳永逸,不加修改呢?2.序列化实现深拷贝我们可以使用序列化来实现深拷贝。先把对象序列化成流,再从流中反序列化成对象,所以一定是新对象。序列化的方式有很多种。例如,我们可以使用各种JSON工具将对象序列化为JSON字符串,再从字符串反序列化为对象。例如使用fastjson实现:UsernewUser=JSON.parseObject(JSON.toJSONString(user),User.class);也可以实现深拷贝。另外,也可以使用ApacheCommonsLang中提供的SerializationUtils工具来实现。我们需要修改上面的User和Address类,让它们实现Serializable接口,否则无法序列化。classUserimplementsSerializableclassAddressimplementsSerializable然后在需要复制的时候:UsernewUser=(User)SerializationUtils.clone(user);同样也可以实现深拷贝~!总结我们在使用各种BeanUtils的时候,一定要注意是浅拷贝还是深拷贝。浅拷贝的结果是两个对象中引用的对象地址相同,任何变化都会产生影响。深拷贝的实现方式有很多种,其中比较常用的有实现Cloneable接口重写clone方法,以及使用序列化+反序列化创建新对象。好了,今天就到这里。
