為啥不建議用BeanUtils.copyProperties拷貝數據

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/12/22/17920721.html
-Advertisement-
Play Games

在實際的業務開發中,我們經常會碰到VO、BO、PO、DTO等對象屬性之間的賦值,當屬性較多的時候我們使用get,set的方式進行賦值的工作量相對較大,因此很多人會選擇使用spring提供的拷貝工具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拷貝數據。文中不足之處,歡迎補充和指正。

作者:京東科技 孫揚威

來源:京東雲開發者社區 轉載請註明來源


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • | 當談到非同步編程時,async/await是JavaScript中常用的功能之一。下麵是10個常用的await和async函數示例,以及對它們的代碼用途的解析: 1.非同步獲取數據 async function fetchData() { const response = await fetch(' ...
  • 在前面隨筆《在Winform應用中增加通用的業務編碼規則生成》,我介紹了基於Winform和WPF的一個通用的業務編碼規則的管理功能,本篇隨筆介紹基於後端Web API介面,實現快速的Vue3+ElementPlus前端界面的開發整合,同樣是基於代碼生成工具實現快速的前端代碼的生成處理。 ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹`QCharts`二維繪圖組件的常用方法及靈活運用。Qt Charts 提供了一個強大且易於使用的工具集,用於在 ... ...
  • Detours 代碼倉庫: https://github.com/microsoft/Detours x64寫一個任意地址hook要比x86麻煩的多,所以這裡直接封裝框架來用於x64的hook。 Detours是微軟發佈的一個API hook框架,同時支持x86和x64,看文檔說也支持ARM和ARM ...
  • 一款好用又強大的開源社區,採用主流的互聯網技術架構、全新的UI設計、支持一鍵源碼部署,擁有完整的文章&教程發佈/搜索/評論/統計流程等,代碼完全開源,沒有任何二次封裝,是一個非常適合二次開發/實戰的現代化社區項目。 ...
  • Python 介紹 Python 是一種 高級 的、解釋型 的、通用 的編程語言。其設計哲學強調代碼的可讀性,使用顯著的縮進。Python 是 動態類型 和 垃圾收集 的。 基本語法 設置 Python 環境並開始基礎知識。 文章鏈接:Python 安裝與快速入門 變數 變數用於存儲在電腦程式中引 ...
  • 平時習慣了./和../作為訪問目錄的路徑,但今天使用golang中fs.ReadDir這個函數的時候發現這個習慣是不正確的。 但是常用的命令並沒有分很清楚.和./ 在這幾個命令中使用.或./都可以到達目錄下 ls cd 錯誤示範 package main import ( "fmt" "io/fs" ...
  • 數據的預處理是數據分析,或者機器學習訓練前的重要步驟。通過數據預處理,可以 提高數據質量,處理數據的缺失值、異常值和重覆值等問題,增加數據的準確性和可靠性 整合不同數據,數據的來源和結構可能多種多樣,分析和訓練前要整合成一個數據集 提高數據性能,對數據的值進行變換,規約等(比如無量綱化),讓演算法更加 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...