为啥不建议用BeanUtils.copyProperties拷贝数据 | 京东云技术团队
在实际的业务开发中,我们经常会碰到VO、BO、PO、DTO等对象属性之间的赋值,当属性较多的时候我们使用get,set的方式进行赋值的工作量相对较大,因此很多人会选择使用spring提供的拷贝工具BeanUtils的copyProperties方法完成对象之间属性的拷贝。通过这种方式可以很大程度上降低我们手动编写对象属性赋值代码的工作量,既然它那么方便为什么还不建议使用呢?下面是我整理的BeanUtils.copyProperties数据拷贝一些常见的坑。
1:属性类型不一致导致拷贝失败
这个坑可以细分为如下两种:
(1)同一属性的类型不同
在实际开发中,很可能会出现同一字段在不同的类中定义的类型不一致,例如ID,可能在A类中定义的类型为Long,在B类中定义的类型为String,此时如果使用BeanUtils.copyProperties进行拷贝,就会出现拷贝失败的现象,导致对应的字段为null,对应案例如下:
public class BeanUtilsTest { public static void main(String[] args) { SourcePoJo sourcePoJo = new SourcePoJo("jingdong", (long) 35711); TargetPoJo targetPoJo = new TargetPoJo(); BeanUtils.copyProperties(sourcePoJo,targetPoJo); System.out.println(targetPoJo); } } @Data @AllArgsConstructor class SourcePoJo{ private String username; private Long id; } @Data class TargetPoJo{ private String username; private String id; }
对应的运行结果如下:
可以看到id字段由于类型不一致,导致拷贝后的值为null。
(2)同一字段分别使用包装类型和基本类型
如果通一个字段分别使用包装类和基本类型,在没有传递实际值的时候,会出现异常,具体案例如下:
public class BeanUtilsTest { public static void main(String[] args) { SourcePoJo sourcePoJo = new SourcePoJo(); sourcePoJo.setUsername("joy"); TargetPoJo targetPoJo = new TargetPoJo(); BeanUtils.copyProperties(sourcePoJo,targetPoJo); System.out.println(targetPoJo); } } @Data class SourcePoJo{ private String username; private Long id; } @Data class TargetPoJo{ private String username; private long id; }
在测试案例中,id字段在拷贝源和拷贝目标中分别使用包装类型和基本类型,可以看到下面在拷贝时出现了异常。
注意:如果一个布尔类型的属性分别使用了基本类型和包装类型,且属性名如果使用is开头,例如isSuccess,也会导致拷贝失败。
2:null值覆盖导致数据异常
在业务开发时,我们可能会有部分字段拷贝的需求,被拷贝的数据里面如果某些字段有null值存在,但是对应的需要被拷贝过去的数据的相同字段的值并不为null,如果直接使用 BeanUtils.copyProperties 进行数据拷贝,就会出现被拷贝数据的null值覆盖拷贝目标数据的字段,导致原有的数据失效。
对应的案例如下:
public class BeanUtilsTest { public static void main(String[] args) { SourcePoJo sourcePoJo = new SourcePoJo(); sourcePoJo.setId("35711"); TargetPoJo targetPoJo = new TargetPoJo(); targetPoJo.setUsername("Joy"); BeanUtils.copyProperties(sourcePoJo,targetPoJo); System.out.println(targetPoJo); } } @Data class SourcePoJo{ private String username; private String id; } @Data class TargetPoJo{ private String username; private String id; }
对应的运行结果如下:
可以看到拷贝目标结果中原本有值的username字段,它的值被覆盖成了null。虽然可以使用 BeanUtils.copyProperties 的重载方法,配合自定义的 ConvertUtilsBean 来实现部分字段的拷贝,但是这么做本身也比较复杂,也就失去了使用BeanUtils.copyProperties 拷贝数据的意义,因此也不推荐这么做。
3:导包错误导致拷贝数据异常
在使用 BeanUtils.copyProperties 拷贝数据时,如果项目中同时引入了Spring的beans包和Apache的beanutils包,在导包的时候,如果导入错误,很可能导致数据拷贝失败,排查起来也不太好发现。我们通常使用的是Sping包中的拷贝方法,两者的区别如下:
//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
4:查找不到字段引用,修改内容难以溯源
在开发或者排查问题过程中,如果我们在链路中查找某个字段值(调用方并未传递)的来源,我们可能会通过全文搜索的方式,去找它对应的赋值方法(例如set方式、build方式等),但是如果在链路中使用BeanUtils.copyProperties拷贝了数据,就很难快速定位到赋值的地方,导致排查效率较低。
5:内部类数据无法成功拷贝
内部类数据无法正常拷贝,及时类型和字段名均相同也无法拷贝成功,如下所示:
public class BeanUtilsTest { public static void main(String[] args) { SourcePoJo sourcePoJo = new SourcePoJo(); sourcePoJo.setUsername("joy"); SourcePoJo.InnerClass innerClass = new SourcePoJo.InnerClass("sourceInner"); sourcePoJo.innerClass=innerClass; System.out.println(sourcePoJo.toString()); TargetPoJo targetPoJo = new TargetPoJo(); BeanUtils.copyProperties(sourcePoJo,targetPoJo); System.out.println(targetPoJo.toString()); } } //下面是类的信息,这里就直接放到一块展示了 @Data @ToString public class SourcePoJo{ private String username; private Long id; public InnerClass innerClass; @Data @ToString @AllArgsConstructor public static class InnerClass{ public String innerName; } } @Data @ToString public class TargetPoJo{ private String username; private Long id; public InnerClass innerClass; @Data @ToString public static class InnerClass{ public String innerName; } }
下面是运行结果:
上面案例中,在拷贝源和拷贝目标中各自存在一个内部类InnerClass,虽然这个内部类属性也相同,类名也相同,但是在不同的类中,因此Spring会认为属性不同,因此不会拷贝数据。
6:BeanUtils.copyProperties是浅拷贝
这里我先给大家复习一下深拷贝和浅拷贝。
浅拷贝是指创建一个新对象,该对象的属性值与原始对象相同,但对于引用类型的属性,仍然共享相同的引用。也就是说在浅拷贝下,当原始内容的引用属性值发生变化时,被拷贝对象的引用属性值也会随之发生变化。
深拷贝是指创建一个新对象,该对象的属性值与原始对象相同,包括引用类型的属性。深拷贝会递归复制引用对象,创建全新的对象,所以深拷贝拷贝后的对象与原始对象完全独立。
下面是对应的代码示例:
public class BeanUtilsTest { public static void main(String[] args) { Person sourcePerson = new Person("sunyangwei",new Card("123456")); Person targetPerson = new Person(); BeanUtils.copyProperties(sourcePerson, targetPerson); sourcePerson.getCard().setNum("35711"); System.out.println(targetPerson); } } @Data @AllArgsConstructor class Card { private String num; } @NoArgsConstructor @AllArgsConstructor @Data class Person { private String name; private Card card; }
下面是运行结果:
总结:通过代码运行结果我们可以发现,一旦你在拷贝后修改了原始对象的引用类型的数据,就会导致拷贝数据的值发生异常,这种问题排查起来也比较困难。
7:底层实现为反射拷贝效率低
BeanUtils.copyProperties底层是通过反射获取到对象的set和get方法,然后通过get、set完成数据的拷贝,整体拷贝效率较低。
下面是使用BeanUtils.copyProperties拷贝数据和直接set的方式赋值效率对比,为了便于直观的看出效果,这里以拷贝1万次为例:
public class BeanUtilsTest { public static void main(String[] args) { long copyStartTime = System.currentTimeMillis(); User sourceUser = new User("sunyangwei"); User targetUser = new User(); for(int i = 0; i < 10000; i++) { BeanUtils.copyProperties(sourceUser, targetUser); } System.out.println("copy方式:"+(System.currentTimeMillis()-copyStartTime)); long setStartTime = System.currentTimeMillis(); for(int i = 0; i < 10000; i++) { targetUser.setUserName(sourceUser.getUserName()); } System.out.println("set方式:"+(System.currentTimeMillis()-setStartTime)); } } @Data @AllArgsConstructor @NoArgsConstructor class User{ private String userName; }
下面是执行的效率结果对比:
可以发现,常规的set和BeanUtils.copyProperties对比,性能差距非常大。因此,慎用BeanUtils.copyProperties。
以上就是在使用BeanUtils.copyProperties拷贝数据时常见的坑,这些坑大多都是比较隐蔽的,出了问题不太好排查,因此不建议在业务中使用BeanUtils.copyProperties拷贝数据。文中不足之处,欢迎补充和指正。
作者:京东科技 孙扬威
来源:京东云开发者社区 转载请注明来源

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
【低代码】低代码平台协同&敏捷场景下的并行开发解决方案探索 | 京东云技术团队
低代码开发平台的出现,大大地提高的产品交付效率,但是在协同开发、敏捷迭代的场景下,也暴露出了一些问题。 例如: 多人同时对项目进行修改,相互影响甚至修改内容被互相覆盖; 同一项目下多个需求同步开发,但需求上线日期不统一,无法拆分上线等等。 本文将根据不同诉求,渐进式的讨论支持并行开发的各种解决方案。 低代码开发平台(Low-Code Development Platform,LCDP),帮助用户使用可视化图形界面(拖拽搭建或配置化方式)编写应用程序,而无需进行传统的编程开发。 低代码开发平台的研发团队往往把更多的经历投入到应用程度搭建过程的完善和丰富上(例如,丰富可通过搭建实现的功能,优化拖拽搭建的交互体验等),而忽略了项目交付过程的能力和体验。 一、项目整体交付 大部分低代码平台,尤其是具备出码能力或托管部署能力的低代码平台,都会采用项目整体交付的形式提供服务。 在用户触发交付流程时,低代码平台会以当前时刻的项目配置、页面配置等,进行代码生产、CI/CD流水线等操作。 二、缩小交付颗粒度 为了提升交付的灵活性,可以通过缩小交付颗粒度的方式,一定程度上的避免多人开发相互影响以及多需求同...
- 下一篇
从DevOps状态报告看技术团队的文化建设
本文源自一次内部分享,借由此机会又把历年的DevOps状态报告翻看了一遍,其实大多数时候我们对于DevOps的理解都在于流程,工具,实践这些看得见摸得着的东西,但就像文末的几点思考所说的那样,我们一直相信技术可以改变世界,但很多时候,你要先改变人才能改变世界,而改变人是最难的。所以从文化的层面反过来看这个似曾相识的DevOps,也有不一样的思考和感悟。 一、先来看一些段子 2017年1月31日,全球最大的代码托管协作平台之一的Gitlab出现了一次长达18小时的停机事故,原因居然是一个IT工程师把生产数据库的数据给清空了。 按道理,这种事情虽然难以接受,但其实并不少见。更加严重的是,当Gitlab尝试恢复数据的时候才发现,他们所谓精心设计的多重备份机制,竟然都无法拯救被删除的数据。最夸张的是,直到这会他们才发现,原来数据库定时备份由于升级后工具版本不匹配而一直处于失败状态,原以为邮件会告警这个问题,但巧合再一次出现,针对自动任务的报警也没有生效。 事已至此,要么是隐藏事实然后给外界一个不疼不痒的解释,要么就是完全公开到每一个细节,你会选择怎么处理呢? Gitlab公司的选择是后者,他们...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Red5直播服务器,属于Java语言的直播服务器
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8编译安装MySQL8.0.19