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) {
}
}
}// 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];
}
}
}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
使用方式: BeanUtil