您好!
欢迎来到京东云开发者社区
登录
首页
博文
课程
大赛
工具
用户中心
开源
首页
博文
课程
大赛
工具
开源
更多
用户中心
开发者社区
>
博文
>
Java对象拷贝原理剖析及最佳实践
分享
打开微信扫码分享
点击前往QQ分享
点击前往微博分享
点击复制链接
Java对象拷贝原理剖析及最佳实践
自猿其说Tech
2022-08-18
IP归属:未知
73960浏览
计算机编程
### 1 前言 对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。 Java对象拷贝分为深拷贝和浅拷贝,目前常用的属性拷贝工具,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct都是浅拷贝。 #### 1.1 深拷贝 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容称为深拷贝。 深拷贝常见有以下四种实现方式: - 构造函数 - Serializable序列化 - 实现Cloneable接口 - JSON序列化 ![](//img1.jcloudcs.com/developer.jdcloud.com/0efe7004-4a90-4607-b1bb-841c59673a5f20220818144524.png) #### 1.2 浅拷贝 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝称为浅拷贝。通过实现Cloneabe接口并重写Object类中的clone()方法可以实现浅克隆。 ![](//img1.jcloudcs.com/developer.jdcloud.com/d38f460d-e558-4f35-ba5e-ef3271830d9620220818144550.png) ### 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循环获取类的字段属性,严重影响拷贝效率。 获取字段属性配置的核心代码如下: ```java 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核心代码如下: ```java 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缓存,避免了类的属性描述重复获取加载,通过懒加载,初次拷贝时加载所有属性描述。 ![](//img1.jcloudcs.com/developer.jdcloud.com/e8bcdee5-4024-4183-9cda-3d9a0b7a548020220818144744.png) ##### 2.1.3 Cglib BeanCopier 使用方式: ```java 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 ![](//img1.jcloudcs.com/developer.jdcloud.com/ad003880-4872-4dbd-bd29-9a2ad7d4793020220818144932.png) ##### 2.1.4 mapstruct 使用方式: - 引入pom依赖 - 声明转换接口 mapstruct基于注解,构建时自动生成实现类,调用链如下: MappingProcessor.process -> MappingProcessor.processMapperElements MapperCreationProcessor.process:生成实现类Mapper MapperRenderingProcessor:将实现类mapper,写入文件,生成impl文件 使用时需要声明转换接口,例如: ```java @Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) public interface AirDepartTaskConvert { AirDepartTaskConvert INSTANCE = getMapper(AirDepartTaskConvert.class); AirDepartTaskDto convertToDto(AirDepartTask airDepartTask); } ``` 生成的实现类如下: ```java 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转换为例,随着拷贝数据量的增大,研究拷贝数据耗时情况 ![](//img1.jcloudcs.com/developer.jdcloud.com/81a709b3-c1f2-43b3-a401-859159ff07af20220818145210.png) #### 2.3 拷贝选型 经过以上分析,随着数据量的增大,耗时整体呈上升趋势 - 整体情况下,Apache BeanUtils的性能最差,日常使用过程中不建议使用 - 在数据规模不大的情况下,spring、cglib、mapstruct差异不大,spring框架下建议使用spring的beanUtils,不需要额外引入依赖包 - 数据量大的情况下,建议使用cglib和mapstruct - 涉及大量数据转换,属性映射,格式转换的,建议使用mapstruct ### 3 最佳实践 #### 3.1 BeanCopier 使用时可以使用map缓存,减少同一类对象转换时,create次数 ```java /** * 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 - 映射枚举 - 嵌套映射 ```java @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 总结 以上就是我在使用对象拷贝过程中的一点浅谈。在日常系统开发过程中,要深究底层逻辑,哪怕发现一小点的改变能够使我们的系统更加稳定、顺畅,都是值得我们去改进的。 最后,希望随着我们的加入,系统会更加稳定、顺畅,我们会变得越来越优秀。 ------------ ###### 自猿其说Tech-JDL京东物流技术与数据智能部 ###### 作者:宁海翔
原创文章,需联系作者,授权转载
上一篇:领域驱动设计:DDD工程参考架构
下一篇:拜占庭将军和 Raft 共识算法
相关文章
Taro小程序跨端开发入门实战
Flutter For Web实践
配运基础数据缓存瘦身实践
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
本文将从Optional所解决的问题开始,逐层解剖,由浅入深,文中会出现Optioanl方法之间的对比,实践,误用情况分析,优缺点等。与大家一起,对这项Java8中的新特性,进行理解和深入。
01
Taro小程序跨端开发入门实战
为了让小程序开发更简单,更高效,我们采用 Taro 作为首选框架,我们将使用 Taro 的实践经验整理了出来,主要内容围绕着什么是 Taro,为什么用 Taro,以及 Taro 如何使用(正确使用的姿势),还有 Taro 背后的一些设计思想来进行展开,让大家能够对 Taro 有个完整的认识。
01
Flutter For Web实践
Flutter For Web 已经发布一年多时间,它的发布意味着我们可以真正地使用一套代码、一套资源部署整个大前端系统(包括:iOS、Android、Web)。渠道研发组经过一段时间的探索,使用Flutter For Web技术开发了移动端可视化编程平台—Flutter乐高,在这里希望和大家分享下使用Flutter For Web实践过程和踩坑实践
01
配运基础数据缓存瘦身实践
在基础数据的常规能力当中,数据的存取是最基础也是最重要的能力,为了整体提高数据的读取能力,缓存技术在基础数据的场景中得到了广泛的使用,下面会重点展示一下配运组近期针对数据缓存做的瘦身实践。
自猿其说Tech
文章数
426
阅读量
2149964
作者其他文章
01
深入JDK中的Optional
01
Taro小程序跨端开发入门实战
01
Flutter For Web实践
01
配运基础数据缓存瘦身实践
添加企业微信
获取1V1专业服务
扫码关注
京东云开发者公众号