Java对象拷贝原理剖析及最佳实践
作者:宁海翔
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 总结
以上就是我在使用对象拷贝过程中的一点浅谈。在日常系统开发过程中,要深究底层逻辑,哪怕发现一小点的改变能够使我们的系统更加稳定、顺畅,都是值得我们去改进的。
最后,希望随着我们的加入,系统会更加稳定、顺畅,我们会变得越来越优秀。
Java对象拷贝原理剖析及最佳实践的更多相关文章
- 《深入理解Android:Telephon原理剖析与最佳实践》学习笔记(系统框架)
Android智能手机的系统结构: 智能手机的硬件基本结构大多采用双处理器架构:主处理器和从处理器,主处理器主要运行开放式操作系统以及操作系统之上的应用,负责整个系统的控制,称之为AP,从处理 ...
- 《AngularJS深度剖析与最佳实践》简介
由于年末将至,前阵子一直忙于工作的事务,不得已暂停了微信订阅号的更新,我将会在后续的时间里尽快的继续为大家推送更多的博文.毕竟一个人的力量微薄,精力有限,希望大家能理解,仍然能一如既往的关注和支持sh ...
- 【转】Java中关于异常处理的十个最佳实践
原文地址:http://www.searchsoa.com.cn/showcontent_71960.htm 导读:异常处理是书写强健Java应用的一个重要部分,Java许你创建新的异常,并通过使用 ...
- Java基础-hashMap原理剖析
Java基础-hashMap原理剖析 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.什么是哈希(Hash) 答:Hash就是散列,即把对象打散.举个例子,有100000条数 ...
- attilax.java 注解的本质and 使用最佳实践(3)O7
attilax.java 注解的本质and 使用最佳实践(3)O7 1. 定义pojo 1 2. 建立注解By eclipse tps 1 3. 注解参数的可支持数据类型: 2 4. 注解处理器 2 ...
- paip.java win程序迁移linux的最佳实践
paip.java win程序迁移linux的最佳实践 1.class load路径的问题... windows哈第一的从calsses目录加载,,而linux优先从jar加载.. 特别的是修理了ja ...
- KiCad EDA 原理图库的最佳实践
KiCad EDA 原理图库的最佳实践 由于有 Alias 别名元件,可以不用一个每一个元件都有一个元件. 对每种元件类型建议一个元件库. 因为 Value 和 元件名是一样的,所以元件名要尽可能的简 ...
- 推荐《深入浅出深度学习原理剖析与python实践》PDF+代码
<深入浅出深度学习原理剖析与Python实践>介绍了深度学习相关的原理与应用,全书共分为三大部分,第一部分主要回顾了深度学习的发展历史,以及Theano的使用:第二部分详细讲解了与深度学习 ...
- 深入浅出深度学习:原理剖析与python实践_黄安埠(著) pdf
深入浅出深度学习:原理剖析与python实践 目录: 第1 部分 概要 1 1 绪论 2 1.1 人工智能.机器学习与深度学习的关系 3 1.1.1 人工智能——机器推理 4 1.1.2 机器学习—— ...
- QingStor 对象存储架构设计及最佳实践
对象存储概念及特性 在介绍 QingStor️对象存储内部的的架构和设计原理之前,我们首先来了解一下对象存储的概念,也就是从外部视角看,对象存储有什么特性,我们应该如何使用. 对象存储本质上是一款存储 ...
随机推荐
- 微信小程序-全局配置、组件、页面跳转、用户信息等
全局配置 三个页面 app.json pages字段 "pages":[ "pages/index/index", # 首页 "pages/home/ ...
- MySQL读写分离之——ProxySQL
文章转载自:https://blog.csdn.net/u012280685/article/details/113520692?spm=1001.2014.3001.5501 实现一个简单的读写分离 ...
- Mapping
dynamic针对的是新增的字段,不是对mapping中已有的字段 (原有mapping中的字段不受影响,只影响新增的字段) 当dynamic被设置成false的时候,存在新增字段可以被写入到索引文件 ...
- 堆Pwn:House Of Storm利用手法
0x00:介绍 利用手法的背景: house of storm是一种结合了unsorted bin attack和Largebin attack的攻击技术,其基本原理和Largebin attack类 ...
- NSIS安装界面无虚线框移动
最近很多应用程序都在安装界面的美化上面下足了功夫,一个漂亮流畅的安装界面无疑会给其带来用户体验上的加分,其中一个无虚线框跟随鼠标移动比较有趣,狂翻msdn后终于找到了控制函数SystemParamet ...
- 玖章算术受邀参加红杉Talk「创新的复利」科技专场,共同探讨云计算的前世今生
9月2日,本周五14:00 「创新的复利」 Sequoia Talk系列论坛,首期直播盛大启动.在第一期科技专场,4位红杉中国资深投资人.8位创新创业者将带我们深入工业软件.机器人.云计算等领域,围绕 ...
- C++面向对象编程之类模板、函数模板等一些补充
1.static数据 和 static函数: 对于 非static函数 在内存中只有一份,当类对象调用时,其实会有该对象的this pointer传进去,那个函数就知道要对那个对象进行操作: stat ...
- 超强的纯 CSS 鼠标点击拖拽效果
背景 鼠标拖拽元素移动,算是一个稍微有点点复杂的交互. 而在本文,我们就将打破常规,向大家介绍一种超强的仅仅使用纯 CSS 就能够实现的鼠标点击拖拽效果. 在之前的这篇文章中 -- 不可思议的纯 CS ...
- Docker Private Registry
Docker Private Registry 目录 Docker Private Registry Docker Registry Docker Private Registry 使用docker- ...
- 大数据技术之HBase原理与实战归纳分享-中
@ 目录 底层原理 Master架构 RegionServer架构 Region/Store/StoreFile/Hfile之间的关系 写流程 写缓存刷写 读流程 文件合并 分区 JAVA API编程 ...