前言
在日常开发中,对象的转换使用的很频繁,其中对象的转换大致可以分为浅拷贝和深拷贝两种,如果细分,对于深拷贝又可以细分为好几种。本文基于主流的几种深拷贝方式做了一系列测试,并探讨其中的原理和适用场景。
欢迎读者进行探讨,如有错漏之处,敬请指导,由于公众号新注册用户没有留言功能,想探讨的同学直接在公众号回复即可。
一、浅拷贝和深拷贝
浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝
深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
这里主要列出了以下几种方式:
浅拷贝:
1)使用Java 的clone方式,要求实现cloneable并且重写clone方法,原理是调用internalClone()
这个native方法,代码侵入性较大,但是性能优秀。
2)Apache的BeanUtil方式,公认的性能差,已经抛弃,这里不再测试。
3)Spring的BeanUtil方式,通过反射。
4)Hutool的BeanUtil方式,这个也是浅拷贝,性能也不错。
5)MapStruct方式,实际是生成get和set方法,性能优异,不过每次更新都需要deploy。
6)Apache的BeanCopier方式,与Hutool的BeanCopier的类似,使用了CGLIB动态代理,比反射性能优异,这里选取Apache的BeanCopier。
浅拷贝主要比较Java 的clone,Spring的BeanUtil,Hutool的BeanUtil,MapStruct,BeanCopier五种方案。
深拷贝:
1)用各种json工具进行序列化和反序列化,性能非常差,这里不再对比。
2)Kryo方式
3)Orika方式
4)Java IO进行序列化和反序列化
5)Protobuf方式,对复杂对象序列化和反序列化还存在问题,Hessian方式和Thrift方式也存在各种问题,这里不再对比。
6)Dozer方式
深拷贝主要比较Kryo,Orika,Java IO,Dozer四种方案。
二、具体场景
浅拷贝:在项目的分层结构中,经常需要BO,VO,DTO之间的转换,这种场景数量巨大,字段较多,因此对性能要求巨大。
深拷贝:在之前做过的一个进销库存项目中,
1)某个逻辑需要对数据库两个表(表A,表B,表A是具体的可用号码表,表B是可用号段表)进行更新操作,并且需要记录每次表操作的记录,前台需要查询历史操作记录。
2)需要对第一次的某些复杂对象的进行修改(因为需要用来映B表,两张表有大量的字段可以映射上,大约有30多个,但是又需要对其中某些复杂对象又需要修改,比如两张表的状态各代表不同含义,比如表的字段A对于表B只是一个子集,如表A中字段C为1,表2中记录为以逗号分隔)。
3)两个操作都完成后才会记录日志。
此时记录的日志数据是步骤1之后,步骤2之前的,但是真正执行记录是在步骤2才进行记录操作,所以需要用到深拷贝方案进行解决。
三、测试准备和配置介绍
1)测试配置:
系统:mac
CPU:i7 六核 2.6GHZ
内存:16G 2667MHZ DDR4
JDK: 1.8.0_241
2)测试准备
其中Orika代码如下,其他大同小异,分别进行1,1000,10w次的测试:
public class DeepCopyTest {
/**
* 循环的次数
*/
private final int LOOP = 1;
@Test
public void test(){
DeepCopyEntity demo = getInit();
StopWatch stopWatch = new StopWatch("test");
stopWatch.start("Orika");
for (int i = 0; i < LOOP; i++) {
final DeepCopyEntity deepCopyEntity =
MapperUtils.INSTANCE.map(DeepCopyEntity.class,
demo);
}
stopWatch.stop();
System.out.println(stopWatch.prettyPrint());
}
}
public enum MapperUtils {
/**
* 默认字段实例
*/
private static final MapperFacade MAPPER_FACADE =
MAPPER_FACTORY.getMapperFacade();
/**
* 映射实体(默认字段)
*
* @param toClass 映射类对象
* @param data 数据(对象)
* @return 映射类对象
*/
public <E, T> E map(Class<E> toClass, T data) {
return MAPPER_FACADE.map(data, toClass);
}
}
@Setter
@Getter
@Accessors(chain = true)
@ToString(callSuper = true)
public class DeepCopyEntity implements Serializable{
/**
* 序列化标识
*/
private static final long serialVersionUID =
6172279441386879379L;
private String id;
private String field1;
...
private Blind blind;
}
3)引用版本
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.2.0</version>
</dependency>
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>1.2.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.2.0.Final</version>
</dependency>
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.5.1</version>
</dependency>
四、测试结果
浅拷贝:
//次数: 1 1000 10w
--------------------------------------------------------------
Task name ns % ns % ns %
--------------------------------------------------------------
clone 000007177 000% 000272971 000% 011918713 001%
Hutool 045269308 017% 092633666 029% 819434606 058%
Spring 148264853 055% 150638012 048% 428660357 030%
MapStruct 002084618 001% 003604989 001% 016203277 001%
BeanCopier 073543500 027% 068328236 022% 135412730 010%
深拷贝:
//次数: 1 1000 10w
--------------------------------------------------------------
Task name ns % ns % ns %
--------------------------------------------------------------
jdk IO 010201657 002% 122777241 008% 4279557438 005%
Kryo 058354177 009% 049618874 003% 157321253 000%
Orika 344777022 056% 324837077 022% 562185127 001%
Dozer 201866716 033% 996948636 067% 75920114538 094%
总结
1)浅拷贝中MapStruct性能与jdk的clone基本一致,项目中推荐使用MapStruct,而且符合JSR 269规范,它还可以支持字段的自定义转换规则,忽略字段等。
2)深拷贝主要推荐使用Kryo,性能稳定,缺点是每个对象都需要注册。如:
/**
*
* @param source
* @return
*/
public DeepCopyEntity copyByKryo(DeepCopyEntity source){
kryo.register(DeepCopyEntity.class);
kryo.register(ArrayList.class);
kryo.register(Blind.class);
return kryo.copy(source);
}
3)MapStruct 使用get和set这种原始方式,是肯定比通过反射获取属性更优,缺点是每次更新mapper时需要deploy,Kryo 方式进行深拷贝,其性能接近RPC的Protobuf,跨平台能力相对也不错,需要注意的是,由于Kryo是线程不安全的,意味着每当需要序列化和反序列化时都需要实例化一次,或者借助ThreadLocal,或者使用Kryo 提供的pool来维护以保证其线程安全,如下所示:
private static final ThreadLocal<Kryo> kryoLocal =
new ThreadLocal<Kryo>() {
protected Kryo initialValue() {
Kryo kryo = new Kryo();
kryo.setInstantiatorStrategy(
new Kryo.DefaultInstantiatorStrategy(
new StdInstantiatorStrategy()));
return kryo;
};
};
public KryoPool newKryoPool() {
return new KryoPool.Builder(() -> {
final Kryo kryo = new Kryo();
kryo.setInstantiatorStrategy(
new Kryo.DefaultInstantiatorStrategy(
new StdInstantiatorStrategy()));
return kryo;
}).softReferences().build();
}
4)Kryo默认支持循环引用,如果不需要,可通过kryo.setReferences(false);关闭,关闭后性能会更优秀。
5)Kryo的源码太长,经过阅读,性能优异主要归结为两点:对long,int等数据类型,采用变长字节存储来代替java中使用固定字节(4,8)字节的模式,因为小值的字段更常见,通过这个方法可以更节省空间,这点与Protobuf 非常的相似,第二个举措是使用了类似缓存的机制,在一次序列化对象中,在整个递归序列化期间,相同的对象,只会序列化一次,后续的用一个局部int值来代替。