Java對象拷貝原理剖析及最佳實踐

来源:https://www.cnblogs.com/Jcloud/archive/2022/11/24/16921336.html
-Advertisement-
Play Games

作者:寧海翔 1 前言 對象拷貝,是我們在開發過程中,繞不開的過程,既存在於Po、Dto、Do、Vo各個表現層數據的轉換,也存在於系統交互如序列化、反序列化。 Java對象拷貝分為深拷貝和淺拷貝,目前常用的屬性拷貝工具,包括Apache的BeanUtils、Spring的BeanUtils、Cgli ...


作者:寧海翔

1 前言

對象拷貝,是我們在開發過程中,繞不開的過程,既存在於Po、Dto、Do、Vo各個表現層數據的轉換,也存在於系統交互如序列化、反序列化。

Java對象拷貝分為深拷貝和淺拷貝,目前常用的屬性拷貝工具,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct都是淺拷貝。

1.1 深拷貝

深拷貝:對基本數據類型進行值傳遞,對引用數據類型,創建一個新的對象,並複製其內容稱為深拷貝。

深拷貝常見有以下四種實現方式:

  • 構造函數
  • Serializable序列化
  • 實現Cloneable介面
  • JSON序列化

1.2 淺拷貝

淺拷貝:對基本數據類型進行值傳遞,對引用數據類型進行引用傳遞般的拷貝稱為淺拷貝。通過實現Cloneabe介面並重寫Object類中的clone()方法可以實現淺克隆。

2 常用對象拷貝工具原理剖析及性能對比

目前常用的屬性拷貝工具,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct。

  • Apache BeanUtils:BeanUtils是Apache commons組件裡面的成員,由Apache提供的一套開源 api,用於簡化對javaBean的操作,能夠對基本類型自動轉換。
  • Spring BeanUtils:BeanUtils是spring框架下自帶的工具,在org.springframework.beans包下, spring項目可以直接使用。
  • Cglib BeanCopier:cglib(Code Generation Library)是一個強大的、高性能、高質量的代碼生成類庫,BeanCopier依托於cglib的位元組碼增強能力,動態生成實現類,完成對象的拷貝。
  • mapstruct:mapstruct 是一個 Java註釋處理器,用於生成類型安全的 bean 映射類,在構建時,根據註解生成實現類,完成對象拷貝。

2.1 原理分析

2.1.1 Apache BeanUtils

使用方式:BeanUtils.copyProperties(target, source);
BeanUtils.copyProperties 對象拷貝的核心代碼如下:


// 1.獲取源對象的屬性描述
PropertyDescriptor[] origDescriptors = this.getPropertyUtils().getPropertyDescriptors(orig);
PropertyDescriptor[] temp = origDescriptors;
int length = origDescriptors.length;
String name;
Object value;

// 2.迴圈獲取源對象每個屬性,設置目標對象屬性值
for(int i = 0; i < length; ++i) {
PropertyDescriptor origDescriptor = temp[i];
name = origDescriptor.getName();
// 3.校驗源對象欄位可讀切目標對象該欄位可寫
if (!"class".equals(name) && this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {
try {
// 4.獲取源對象欄位值
value = this.getPropertyUtils().getSimpleProperty(orig, name);
// 5.拷貝屬性
this.copyProperty(dest, name, value);
} catch (NoSuchMethodException var10) {
}
}
}

迴圈遍歷源對象的每個屬性,對於每個屬性,拷貝流程為:

  • 校驗來源類的欄位是否可讀isReadable
  • 校驗目標類的欄位是否可寫isWriteable
  • 獲取來源類的欄位屬性值getSimpleProperty
  • 獲取目標類欄位的類型type,併進行類型轉換
  • 設置目標類欄位的值

由於單欄位拷貝時每個階段都會調用PropertyUtilsBean.getPropertyDescriptor獲取屬性配置,而該方法通過for迴圈獲取類的欄位屬性,嚴重影響拷貝效率。
獲取欄位屬性配置的核心代碼如下:

PropertyDescriptor[] descriptors = this.getPropertyDescriptors(bean);
if (descriptors != null) {
for (int i = 0; i < descriptors.length; ++i) {
if (name.equals(descriptors[i].getName())) {
return descriptors[i];
}
}
}

2.1.2 Spring BeanUtils

使用方式: BeanUtils.copyProperties(source, target);
BeanUtils.copyProperties核心代碼如下:

PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
PropertyDescriptor[] arr$ = targetPds;
int len$ = targetPds.length;
for(int i$ = 0; i$ < len$; ++i$) {
PropertyDescriptor targetPd = arr$[i$];
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
} catch (Throwable var15) {
throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
}
}
}
}
}

拷貝流程簡要描述如下:

  • 獲取目標類的所有屬性描述
  • 迴圈目標類的屬性值做以下操作
    • 獲取目標類的寫方法
    • 獲取來源類的該屬性的屬性描述(緩存獲取)
    • 獲取來源類的讀方法
    • 讀來源屬性值
    • 寫目標屬性值

與Apache BeanUtils的屬性拷貝相比,Spring通過Map緩存,避免了類的屬性描述重覆獲取載入,通過懶載入,初次拷貝時載入所有屬性描述。

2.1.3 Cglib BeanCopier

使用方式:

BeanCopier beanCopier = BeanCopier.create(AirDepartTask.class, AirDepartTaskDto.class, false);
beanCopier.copy(airDepartTask, airDepartTaskDto, null);

create調用鏈如下:

BeanCopier.create
-> BeanCopier.Generator.create
-> AbstractClassGenerator.create
->DefaultGeneratorStrategy.generate
-> BeanCopier.Generator.generateClass

BeanCopier 通過cglib動態代理操作位元組碼,生成一個複製類,觸發點為BeanCopier.create

2.1.4 mapstruct

使用方式:

  • 引入pom依賴
  • 聲明轉換介面

mapstruct基於註解,構建時自動生成實現類,調用鏈如下:
MappingProcessor.process -> MappingProcessor.processMapperElements
MapperCreationProcessor.process:生成實現類Mapper
MapperRenderingProcessor:將實現類mapper,寫入文件,生成impl文件
使用時需要聲明轉換介面,例如:

@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface AirDepartTaskConvert {
AirDepartTaskConvert INSTANCE = getMapper(AirDepartTaskConvert.class);
AirDepartTaskDto convertToDto(AirDepartTask airDepartTask);
}

生成的實現類如下:

public class AirDepartTaskConvertImpl implements AirDepartTaskConvert {

@Override
public AirDepartTaskDto convertToDto(AirDepartTask airDepartTask) {
if ( airDepartTask == null ) {
return null;
}

AirDepartTaskDto airDepartTaskDto = new AirDepartTaskDto();

airDepartTaskDto.setId( airDepartTask.getId() );
airDepartTaskDto.setTaskId( airDepartTask.getTaskId() );
airDepartTaskDto.setPreTaskId( airDepartTask.getPreTaskId() );
List<String> list = airDepartTask.getTaskBeginNodeCodes();
if ( list != null ) {
airDepartTaskDto.setTaskBeginNodeCodes( new ArrayList<String>( list ) );
}
// 其他屬性拷貝
airDepartTaskDto.setYn( airDepartTask.getYn() );

return airDepartTaskDto;
}
}

2.2 性能對比

以航空業務系統中發貨任務po到dto轉換為例,隨著拷貝數據量的增大,研究拷貝數據耗時情況

2.3 拷貝選型

經過以上分析,隨著數據量的增大,耗時整體呈上升趨勢

  • 整體情況下,Apache BeanUtils的性能最差,日常使用過程中不建議使用
  • 在數據規模不大的情況下,spring、cglib、mapstruct差異不大,spring框架下建議使用spring的beanUtils,不需要額外引入依賴包
  • 數據量大的情況下,建議使用cglib和mapstruct
  • 涉及大量數據轉換,屬性映射,格式轉換的,建議使用mapstruct

3 最佳實踐

3.1 BeanCopier

使用時可以使用map緩存,減少同一類對象轉換時,create次數

/**
* BeanCopier的緩存,避免頻繁創建,高效復用
*/
private static final ConcurrentHashMap<String, BeanCopier> BEAN_COPIER_MAP_CACHE = new ConcurrentHashMap<String, BeanCopier>();

/**
* BeanCopier的copyBean,高性能推薦使用,增加緩存
*
* @param source 源文件的
* @param target 目標文件
*/
public static void copyBean(Object source, Object target) {
String key = genKey(source.getClass(), target.getClass());
BeanCopier beanCopier;
if (BEAN_COPIER_MAP_CACHE.containsKey(key)) {
beanCopier = BEAN_COPIER_MAP_CACHE.get(key);
} else {
beanCopier = BeanCopier.create(source.getClass(), target.getClass(), false);
BEAN_COPIER_MAP_CACHE.put(key, beanCopier);
}
beanCopier.copy(source, target, null);
}

/**
* 不同類型對象數據copylist
*
* @param sourceList
* @param targetClass
* @param <T>
* @return
*/
public static <T> List<T> copyListProperties(List<?> sourceList, Class<T> targetClass) throws Exception {
if (CollectionUtils.isNotEmpty(sourceList)) {
List<T> list = new ArrayList<T>(sourceList.size());
for (Object source : sourceList) {
T target = copyProperties(source, targetClass);
list.add(target);
}
return list;
}
return Lists.newArrayList();
}

/**
* 返回不同類型對象數據copy,使用此方法需註意不能覆蓋預設的無參構造方法
*
* @param source
* @param targetClass
* @param <T>
* @return
*/
public static <T> T copyProperties(Object source, Class<T> targetClass) throws Exception {
T target = targetClass.newInstance();
copyBean(source, target);
return target;
}

/**
* @param srcClazz 源class
* @param tgtClazz 目標class
* @return string
*/
private static String genKey(Class<?> srcClazz, Class<?> tgtClazz) {
return srcClazz.getName() + tgtClazz.getName();
}

3.2 mapstruct

mapstruct支持多種形式對象的映射,主要有下麵幾種

  • 基本映射
  • 映射表達式
  • 多個對象映射到一個對象
  • 映射集合
  • 映射map
  • 映射枚舉
  • 嵌套映射
@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface AirDepartTaskConvert {
AirDepartTaskConvert INSTANCE = getMapper(AirDepartTaskConvert.class);

// a.基本映射
@Mapping(target = "createTime", source = "updateTime")
// b.映射表達式
@Mapping(target = "updateTimeStr", expression = "java(new SimpleDateFormat( \"yyyy-MM-dd\" ).format(airDepartTask.getCreateTime()))")
AirDepartTaskDto convertToDto(AirDepartTask airDepartTask);
}

@Mapper
public interface AddressMapper {
AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

// c.多個對象映射到一個對象
@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}

@Mapper
public interface CarMapper {
// d.映射集合
Set<String> integerSetToStringSet(Set<Integer> integers);

List<CarDto> carsToCarDtos(List<Car> cars);

CarDto carToCarDto(Car car);
// e.映射map
@MapMapping(valueDateFormat = "dd.MM.yyyy")
Map<String,String> longDateMapToStringStringMap(Map<Long, Date> source);

// f.映射枚舉
@ValueMappings({
@ValueMapping(source = "EXTRA", target = "SPECIAL"),
@ValueMapping(source = "STANDARD", target = "DEFAULT"),
@ValueMapping(source = "NORMAL", target = "DEFAULT")
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
// g.嵌套映射
@Mapping(target = "fish.kind", source = "fish.type")
@Mapping(target = "fish.name", ignore = true)
@Mapping(target = "ornament", source = "interior.ornament")
@Mapping(target = "material.materialType", source = "material")
@Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")
FishTankDto map( FishTank source );
}

4 總結

以上就是我在使用對象拷貝過程中的一點淺談。在日常系統開發過程中,要深究底層邏輯,哪怕發現一小點的改變能夠使我們的系統更加穩定、順暢,都是值得我們去改進的。

最後,希望隨著我們的加入,系統會更加穩定、順暢,我們會變得越來越優秀。


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

-Advertisement-
Play Games
更多相關文章
  • 一、介紹 Java由Sun Microsystems發明併在1995年發佈,是世界上使用最廣泛的編程語言之一。Java是一個通用編程語言。由於它擁有功能強大的庫、運行時、簡單的語法、平臺無關(Write Once, Run Anywhere - WORA)以及令人敬畏的社區從而吸引了很多的開發者。 ...
  • 前言 MQ(Message Queue)就是消息隊列,其有點有很多:解耦、非同步、削峰等等,本文來聊一下RabbitMQ的一些概念以及使用。 RabbitMq 案例 Springboot整合RabbitMQ簡單案例 基本概念 Exchange:消息交換機,它指定消息按什麼規則,路由到哪個隊列。 Que ...
  • 使用mybatis-plus批量插入的時候報錯信息為:com.alibaba.druid.sql.parser.ParserException: syntax error, expect ')', pos 40, line 1, column 41, token EOF 排查sql日誌發現生成的sq ...
  • 什麼是數組? 官方定義:數組(Array)是有序的元素序列。 簡單來說:可以把數組想象成一個線性數據結構,用來裝東西的,每個東西有自己的編號,並且編號是從0 開始(重點) 直接來看語法: 數據類型 [] 標識符(自己取的名字) = new 數據類型 [數組裡元素個數] 或者 數據類型 [] 標識符( ...
  • 接上篇: 通過位元組碼,我們瞭解了class文件的結構 通過運行數據區,我們瞭解了jvm內部的記憶體劃分及結構 接下來,讓我們看看,位元組碼怎麼進入jvm的記憶體空間,各自進入那個空間,以及怎麼跑起來。 4.1 載入 4.1.1 概述 類的載入就是將class文件中的二進位數據讀取到記憶體中,然後將該位元組流所 ...
  • 主要推到了極化碼編碼矩陣生成迭代方式,並針對遞歸方法和按位生成(硬體生成不適用遞歸方案)的方法用matlab實現。 通道組合 W表示原始B-DMC通道。 下圖是兩個通道組合的例子。 長度為2的通道組合模型 長度為4的通道組合模型 長度為N/2與N的通道組合形式 G的推導及性質 G公式推導 編碼矩陣生 ...
  • 對於緩存容器而言,容量限制與數據淘汰是兩個基礎且核心的關鍵點,也是實際使用的時候使用頻率最高的特性。本篇在上一文基礎上深入解讀下Guava Cache中的容量限制與數據淘汰策略的實現與使用約束。 ...
  • Spring,作為 Java EE 的事實規範,在2022年11月16日發佈了最新的 6.0.0 GA 版本。這個版本是框架後續新生代的初始版本,擁抱持續創新的 OpenJDK 和 Java 生態。新的版本以 Java 17+ 作為 baseline,並遷移至 Jakarta EE 9+(即,使用 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...