天天看點

Java 對象拷貝原理剖析

作者:開始自媒體

1 前言

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

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

1.1 深拷貝

深拷貝:對基本資料類型進行值傳遞,對引用資料類型,建立一個新的對象,并複制其内容稱為深拷貝。

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

  • 構造函數
  • Serializable 序列化
  • 實作 Cloneable 接口
  • JSON 序列化
Java 對象拷貝原理剖析

1.2 淺拷貝

淺拷貝:對基本資料類型進行值傳遞,對引用資料類型進行引用傳遞般的拷貝稱為淺拷貝。通過實作 Cloneabe 接口并重寫 Object 類中的 clone () 方法可以實作淺克隆。

Java 對象拷貝原理剖析

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 緩存,避免了類的屬性描述重複擷取加載,通過懶加載,初次拷貝時加載所有屬性描述。

Java 對象拷貝原理剖析

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

Java 對象拷貝原理剖析

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 轉換為例,随着拷貝資料量的增大,研究拷貝資料耗時情況

Java 對象拷貝原理剖析

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 總結

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

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

繼續閱讀