都说了别用BeanUtils.copyProperties,这不翻车了吧

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/411362.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Arduino中安装ESP32网络抽风无法下载 暴力解决办法 python

不知道什么仙人设计的arduino连接网络部分&#xff0c;死活下不下来。&#xff08;真的沙口&#xff0c;第一次看到这么抽风的下载口&#xff09; 操作 给爷惹火了我踏马解析json选zip直接全部下下来 把这个大家的开发板管理地址下下来跟后面python放在同一目录下&#xff0c…

【Java程序设计】【C00317】基于Springboot的智慧社区居家养老健康管理系统(有论文)

基于Springboot的智慧社区居家养老健康管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的智慧社区居家养老健康管理系统设计与实现&#xff0c;本系统有管理员、社区工作人员、医生以及家属四种角色权限 管…

理解这几个安全漏洞,你也能做安全测试!

如今安全问题显得越来越重要&#xff0c;一个大型的互联网站点&#xff0c;你如果每天查看日志&#xff0c;会发现有很多尝试攻击性的脚本。 如果没有&#xff0c;证明网站影响力还不够大。信息一体化的背后深藏着各类安全隐患&#xff0c;例如由于开发人员的不严谨导致为Web应…

基于24扇区细分的三电平逆变器异步电机直接转矩控制系统学习

导读&#xff1a;本期文章介绍异步电机三电平24扇区的直接转矩控制。三电平逆变器直接转矩控制中&#xff0c;传统的PWM控制方法存在错判区间等问题。本文在借鉴三电平逆变器单一矢量及合成矢量的直接转矩控制研究和两电平12扇区直接转矩控制的基础上&#xff0c;将两电平12扇区…

堆/堆排序(C/C++)

本篇文章将会较为全面的介绍堆的概念以及实现堆两个重要算法&#xff1a;向上调整算法和向下调整算法。接着实现了堆排序。 若想查看对应位置&#xff0c;可直接按照以下目录进行查看&#xff1a; 目录 1.堆的概念及结构 2.堆的实现 2.1 堆的向上调整算法 2.2 堆的向下调整算法…

beego代理前端web的bug

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、beego代理前端web的bug总结 一、beego代理前端web的bug *报错&#xff0c;为web压缩包index.html里面的注释被错误解析&#xff0c;删掉就行 2024/02/22 10:2…

解析Hadoop三大核心组件:HDFS、MapReduce和YARN

目录 HadoopHadoop的优势 Hadoop的组成HDFS架构设计Yarn架构设计MapReduce架构设计 总结 在大数据时代&#xff0c;Hadoop作为一种开源的分布式计算框架&#xff0c;已经成为处理大规模数据的首选工具。它采用了分布式存储和计算的方式&#xff0c;能够高效地处理海量数据。Had…

蛇形矩阵1

题目描述 把数1&#xff0c;2&#xff0c;3&#xff0c;…&#xff0c;N*N按照“蛇形1”放入N*N的矩形中&#xff0c;输出结果。 下面是N10的蛇形1的图示 输入格式 第一行1个正整数&#xff1a;N&#xff0c;范围在[1,100]。 输出格式 N行&#xff0c;每行N个整数。 输入/…

docker下gitlab安装配置

一、安装及配置 1.gitlab镜像拉取 docker pull gitlab/gitlab-ce:latest2.运行gitlab镜像 docker run -d -p 443:443 -p 80:80 -p 222:22 --name gitlab --restart always --privilegedtrue -v /home/gitlab/config:/etc/gitlab -v /home/gitlab/logs:/var/log/gitlab -v …

小家电—简易过零检测电路

趁刚开工时间有空&#xff0c;总结分析下&#xff0c;在工作项目中常用过零检测电路。 图一 图二 图一在项目中较为常用&#xff0c;两个电路都是通过钳位二极管限幅产生过零脉冲信号。 过零信号高电平被钳位在5.7V&#xff0c;低电平为-0.7V 高电平&#xff1a;VCC0.7V 低电…

LeetCode第七题: 整数反转

题目描述 给你一个 32 位的有符号整数 x​ &#xff0c;返回将 x​ 中的数字部分反转后的结果。 如果反转后整数超过 32 位的有符号整数的范围 [−2^31, 2^31 − 1]​ &#xff0c;就返回 0。 假设环境不允许存储 64 位整数&#xff08;有符号或无符号&#xff09;。 示例 …

江大白 | 目标检测YOLOv9算法,重磅开源!(附论文及源码)

本文来源公众号“江大白”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;目标检测YOLOv9算法&#xff0c;重磅开源&#xff01;&#xff08;附论文及源码&#xff09; 以下文章来源于知乎&#xff1a;cvprLab作者&#xff1a;cvp…

服务器防漏扫

什么是漏扫&#xff1f; 漏扫是漏洞扫描的简称。漏洞扫描是一种安全测试方法&#xff0c;用于发现计算机系统、网络或应用程序中的潜在漏洞和安全弱点。通过使用自动化工具或软件&#xff0c;漏洞扫描可以检测系统中存在的已知漏洞&#xff0c;并提供相关的报告和建议&#xf…

Nexus Repository Manager

Nexus Repository Manager https://s01.oss.sonatype.org/#welcome https://mvnrepository.com/-CSDN博客

网络安全与信创产业发展:构建数字时代的护城河

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua&#xff0c;在这里我会分享我的知识和经验。&#x…

二,几何相交---1,预备--(1)元素唯一性EU

如何获取数组中元素的唯一性&#xff1f; 先通过o(nlogn)的时间复杂度排序(上下界都是o(nlogn)&#xff0c;再比较相邻的两个元素即可。

VScode连接远端服务器一直输入密码解决方法

文章目录 1 关闭远程连接2打开命令面板3 输入remote-ssh: kill vs code server on host… 1 关闭远程连接 2打开命令面板 3 输入remote-ssh: kill vs code server on host… remote-ssh: kill vs code server on host… 然后一路回车(选中出问题的主机)&#xff0c;输一遍密码…

LASSO算法

LASSO (Least Absolute Shrinkage and Selection Operator) 是一种回归分析的方法&#xff0c;它能够同时进行变量选择和正则化&#xff0c;以增强预测准确性和模型的解释性。LASSO通过在损失函数中加入一个L1惩罚项来实现这一点。该惩罚项对系数的绝对值进行约束。 基本概念 …

nginx-ingress-controller组件中Nginx的版本升级

参考链接&#xff1a;https://blog.csdn.net/qq_22824481/article/details/133761302 https://blog.csdn.net/mengfanshaoxia/article/details/127155020 https://blog.csdn.net/weixin_39961559/article/details/87935873 概要 业务区k…

Vue响应式状态ref()与reactive()

1. ref()声明响应式状态 <template><!--在DOM元素调用变量时,不需要指定输出变量的value,因为Vue会帮你输出.value但是注意,这个帮助只会帮助顶级的ref属性才会被解包--><div>{{ count }}</div><div>{{ object }}</div><div>{{ arr…