作者:寧海翔
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 總結
以上就是我在使用物件複製過程中的一點淺談。在日常系統開發過程中,要深究底層邏輯,哪怕發現一小點的改變能夠使我們的系統更加穩定、順暢,都是值得我們去改進的。
最後,希望隨著我們的加入,系統會更加穩定、順暢,我們會變得越來越優秀。