都说了别用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.mzph.cn/news/704648.shtml

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

相关文章

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

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

Open CASCADE学习|视图

目录 Mainwin.h Mainwin.cpp Mainwin.h ​#pragma once#include <QtWidgets/QMainWindow>#include "Displaywin.h"#include "OCC.h"class Mainwin : public QMainWindow{ Q_OBJECTpublic: Mainwin(QWidget* parent nullptr); ~Mainwin();​pri…

【Java程序设计】【C00277】基于Springboot的招生管理系统(有论文)

基于Springboot的招生管理系统&#xff08;有论文&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这是一个基于Springboot的招生管理系统 本系统分为系统功能模块、管理员功能模块以及学生功能模块。 系统功能模块&#xff1a;在系统首页可以查看首页、专业…

opengl 学习纹理

一.纹理是什么&#xff1f; 纹理是一个2D图片&#xff08;甚至也有1D和3D的纹理&#xff09;&#xff0c;它可以用来添加物体的细节&#xff1b;类似于图像一样&#xff0c;纹理也可以被用来储存大量的数据&#xff0c;这些数据可以发送到着色器上。 采样是指用纹理坐标来获取纹…

机器学习简单介绍

&#xff08;本文为简单介绍&#xff0c;内容源于网络和AI&#xff09; 当今世界,技术与创新的步伐日新月异。在各类智能技术当中,如果说有一个绝对不容忽视的关键词,那就是“机器学习”(Machine Learning)。它是人工智能领域的核心分支,使得机器获得从数据中学习、进而做出决…

Modern C++ std::variant的6个特性+原理

1 前言 上一节《Modern C std::variant的实现原理》我们简单分析了std::variant的实现原理&#xff0c;其实要学好C编程&#xff0c;除了看优秀的代码包括标准库实现&#xff0c;读文档也是很便捷且必须的一种办法。 本节我将逐条解析文档中的五个特性&#xff0c;解析的办法有…

H12-821_77

77.如图所示的交换网络&#xff0c;所有交换机都运行了STP协议&#xff0c;当拓扑稳定后&#xff0c;在以下哪台交换机上修改配置BPDU的发送周期&#xff0c;可以影响STD配置BPDU的发送周期&#xff1f; A.STC B.SWD C.SWA D.SWB 答案&#xff1a;C 注释&#xff1a; 在根桥上…

(十九)devops持续集成开发——jenkins的一些常用插件和工具的安装

前言 本节内容会着重介绍jenkins持续集成开发工具的一些常用插件安装以及全局工具的配置安装&#xff0c;并说明其主要作用。在开始插件和工具安装之前&#xff0c;我们要保证可以正常访问网络&#xff0c;并且使用国内的插件更新地址&#xff0c;便于插件的正常安装。官方的地…

【JavaEE】网络原理: HTTPS协议相关内容

目录 HTTPS 是什么 HTTPS 的工作过程 对称加密 非对称加密 引入证书 理解数据签名 通过证书解决黑客攻击 HTTPS 是什么 HTTPS也是一个应用层协议, 是在HTTP协议的基础上引入了一个加密层. HTTP协议内容都是按照文本的方式明文传输的, 这就导致在传输过程中出现一些被篡…

蜂邮EDM-新手教程-新手也能使用

一、登录注册账号&#xff0c;注册登录地址&#xff1a;fengemail.com 二、配置邮箱 选择“账号设置”——“邮箱设置”进行发信邮箱配置。每个账号将默认存在一个“系统默认接口”&#xff0c;点击右侧的编辑按钮即可对该配置进行修改。 注&#xff1a;发信邮箱暂不支持个人…

抖音数据抓取工具|抖音视频下载工具

抖音数据抓取工具是一款基于C#开发的高效实用软件&#xff0c;旨在为用户提供便捷的抖音视频数据获取和处理功能。该工具不仅支持通过关键词进行搜索抓取&#xff0c;还能够通过分享链接进行单个视频的抓取和下载&#xff0c;为用户提供了多样化的数据采集方式。 主要功能模块…

SpringMVC 学习(四)之获取请求参数

目录 1 通过 HttpServletRequest 获取请求参数 2 通过控制器方法的形参获取请求参数 3 通过 POJO 获取请求参数&#xff08;重点&#xff09; 1 通过 HttpServletRequest 获取请求参数 public String handler1(HttpServletRequest request) <form action"${pageCont…

安装python的docker库

文章目录 一、在线安装二、制作离线安装包2.1 报错处理 一、在线安装 先确定是否有pip命令。 yum install python-pip直接安装。 pip install docker查看docker库。 pip list二、制作离线安装包 在有互联网的环境下直接安装。 #docker为下载下来的包名。 pip download do…

typecho 给文章创建目录树

受益于 shortcode 短代码插件和泽泽短代码中目录树的显示样式&#xff0c;形成了自己实现添加文章目录的思路&#xff1a; 一、文章目录树的结构 <div id"toc"><div class"toc-left"><div class"toc-btn" type"button&quo…

搜维尔科技:用于运动科学的 OptiTrack,范围标记、步态捕捉!

OptiTrack 系统提供世界领先的测量精度和简单易用的工作流程&#xff0c;为研究人员和生物力学师的研究提供理想的 3D 跟踪数据。 对所有主要数字测力台、EMG 和模拟设备的本机即插即用支持为研究人员提供了在 Visual3D、MotionMonitor、MATLAB 和其他第三方生物力学软件包中进…

Android加载富文本

直接用webview加载&#xff1a; package com.example.testcsdnproject;import androidx.appcompat.app.AppCompatActivity;import android.annotation.SuppressLint; import android.graphics.Color; import android.os.Bundle; import android.util.Log; import android.webk…

Nexus Repository Manager

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

Ubuntu22.04环境下载安装中文搜狗输入法

0、查看CPU系统架构 确定架构后&#xff0c;下载对应的安装包&#xff0c;否则无法正常安装应用程序 1、进入搜狗拼音输入法官网&#xff0c;下载搜狗输入法 搜狗输入法-首页搜狗拼音输入法官网下载&#xff0c;荣获多个国内软件大奖的搜狗拼音输入法是一款打字更准、词库更大…

3、函数定义,函数调用,this指向总结,闭包

一、函数的定义方式 1、函数声明 function demo1() {var num 12var result Math.pow(num,2)//指数函数return result }2、函数表达式 var demo2 function (x,y) { //内置对象arguments前面的两个参数 是 x,yvar sum arguments[0] arguments[1]console.log(sum) }3、构…

精品基于SpringBoot+Vue的常规应急物资管理系统

《[含文档PPT源码等]精品基于SpringBootVue的常规应急物资管理系统[包运行成功]》该项目含有源码、文档、PPT、配套开发软件、软件安装教程、项目发布教程、包运行成功&#xff01; 软件开发环境及开发工具&#xff1a; Java——涉及技术&#xff1a; 前端使用技术&#xff…