从零开始SpringCloud Alibaba实战(53)——使用各类BeanUtils的时候,切记注意这个坑!

前言

在日常开发过程中总会遇到VO DTO POJO等对象互相转换,转换的两个对象之间属性大致相同,会考虑使用属性拷贝工具进行,使用属性拷贝类工具可以帮助我们节省大量的set和get操作。

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

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

这里面我比较建议大家使用的是MapStructs
MapStruct(https://mapstruct.org/ )是一种代码生成器,它极大地简化了基于”约定优于配置”方法的Java bean类型之间映射的实现。生成的映射代码使用纯方法调用,因此快速、类型安全且易于理解。
Spring的BeanUtils用起来也比较方便,所以,代码中对于需要beanCopy的地方主要在使用这两个框架。

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

如果是同对象之间的拷贝(如用一个DO创建一个新的DO),或者完全不相关的两个对象转换,则使用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
可以发现,原来的对象也受到了修改的影响。

这就是所谓的浅拷贝!

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

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中新增属性,这个地方也要改。

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

序列化实现深拷贝

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

序列化的方式有很多,比如我们可以使用各种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);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值