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

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

[计算机网络]--IP协议

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、IP协议…

大数据构建知识图谱:从技术到实战的完整指南

文章目录 大数据构建知识图谱&#xff1a;从技术到实战的完整指南一、概述二、知识图谱的基础理论定义与分类核心组成历史与发展 三、知识获取与预处理数据源选择数据清洗实体识别 四、知识表示方法知识表示模型RDFOWL属性图模型 本体构建关系提取与表示 五、知识图谱构建技术图…

低功耗设计——门控时钟

1. 前言 芯片功耗组成中&#xff0c;有高达40%甚至更多是由时钟树消耗掉的。这个结果的原因也很直观&#xff0c;因为这些时钟树在系统中具有最高的切换频率&#xff0c;而且有很多时钟buffer&#xff0c;而且为了最小化时钟延时&#xff0c;它们通常具有很高的驱动强度。此外&…

2024Node.js零基础教程(小白友好型),nodejs新手到高手,(九)NodeJS入门——http模块

060_http模块_网页URL之绝对路径 hello&#xff0c;大家好&#xff0c;这一个小题的话我们来补充一个之前学习过的内容&#xff0c;就是网页当中的URL&#xff0c;咱们这个小题的话主要是来说一下绝对路径&#xff0c;有同学可能会说&#xff0c;这这这&#xff0c;不对劲&…

yolov8学习笔记(二)模型训练

目录 yolov8的模型训练 1、制作数据集&#xff08;标记数据集&#xff09; 2、模型训练&#xff08;标记数据集、参数设置、跟踪模型随时间的性能变化&#xff09; 2.1、租服务器训练 2.2、加训练参数 2.3、看训练时的参数&#xff08;有条件&#xff0c;就使用TensorBoard&…

nginx设置缓存时间

一、设置缓存时间 当网页数据返回给客户端后&#xff0c;可针对静态网页设置缓存时间&#xff0c;在配置文件内的http段内server段添加location&#xff0c;更改字段expires 1d来实现&#xff1a;避免重复请求&#xff0c;加快访问速度 第一步&#xff1a;修改主配置文件 #修…

vue基础操作(vue基础)

想到多少写多少把&#xff0c;其他的想起来了在写。也写了一些css的 input框的双向数据绑定 html <input value"123456" type"text" v-model"account" input"accou" class"bottom-line bottom" placeholder"请输入…

Vue 卸载eslint

卸载依赖 npm uninstall eslint --save 然后 进入package.json中&#xff0c;删除残留信息。 否则在执行卸载后&#xff0c;运行会报错。 之后再起项目。

ETL快速拉取物流信息

我国作为世界第一的物流大国&#xff0c;但是在目前的物流信息系统还存在着几大的痛点。主要包括以下几个方面&#xff1a; 数据孤岛&#xff1a;有些物流企业各个部门之间的数据标准不一致&#xff0c;难以实现数据共享和协同&#xff0c;容易导致信息孤岛。 操作繁琐&#x…

Fisc: A Large-scale Cloud-native-oriented File System——论文泛读

FAST 2023 Paper 元数据论文阅读笔记汇总 问题和局限性 尽管云原生技术取得了进展&#xff0c;但现有的分布式文件系统不适合多租户云原生应用&#xff0c;原因有两点。 它们的客户端通常较重&#xff0c;导致容器之间的资源复用水平较低。每个客户端都需要保留许多独占资源&…

HDFS中常用的Shell命令 全面且详细

HDFS中常用的Shell命令目录 一、ls命令 二、mkdir 命令 三、put命令 四、get命令 五、mv命令 六、rm命令 七、cp命令 八、cat命令 前言 安装好hadoop环境之后&#xff0c;可以执行hdfs相关的shell命令对hdfs文件系统进行操作&#xff0c;比如文件的创建、删除、修改文…

Vue packages version mismatch 报错解决

问题 npm run dev 运行项目的过程中&#xff0c;报错 Vue packages version mismatch 解决方法 根据报错不难看出是 vue 与 vue-template-compiler 版本产生了冲突&#xff0c;vue 与 vue-template-compiler 的版本是需要匹配的。所以解决的办法就是先修改其中一个的版本将 v…

基于Springboot的旅游网管理系统设计与实现(有报告)。Javaee项目,springboot项目。

演示视频&#xff1a; 基于Springboot的旅游网管理系统设计与实现&#xff08;有报告&#xff09;。Javaee项目&#xff0c;springboot项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层…

利用nginx内部访问特性实现静态资源授权访问

在nginx中&#xff0c;将静态资源设为internal&#xff1b;然后将前端的静态资源地址改为指向后端&#xff0c;在后端的响应头部中写上静态资源地址。 近期客户对我们项目做安全性测评&#xff0c;暴露出一些安全性问题&#xff0c;其中一个是有些静态页面&#xff08;*.html&…

数据安全策略

当您在第一线担负着确保公司的信息和系统尽可能免受风险的关键职责时&#xff0c;您的数据安全策略需要复杂且多层次。威胁可能有多种形式&#xff1a;恶意软件、黑客攻击、财务或信息盗窃、破坏、间谍活动&#xff0c;甚至是您信任的员工故意或无意的活动造成的。因此&#xf…

c++:蓝桥杯中的基础算法1(枚举,双指针)

枚举 基础概念&#xff1a; 枚举&#xff08;Enum&#xff09;是一种用户定义的数据类型&#xff0c;用于定义一个有限集合的命名常量。在C中&#xff0c;枚举类型可以通过关键字enum来定义。 下面是一个简单的枚举类型的定义示例&#xff1a; #include <iostream>enum…

Android T 远程动画显示流程其二——动画的添加流程(更新中)

前言 接着上篇文章分析 Android T 远程动画显示流程其一 切入点——处理应用的显示过渡 下面&#xff0c;我们以从桌面点击一个应用启动的场景来分析远程动画的流程&#xff0c;窗口添加的流程见Android T WMS窗口相关流程 这里我们从AppTransitionController.handleAppTran…

学习python的第7天,她不再开放她的听歌榜单

我下午登录上小号&#xff0c;打开聊天消息看到了她的回复&#xff0c;我很开心兴奋&#xff0c;可是她不再开放她的听歌榜单了&#xff0c;我感觉得到&#xff0c;我要失恋了。 “因为当年电视上看没有王菲版本的” “行”。 “那你以后还会开放听歌榜单吗&#xff1f;”我…

【监控】grafana图表使用快速上手

目录 1.前言 2.连接 3.图表 4.job和path 5.总结 1.前言 上一篇文章中&#xff0c;我们使用spring actuatorPrometheusgrafana实现了对一个spring boot应用的可视化监控。 【监控】Spring BootPrometheusGrafana实现可视化监控-CSDN博客 其中对grafana只是打开了一下&am…

【Azure 架构师学习笔记】- Azure Databricks (10) -- UC 使用

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Databricks】系列。 接上文 【Azure 架构师学习笔记】- Azure Databricks (9) – UC权限 在前面的文章&#xff1a;【Azure 架构师学习笔记】- Azure Databricks (6) - 配置Unity Catalog中演示了如何配置一个UC。 本文…