在日常开发中,我们经常需要给对象进行赋值,通常会调用其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 @Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
} class User implements Cloneable{
private String name;
private String password;
private HomeAddress address;
//省略构造函数和setter/getter @Override
protected 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. Python中的logging模块

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

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

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

  3. python logging method 02

    基本用法 下面的代码展示了logging最基本的用法.     1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 ...

  4. redis的入门篇---五种数据类型及基本操作

    查看所有的key keys * 清空所有的key flushall 检查key是否存在 exists key 设置已存在的key的时长 expire key //设置key为10s 查看key还剩多少 ...

  5. 从技术专家到管理者的思路转变(V1)

    作为技术专家出身的管理者,是一种优势(你所做的很多决策可能比非技术出身的管理者更加具有可行性和性价比).也是一种劣势(你可能会过于自恋自己的技术优势).这取决于你在接下去的职业生涯中,如何取舍你的技术 ...

  6. JSON.toJSONString中序列化空字符串遇到的坑

    前言 最近在做系统Bug修复时遇到了一个问题,调用其他服务时传递的参数和自己预先的不一致,例如Map中有10条记录,然后使用JSON.toJSONString 包装后进行网络传递,但是通过调试发现接收 ...

  7. c json实战引擎四 , 最后❤跳跃

    引言  - 以前那些系列 长活短说, 写的最终 scjson 纯c跨平台引擎, 希望在合适场景中替代老的csjon引擎, 速度更快, 更轻巧. 下面也是算一个系列吧. 从cjson 中得到灵感, 外加 ...

  8. python的logging日志模块(一)

    最近修改了项目里的logging相关功能,用到了Python标准库里的logging模块,在此做一些记录.主要是从官方文档和stackoverflow上查询到的一些内容. 官方文档 技术博客 基本用法 ...

  9. 小程序 大转盘 抽奖 canvas animation

    项目需求运用到大转盘 可设置概率 可直接自定义结果 效果如下

随机推荐

  1. [JLOI2011]飞行路线题解

    题目描述 Alice和Bob现在要乘飞机旅行,他们选择了一家相对便宜的航空公司.该航空公司一共在n个城市设有业务,设这些城市分别标记为0到n-1,一共有m种航线,每种航线连接两个城市,并且航线有一定的 ...

  2. @EnableDiscoveryClient与Nacos自动注册

    前一阵看到有篇博客说cloud从Edgware版本开始,可以不加@EnableDiscoveryClient注解,只要配置好注册中心的相关配置即可自动开启服务注册功能,比较好奇其中的原理,研究了一番特 ...

  3. AcWing 903. 昂贵的聘礼

    年轻的探险家来到了一个印第安部落里. 在那里他和酋长的女儿相爱了,于是便向酋长去求亲. 酋长要他用10000个金币作为聘礼才答应把女儿嫁给他. 探险家拿不出这么多金币,便请求酋长降低要求. 酋长说:& ...

  4. SpringBoot:Service层使用@Autowired 注解 mapper对象爆红问题

    问题点 这个报错可能导致程序运行大面积爆红 这个报错会逼疯强迫症 解决方法 为避免程序运行报错 ,需要在Application.class添加注解@MapperScan(mapper包位置) @Spr ...

  5. Android hacking event 2017

    1.you can't find me, 老规矩先打开jeb,然后看下主活动, 发现又调用了mainthread类的startWrites方法,继续跟进去. 发现是新建了一个随机输入流的文件对象,然后 ...

  6. Java 8 Function 函数接口

    这篇文章属于 Java 8 教程(LTS)系列教程 在 Java 8 中,Function 接口是一个函数接口,它位于包 java.util.function 下. Function 接口中定义了一个 ...

  7. MongoDB 基础学习

    1.MongoDB 概念解析 SQL术语/概念 MongoDB术语/概念 解释/说明 database database 数据库 table collection 数据库表/集合 row docume ...

  8. python -- 程序异常与调试(程序调试)

    一.程序调试 A.使用assert语句检测程序代码中的错误. assert 表达式[, 参数] 如果表达式为True,则继续往下运行:如果为False,则抛出一个AssertionError异常,并且 ...

  9. Springboot中注解@Configuration源码分析

    Springboot中注解@Configuration和@Component的区别 1.先说结论,@Configuration注解上面有@Component注解,所以@Component有的功能@Co ...

  10. VM虚拟机桥接模式无法联网、NAT模式能正常联网

    桥接模式:使虚拟机客户机可以和主机在同一网段,这样,和主机同局域网内的其他主机就也可以ping到虚拟机了: 因此,虚拟机设置为桥接模式,这样以后就可以方便的使用虚拟机了: 有时,虚拟机为桥接模式上不了 ...