| 
 | 
	
 
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; 
name = origDescriptor.getName(); 
// 3.校验源对象字段可读切目标对象该字段可写 
if (!&#34;class&#34;.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.getName())) { 
return descriptors; 
} 
} 
}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(&#34;Could not copy property &#39;&#34; + targetPd.getName() + &#34;&#39; from source to target&#34;, 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 
 
使用方式: 
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 = &#34;createTime&#34;, source = &#34;updateTime&#34;) 
 // b.映射表达式 
 @Mapping(target = &#34;updateTimeStr&#34;, expression = &#34;java(new SimpleDateFormat( \&#34;yyyy-MM-dd\&#34; ).format(airDepartTask.getCreateTime()))&#34;) 
 AirDepartTaskDto convertToDto(AirDepartTask airDepartTask); 
} 
 
@Mapper 
public interface AddressMapper { 
 AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class); 
 
 // c.多个对象映射到一个对象 
 @Mapping(source = &#34;person.description&#34;, target = &#34;description&#34;) 
 @Mapping(source = &#34;address.houseNo&#34;, target = &#34;houseNumber&#34;) 
 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 = &#34;dd.MM.yyyy&#34;) 
 Map<String,String> longDateMapToStringStringMap(Map<Long, Date> source); 
 
 // f.映射枚举 
 @ValueMappings({ 
 @ValueMapping(source = &#34;EXTRA&#34;, target = &#34;SPECIAL&#34;), 
 @ValueMapping(source = &#34;STANDARD&#34;, target = &#34;DEFAULT&#34;), 
 @ValueMapping(source = &#34;NORMAL&#34;, target = &#34;DEFAULT&#34;) 
 }) 
 ExternalOrderType orderTypeToExternalOrderType(OrderType orderType); 
 // g.嵌套映射 
 @Mapping(target = &#34;fish.kind&#34;, source = &#34;fish.type&#34;) 
 @Mapping(target = &#34;fish.name&#34;, ignore = true) 
 @Mapping(target = &#34;ornament&#34;, source = &#34;interior.ornament&#34;) 
 @Mapping(target = &#34;material.materialType&#34;, source = &#34;material&#34;) 
 @Mapping(target = &#34;quality.report.organisation.name&#34;, source = &#34;quality.report.organisationName&#34;) 
 FishTankDto map( FishTank source ); 
} 
4 总结 
 
以上就是我在使用对象拷贝过程中的一点浅谈。在日常系统开发过程中,要深究底层逻辑,哪怕发现一小点的改变能够使我们的系统更加稳定、顺畅,都是值得我们去改进的。 
最后,希望随着我们的加入,系统会更加稳定、顺畅,我们会变得越来越优秀。 
<hr/>欢迎来京东云开发者社区交流技术,原文地址Java对象拷贝原理剖析及最佳实践-京东云开发者社区 
作者:宁海翔 |   
 
 
 
 |