Stories

Detail Return Return

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

作者:寧海翔

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

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

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

user avatar u_16756731 Avatar kanjianliao Avatar yinzhixiaxue Avatar alibabawenyujishu Avatar cbuc Avatar tdengine Avatar wosign Avatar yimo Avatar huanledeyanjing Avatar miaodaxia Avatar shenchendexiaodao Avatar jinl9s27 Avatar
Favorites 14 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.