都說了別用BeanUtils.copyProperties,這不翻車了吧

来源:https://www.cnblogs.com/kdaddy/p/18035763
-Advertisement-
Play Games

即使再小再簡單的需求,作為研發開發完畢之後,我們可以直接上線麽?其實很多時候事故往往就是由於“不以為意”發生的。事故的發生往往也遵循“墨菲定律”,這就要求我們更要敬畏線上,再小的需求點都需要經過嚴格的測試驗證才能上線。 ...


分享是最有效的學習方式。

博客: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的壞話,那麼這種拷貝方式是不是真的就一無是處呢?其實不是的,所謂存在即合理。很多時候使用的時候踩坑說白了我們沒有理解好這個拷貝工具的特性。很多時候大家在使用使用一個技術的時候都是囫圇吞棗,為了使用而去使用,壓根就沒有深入瞭解這個技術的特性以及使用註意點。所以在我們使用第三方工具的時候,我們需要更好地瞭解其特性,知其所以然才能更好更正確的使用。小伙伴們你們覺得呢?

如果還有需要補充的點,也歡迎小伙伴們留言。

我是老貓,10year+資深研發,讓我們一起聊聊技術,聊聊職場,聊聊人生~ 更多精彩,歡迎關註公眾號“程式員老貓”。 個人博客:https://blog.ktdaddy.com/
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一:背景 1. 講故事 過年喝了不少酒,腦子不靈光了,停了將近一個月沒寫博客,今天就當新年開工寫一篇吧。 去年年初有位朋友找到我,說他們的系統會偶發性崩潰,在網上也發了不少帖子求助,沒找到自己滿意的答案,讓我看看有沒有什麼線索,看樣子這是一個牛皮蘚的問題,既然對方有了dump,那就分析起來吧。 二: ...
  • 哈嘍大家好,我是鹹魚。 之前寫過兩篇關於 SSL 過期巡檢腳本的文章: SSL 證書過期巡檢腳本 SSL 證書過期巡檢腳本(Python 版) 這兩篇文章都是講如何通過腳本去自動檢測 SSL 過期時間的,當我們發現某一功能變數名稱的 SSL 證書過期之後,就要及時更換。 如果這個功能變數名稱下有很多伺服器,我們一臺 ...
  • Win + R 運行 rdpclip.exe 或者任務管理器關閉rdpclip.exe並重新運行。 就這麼簡單~ ...
  • 非常歡迎大家來到Apache DolphinScheduler社區!隨著開源技術在全球範圍內的快速發展,社區的貢獻者 “同仁” 一直致力於構建一個強大而活躍的開源調度系統社區,為用戶提供高效、可靠的任務調度和工作流管理解決方案。 在過去的一段時間里,我們取得了一些重要的成就,但我們的願景遠未實現。為 ...
  • Android 多包名,icon 本篇文章主要記錄下android 下的同一工程,打包時配置不同的包名,icon,名稱等信息. 1: 多包名 首先講述下如何配置多包名. 在build.gralde的android 標簽下添加: productFlavors{ xiaomi{ applicationI ...
  • 寫在前面 我知道自己現在的狀態很不好,以為放個假能好好放鬆下心情,結果昨晚做夢還在工作,調試代碼,和領導彙報工作。 天吶,明明是在放假,可大腦還在考慮工作的事,我的天那,這是怎麼了? Vue頁面參數傳遞 1、任務拆解 頁面跳轉時帶上當前電子書id參數ebookId 新增/編輯文檔時,讀取電子書id參 ...
  • 枚舉Enum是在多種語言中都有的一種數據類型,用於表示一組特定相關的常量數據集合,如性別(男、女)、數據狀態(可用、禁用)、垂直對齊(頂端、居中、底部)、星期等。特點是數據值固定,不會變,存儲和顯示的內容不同。然而在JavaScript中並沒有枚舉Enum類型,TypeScript算是有(本文中暫沒... ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、是什麼 許可權是對特定資源的訪問許可,所謂許可權控制,也就是確保用戶只能訪問到被分配的資源 而前端許可權歸根結底是請求的發起權,請求的發起可能有下麵兩種形式觸發 頁面載入觸發 頁面上的按鈕點擊觸發 總的來說,所有的請求發起都觸發自前端路由或 ...
一周排行
    -Advertisement-
    Play Games
  • 最近做項目過程中,使用到了海康相機,官方只提供了C/C++的SDK,沒有搜尋到一個合適的封裝了的C#庫,故自己動手,簡單的封裝了一下,方便大家也方便自己使用和二次開發 ...
  • 前言 MediatR 是 .NET 下的一個實現消息傳遞的庫,輕量級、簡潔高效,用於實現進程內的消息傳遞機制。它基於中介者設計模式,支持請求/響應、命令、查詢、通知和事件等多種消息傳遞模式。通過泛型支持,MediatR 可以智能地調度不同類型的消息,非常適合用於領域事件處理。 在本文中,將通過一個簡 ...
  • 前言 今天給大家推薦一個超實用的開源項目《.NET 7 + Vue 許可權管理系統 小白快速上手》,DncZeus的願景就是做一個.NET 領域小白也能上手的簡易、通用的後臺許可權管理模板系統基礎框架。 不管你是技術小白還是技術大佬或者是不懂前端Vue 的新手,這個項目可以快速上手讓我們從0到1,搭建自 ...
  • 第1章:WPF概述 本章目標 瞭解Windows圖形演化 瞭解WPF高級API 瞭解解析度無關性概念 瞭解WPF體繫結構 瞭解WPF 4.5 WPF概述 ​ 歡迎使用 Windows Presentation Foundation (WPF) 桌面指南,這是一個與解析度無關的 UI 框架,使用基於矢 ...
  • 在日常開發中,並不是所有的功能都是用戶可見的,還在一些背後默默支持的程式,這些程式通常以服務的形式出現,統稱為輔助角色服務。今天以一個簡單的小例子,簡述基於.NET開發輔助角色服務的相關內容,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 第3章:佈局 本章目標 理解佈局的原則 理解佈局的過程 理解佈局的容器 掌握各類佈局容器的運用 理解 WPF 中的佈局 WPF 佈局原則 ​ WPF 視窗只能包含單個元素。為在WPF 視窗中放置多個元素並創建更貼近實用的用戶男面,需要在視窗上放置一個容器,然後在這個容器中添加其他元素。造成這一限制的 ...
  • 前言 在平時項目開發中,定時任務調度是一項重要的功能,廣泛應用於後臺作業、計劃任務和自動化腳本等模塊。 FreeScheduler 是一款輕量級且功能強大的定時任務調度庫,它支持臨時的延時任務和重覆迴圈任務(可持久化),能夠按秒、每天/每周/每月固定時間或自定義間隔執行(CRON 表達式)。 此外 ...
  • 目錄Blazor 組件基礎路由導航參數組件參數路由參數生命周期事件狀態更改組件事件 Blazor 組件 基礎 新建一個項目命名為 MyComponents ,項目模板的交互類型選 Auto ,其它保持預設選項: 客戶端組件 (Auto/WebAssembly): 最終解決方案裡面會有兩個項目:伺服器 ...
  • 先看一下效果吧: isChecked = false 的時候的效果 isChecked = true 的時候的效果 然後我們來實現一下這個效果吧 第一步:創建一個空的wpf項目; 第二步:在項目裡面添加一個checkbox <Grid> <CheckBox HorizontalAlignment=" ...
  • 在編寫上位機軟體時,需要經常處理命令拼接與其他設備進行通信,通常對不同的命令封裝成不同的方法,擴展稍許麻煩。 本次擬以特性方式實現,以兼顧維護性與擴展性。 思想: 一種命令對應一個類,其類中的各個屬性對應各個命令段,通過特性的方式,實現其在這包數據命令中的位置、大端或小端及其轉換為對應的目標類型; ...