作者:宁海翔
对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。
Java对象拷贝分为深拷贝和浅拷贝,目前常用的属性拷贝工具,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct都是浅拷贝。
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容称为深拷贝。
深拷贝常见有以下四种实现方式:
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝称为浅拷贝。通过实现Cloneabe接口并重写Object类中的clone()方法可以实现浅克隆。
目前常用的属性拷贝工具,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct。
2.1.1 Apache BeanUtils
使用方式:BeanUtils.copyProperties(target, source);
BeanUtils.copyProperties 对象拷贝的核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | / / 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) { } } } |
循环遍历源对象的每个属性,对于每个属性,拷贝流程为:
由于单字段拷贝时每个阶段都会调用PropertyUtilsBean.getPropertyDescriptor获取属性配置,而该方法通过for循环获取类的字段属性,严重影响拷贝效率。
获取字段属性配置的核心代码如下:
1 2 3 4 5 6 7 8 | 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]; } } } |
使用方式: BeanUtils.copyProperties(source, target);
BeanUtils.copyProperties核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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
使用方式:
1 2 | 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文件
使用时需要声明转换接口,例如:
1 2 3 4 5 | @Mapper (nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) public interface AirDepartTaskConvert { AirDepartTaskConvert INSTANCE = getMapper(AirDepartTaskConvert. class ); AirDepartTaskDto convertToDto(AirDepartTask airDepartTask); } |
生成的实现类如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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; } } |
以航空业务系统中发货任务po到dto转换为例,随着拷贝数据量的增大,研究拷贝数据耗时情况
经过以上分析,随着数据量的增大,耗时整体呈上升趋势
使用时可以使用map缓存,减少同一类对象转换时,create次数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | / * * * 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(); } |
mapstruct支持多种形式对象的映射,主要有下面几种
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | @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 ); } |
以上就是我在使用对象拷贝过程中的一点浅谈。在日常系统开发过程中,要深究底层逻辑,哪怕发现一小点的改变能够使我们的系统更加稳定、顺畅,都是值得我们去改进的。
最后,希望随着我们的加入,系统会更加稳定、顺畅,我们会变得越来越优秀。