分享是最有效的学习方式。

博客:https://blog.ktdaddy.com/

故事

新年新气象,小猫也是踏上了新年新征程,自从小猫按照老猫给的建议【系统梳理大法】完完整整地梳理完毕系统之后,小猫对整个系统的把控可谓又是上到可一个新的高度。开工一周,事情还不是很多,寥寥几个需求,小猫分分钟搞定。

类似于开放平台的老六接到客户的需求,需要在查询订单新增一个下单时间的返回值,然后这就需要提供底层服务的小猫在接口层给出这个字段,然后老六通过包装之后给客户。由于需求比较简单,所以加完字段之后,老六和小猫也就直接上线了。

上线之后事儿来了,对面客户研发一直询问为什么还是没有下单时间,总是空的。老六于是直接找到了小猫,可是小猫经过了一些列的自测发现返回值都是有的,后来排查到在老六封装之后值不见了。经过仔细排查,终于找到了问题,虽然没有造成太大的影响,但是总归给客户研发的心里留下了一个不好的印象。

虽然下单时间老六和小猫定义的都是orderTime这样一个字段,但是字段类型小猫用的是Date类型而老六用的是LocalDate,恰巧老六在进行对象赋值的时候偷了个懒直接用了spring的BeanUtils.copyProperties工具类,于是导致日期类型的值并没有被赋值过去,踩坑了。

老六这才回想起前段时间架构师在群里@ALL的一段话,“大家用BeanUtils拷贝对象的时候注意点,有坑啊,大家尽量用get,set方法啊”。当时的老六不以为意,想着,“切,这得多麻烦,一个个set不花时间啊,有工具类不用”。现在想来看来是真踩到BeanUtils的坑了。

老六一边改着代码一边叨叨:“这也没说坑在哪里啊......”

盘点BeanUtils.copyProperties坑点

相信很多小伙伴在日常开发的过程中都用过BeanUtils.copyProperties。因为我们日常开发中,经常涉及到DO、DTO、VO对象属性拷贝赋值。很多开发为了省去繁琐而又无聊的set方法往往都会用到这样的工具类进行值拷贝,但是看似简单的拷贝程序,其实往往暗藏坑点,这不上面的老六就踩雷了么。

下面咱们一起来盘点一下这个拷贝存在哪些坑点吧。见下图。

目标赋值对象属性非预期

这里主要说的是从老对象进行属性拷贝到新对象之后,新对象的属性值不是所期待的。这里分为两种。

  1. 两对象属性命名一致,但是类型不一致(即老六遇到的坑点)。
  2. 由于开发编写没有核对好,两个对象属性值不一致,却采用了拷贝,导致异常。
  3. loombook+Boolean类型数据+is属性开头的坑。
  4. 不同内部类,相同属性,目标对象赋值有问题。

类型不匹配

我们来重放一下老六和小猫遇到坑。代码如下:

/**
* 公众号:程序员老猫
**/
public class BeanCopyHelper {
public static void main(String[] args) {
Origin a = new Origin();
a.setOrderTime(new Date()); Target b = new Target();
BeanUtils.copyProperties(a,b);
System.out.println(a.getOrderTime());
System.out.println(b.getOrderTime());
}
}
@Data
class Origin {
private Date orderTime;
}
@Data
class Target {
private LocalDate orderTime;
}

输出结果:

Sun Feb 25 21:52:22 CST 2024
null

我看看到两个对象的命名虽然是一致的,但是一个是Date另外一个是LocaDate,这样导致值并没有被赋值过去。

两对象属性命名差异导致赋值不成功

这种拷贝不成功的原因很多时候是由于研发人员粗心,没有校对好导致的。例如下面两个类:

@Data
class Origin {
private Date ordertime;
}
@Data
class Target {
private Date orderTime;
}

这种显而易见是无法赋值成功的,因为仔细看来两个属性名称不一致。当然不会赋值成功了。

loombook+Boolean类型数据+is属性开头的坑

这种情况是比较极端的,在用loombook和不用loombook的情况下是不一样的。我们看一下下面例子。

当我们不用loombook的时候,如下代码:

public class BeanCopyHelper {
public static void main(String[] args) {
Origin origin = new Origin();
origin.setOrderTime(true);
Target target = new Target();
BeanUtils.copyProperties(origin,target);
System.out.println(origin.getOrderTime());
System.out.println(target.isOrderTime());
}
} class Origin {
private Boolean isOrderTime;
public Boolean getOrderTime() {
return isOrderTime;
}
public void setOrderTime(Boolean orderTime) {
isOrderTime = orderTime;
}
}
class Target {
private boolean isOrderTime;
public boolean isOrderTime() {
return isOrderTime;
}
public void setOrderTime(boolean orderTime) {
isOrderTime = orderTime;
}
}

上面的代码中,我们看到基础属性的类型分别是包装类还有一个是非包装类,属性的命名都是一致的。其最终的输出结果,我们看到两者是一致的:

true
true

当如果我们使用loombook的时候,问题就来了,我们看一下loombook改造之后的代码:

public class BeanCopyHelper {
public static void main(String[] args) {
Origin origin = new Origin();
origin.setIsOrderTime(true);
Target target = new Target();
BeanUtils.copyProperties(origin,target);
System.out.println(origin.getIsOrderTime());
System.out.println(target.isOrderTime());
}
} @Data
class Origin {
private Boolean isOrderTime;
}
@Data
class Target {
private boolean isOrderTime;
}

最后的输出结果为:

true
false

那么这是为什么呢?老猫在这里简单分享一下,BeanUtils.copyProperties用户在两个对象之间进行属性的复制,底层基于JavaBean的内省机制,通过内省得到拷贝源对象和目的对象属性的读方法和写方法,然后调用对应的方法进行属性的复制。

所以在进行拷贝时,如果手动生成get和set那么方法分别为:getOrderTime()以及setOrderTime()。我们再来看一下如果采用LoomBook的时候,那么对应的get和set的方法分别为:getIsOrderTime()以及setOrderTime(),抛开set和get本身关键字不看,那么后面的肯定是对应不起来了。

这里我们再发散一下,如果说对应的两个类其属性压根连get和set方法都没有设置,那么两个对象能够被拷贝成功吗?答案是显而易见的,无法被拷贝成功。所以这里也是用这个拷贝方法的时候的一个坑点。

不同内部类,相同属性,目标对象赋值有问题。

看标题还是比较抽象的,我们一起来看一下下面的代码实现:

public class BeanCopyHelper {
public static void main(String[] args) {
Origin test1 = new Origin();
test1.outerName = "程序员老猫";
Origin.InnerClass innerClass = new Origin.InnerClass();
innerClass.InnerName = "程序员老猫 内部类";
test1.innerClass = innerClass;
System.out.println(test1);
Target test2 = new Target();
BeanUtils.copyProperties(test1, test2);
System.out.println(test2);
}
}
@Data
class Origin {
public String outerName;
public Origin.InnerClass innerClass; @Data
public static class InnerClass {
public String InnerName;
}
}
@Data
class Target {
public String outerName;
public Target.InnerClass innerClass; @Data
public static class InnerClass {
public String InnerName;
}
}

输出最终结果如下:

Origin(outerName=程序员老猫, innerClass=Origin.InnerClass(InnerName=程序员老猫 内部类))
Target(outerName=程序员老猫, innerClass=null)

最终我们发现其内部内的属性并没有被赋值过去。

引包冲突导致问题

BeanUtils.copyProperties其实同命名的方法存在于两个不同的包中,一个是spring的另外一个是apache的,如果不注意的话,很容易就会有问题。如下代码:

//org.springframework.beans.BeanUtils(源对象在左边,目标对象在右边)
public static void copyProperties(Object source, Object target) throws BeansException
//org.apache.commons.beanutils.BeanUtils(源对象在右边,目标对象在左边)
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException

位于org.springframework.beans包下。

其copyProperties方法实现原理和Apache BeanUtils.copyProperties原理类似,默认实现浅拷贝

区别在于对PropertyDescriptor(内省机制相关)的处理结果做了缓存来提升性能。这里大家有兴趣可以自行去查阅一下源代码。

查找字段引用困难

当我们在排查问题的时候,或者在熟悉业务的过程中,常常会想要看一个整个属性值的调用链路,从而来跟踪其设值源头。如果我想看当前的这个属性是什么时候被设值值的时候,老猫的做法通常是找到当前的那个属性的set方法,然后使用idea中的“Find Usages”或者快捷键ALT+F7。得到需要属性值被设置的地方。如下图,就能清晰看到在哪里设值了。

但是,如果用了工具类进行拷贝的话,那么在代码复杂的情况下,我们就很难定位其在什么时候被调用的了。

BeanUtils.copyProperties是浅拷贝

在这里,咱们要回忆一下什么时候浅拷贝,什么是深拷贝。

浅拷贝:浅拷贝是指创建一个新对象,然后将原始对象的内容逐个复制到新对象中。在浅拷贝中,只有最外层对象被复制,而内部的嵌套对象只是引用而已,没有被递归复制。这意味着原始对象和浅拷贝对象之间共享内部对象,修改其中一个对象的内部对象会影响到另一个对象。如下示意图:

深拷贝:深拷贝是指在进行复制操作时,创建一个完全独立的新对象,并递归地复制原始对象及其所有子对象。换句话说,深拷贝会复制对象的所有层级,包括对象的属性、嵌套对象、引用等。因此,原始对象和复制对象是完全独立的,修改其中一个对象不会影响另一个对象。

根据上面的描述,我们通过代码来重现一下坑点,具体如下:

public class Address {
private String city;
...
}
public class Person {
private String name;
private Address address;
...
}
public class TestMain {
public static void main(String[] args) {
Person sourcePerson = new Person();
sourcePerson.setName("老六");
Address address = new Address();
address.setCity("上海 徐汇");
sourcePerson.setAddress(address);
Person targetPerson = new Person();
BeanUtils.copyProperties(sourcePerson, targetPerson);
System.out.println(targetPerson.getAddress().getCity());
sourcePerson.getAddress().setCity("上海 黄埔");
System.out.println(targetPerson.getAddress().getCity());
}
}

输出结果为:

上海 徐汇
上海 黄埔

我们很明显地看到操作原始属性的地址,直接影响到了新对象的属性的地址。所以这个坑大家也要当心。当然由于浅拷贝的原因导致拷贝出现问题还涉及集合类进行拷贝。例如我们需要对List或者Map进行拷贝的时候也不能直接去拷贝list以及map。

性能问题

由于BeanUtils.copyProperties其实底层是通过反射实现的,所以其程序执行的效率还是比较低的。我们看一下下面的对比代码:

public class BeanCopyHelper {
public static void main(String[] args) {
Origin test1 = new Origin();
test1.outerName = "公众号:程序员老猫";
Target test2 = new Target();
long beginTime = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) { //循环10万次
test2.setOuterName(test1.getOuterName());
}
System.out.println(test2);
System.out.println("common setter time:" + (System.currentTimeMillis() - beginTime));
long beginTime2 = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) { //循环10万次
BeanUtils.copyProperties(test1, test2);
}
System.out.println(test2);
System.out.println("common setter time:" + (System.currentTimeMillis() - beginTime2));
}
} @Data
class Origin {
public String outerName;
}
@Data
class Target {
public String outerName;
}

输出结果如下:

Target(outerName=公众号:程序员老猫)
common setter time:14
Target(outerName=公众号:程序员老猫)
common setter time:291

上述结果,很好地证明了这个结论。有小伙伴肯定会说,这种场景应该比较少吧,太极端了。那么极端吗?大家回忆一下上面老猫提到了,如果用这个工具复制List或者Map这种集合的时候,其实如果把List和Map当做整个对象来复制往往是失败的。相信如果不是小白的话一般都会知道这个坑点,为了解决这个问题,很多小伙伴可能会选择在List或者Map等集合内部进行循环一一遍历去进行单个对象的拷贝赋值,那么这样的场景下,性能是不是就受到了影响呢?

替换方案

既然说了bean拷贝工具类这么多的坏话,那么我们如何去替换这种写法呢?

第一种:当然是直接采用原始的get以及set方法了。这种方式好像除了代码长了一些之外好像也没有什么缺点了。有小伙伴可能会跳出来说,这不撸起来麻烦么。不着急,idea这款强大的工具不是已经给我们提供插件了么。如下图:

第二种:使用映射工具库,如MapStruct、ModelMapper等,它们可以自动生成属性映射的代码。这些工具库可以减少手动编写setter方法的工作量,并提供更好的性能。

如下使用代码:

    /**
* 公众号:程序员老猫
**/
@Mapper
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper(SourceTargetMapper.class); @Mapping(source = "name", target = "name")
@Mapping(source = "age", target = "age")
Target mapToTarget(Source source);
} //使用
Target target = SourceTargetMapper.INSTANCE.mapToTarget(source);

上述这两种替换方案,说真的作为开发者而言,老猫更喜欢第一种,简单方便,而且不需要依赖第三方maven依赖。第二种个人感觉用起来反而比较繁琐,上述当然纯属个人偏好。

总结

上述小猫和老六的案例中,其实存在的问题需要我们思考的。

即使再小再简单的需求,作为研发开发完毕之后,我们可以直接上线么?其实很多时候事故往往就是由于“不以为意”发生的。事故的发生往往也遵循“墨菲定律”,这就要求我们更要敬畏线上,再小的需求点都需要经过严格的测试验证才能上线。

说了那么多BeanUtils.copyProperties的坏话,那么这种拷贝方式是不是真的就一无是处呢?其实不是的,所谓存在即合理。很多时候使用的时候踩坑说白了我们没有理解好这个拷贝工具的特性。很多时候大家在使用使用一个技术的时候都是囫囵吞枣,为了使用而去使用,压根就没有深入了解这个技术的特性以及使用注意点。所以在我们使用第三方工具的时候,我们需要更好地了解其特性,知其所以然才能更好更正确的使用。小伙伴们你们觉得呢?

如果还有需要补充的点,也欢迎小伙伴们留言。

都说了别用BeanUtils.copyProperties,这不翻车了吧的更多相关文章

  1. BeanUtils.copyProperties() 用法

    BeanUtils提供对Java反射和自省API的包装.其主要目的是利用反射机制对JavaBean的属性进行处理.我们知道,一个JavaBean通常包含了大量的属性,很多情况下,对JavaBean的处 ...

  2. BeanUtils.copyProperties VS PropertyUtils.copyProperties

    作为两个bean属性copy的工具类,他们被广泛使用,同时也很容易误用,给人造成困然:比如:昨天发现同事在使用BeanUtils.copyProperties copy有integer类型属性的bea ...

  3. BeanUtils.copyProperties缓解代码压力,释放双手

    简单描述:之前在写代码的时候,经常把表单提交到后台的对象的参数,通过getter方法取出来,然后,再通过setter方法传递给需要的对象,代码中写了很多get set这种方法,后来听同事说,sprin ...

  4. BeanUtils.copyProperties(A,B)使用注意事项

    ***最近项目中用到BeanUtils.copyProperties(),然后踩了一些坑,也在网上查看了很多同行的测试和总结,现在将自己的测试.整理的注意事项分享如下,希望大家一起学习进步. ***注 ...

  5. BeanUtils.copyProperties方法,当属性Date为null解决

    问题描述:org.apache.commons.beanutils user对象和formBean对象都有属性birthday,而且都是java.sql.Date类型的 当进行BeanUtils.co ...

  6. Spring、Commons的BeanUtils.copyProperties用法

    如果两个对象A.B的大部分属性的名字都一样,此时想将A的属性值复制给B,一个一个属性GET\SET代码量太大,可以通过复制属性的方式减小工作量,同时代码看起来更加简洁明了,复制属性可以用Spring或 ...

  7. 利用BeanUtils.copyProperties 克隆出新对象,避免对象重复问题

    1.经常用jQuery获取标签里面值val(),或者html(),text()等等,有次想把获取标签的全部html元素包括自己也用来操作,查询了半天发现$("#lefttr1"). ...

  8. java Beanutils.copyProperties( )用法

    这是一篇开发自辩甩锅稿~~~~ 昨天测试小姐姐将我的一个bug单重开了,emmmm....内心OS:就调整下对象某个属性类型这么简单的操作,我怎么可能会出错呢,一定不是我的锅!!but再怎么抗拒,bu ...

  9. (转载)BeanUtils.copyProperties() 用法

    BeanUtils.copyProperties() 用法 标签: hibernateuserjdbc数据库strutsjava 2009-10-17 23:04 35498人阅读 评论(6) 收藏  ...

  10. BeanUtils.copyProperties()怎样去掉字段首尾的空格

    背景 下午三时许,笔者正戴着耳机听着歌开心的敲着bug,忽然听到办公室的吵架声,原来是ios开发和产品小姐姐吵起来了,为了一个车牌号的校验问题.起因是ios传的车牌号没有将字符串的首尾空格去掉,后端直 ...

随机推荐

  1. 【转帖】eBay 流量管理之 Kubernetes 网络硬核排查案例

    https://www.infoq.cn/article/L4vyfdyvHYM5EV8d3CdD 一.引子 在 eBay 新一代基于 Kubernetes 的云平台 Tess 环境中,流量管理的实现 ...

  2. [转帖]SpringBoot项目banner.txt生成教程

    文章目录 近期在做毕业设计,后端框架用到了SpringBoot,可以自己个性化设置banner.txt 地址:https://www.bootschool.net/ascii 可以直接下载,然后直接将 ...

  3. ESXi上面虚拟机磁盘损坏修复案例

    事故情况 最近同事反馈, 一个文件更新后出现了文件部分不可读的情况 具体现象为: 前端功能打开白屏 后端文件 前面93行不显示, notepad++打开都是 NULL 黑框. 然后重新覆盖文件, 有概 ...

  4. UOS可能的来源

    1050a 行业版 是基于 阿里的Anolis 1050d 企业版 是基于debian 1050e 欧拉版 是基于华为欧拉 euler

  5. CTT Day3

    T1 忘了叫什么名字 对于一个排列 \(p\),定义它的权值为其有多少个子串是一个值域从 \(1\) 开始的排列.给定排列 \(p\),对于 \(1\le i\le j\le n\),定义 \(f(i ...

  6. css 动画 div顺时针方向移动,

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. js解析上传APK文件的详细信息

    前端解析APK版本信息 需要安装这个包,可以使用cnpm或者npm npm 安装 app-info-parser ( 命令:npm install app-info-parser) APKInfo为i ...

  8. 🛠 开源即时通讯(IM)项目OpenIM源码部署指南

    OpenIM的部署涉及多个组件,并支持多种方式,包括源码.Docker和Kubernetes等.这要求在确保不同部署方式之间的兼容性同时,还需有效管理各版本之间的差异.确实,这些都是复杂的问题,涉及到 ...

  9. svn把文件日期设置为最后提交的时间

    在使用svn进行checkout或update时,我想让文件的日期为提交那时的日期,这要怎样做? 说明:我是在windows下使用TortoiseSVN进行操作的 方法1.修改config [misc ...

  10. 从零开始配置vim(26)——LSP UI 美化

    之前我们通过几个实例演示如何配置其他语言的lsp服务,相信各位小伙伴碰到其他的编程语言也能熟练的配置它对应的lsp服务.本篇讲作为一个补充,我们来优化一下LSP 相关的显示 配置 UI 原始的 lsp ...