一 背景介绍序列化与反序列化是我们日常数据持久化和网络传输中经常使用的技术,但是目前各种序列化框架让人眼花缭乱,不清楚什么场景到底采用哪种序列化框架。本文会将业界开源的序列化框架进行对比测试,分别从通用性、易用性、可扩展性、性能和数据类型与Java语法支持五方面给出对比测试。
-
通用性:通用性是指序列化框架是否支持跨语言、跨平台。
-
易用性:易用性是指序列化框架是否便于使用、调试,会影响开发效率。
-
可扩展性:随着业务的发展,传输实体可能会发生变化,但是旧实体有可能还会被使用。这时候就需要考虑所选择的序列化框架是否具有良好的扩展性。
-
性能:序列化性能主要包括时间开销和空间开销。序列化的数据通常用于持久化或网络传输,所以其大小是一个重要的指标。而编解码时间同样是影响序列化协议选择的重要指标,因为如今的系统都在追求高性能。
-
Java数据类型和语法支持:不同序列化框架所能够支持的数据类型以及语法结构是不同的。这里我们要对Java的数据类型和语法特性进行测试,来看看不同序列化框架对Java数据类型和语法结构的支持度。对于新手小白想更轻松的学好Java提升,Java架构,web开发、大数据,数据分析,人工智能等技术,这里给大家分享系统教学资源,扩列下我尉(同英):CGMX9880 【教程/工具/方法/解疑】
下面分别对JDK Serializable、FST、Kryo、Protobuf、Thrift、Hession和Avro进行对比测试。二 序列化框架1 JDK SerializableJDK Serializable是Java自带的序列化框架,我们只需要实现java.io.Serializable或java.io.Externalizable接口,就可以使用Java自带的序列化机制。实现序列化接口只是表示该类能够被序列化/反序列化,我们还需要借助I/O操作的ObjectInputStream和ObjectOutputStream对对象进行序列化和反序列化。下面是使用JDK 序列化框架进行编解码的Demo:
/**
- 编码
/
public static byte[] encoder(Object ob) throws Exception{
//用于缓冲字节数字
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
//序列化对象
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(ob);
byte[] result = byteArrayOutputStream.toByteArray();
//关闭流
objectOutputStream.close();
byteArrayOutputStream.close();
return result;
}
/* - 解码
*/
public static T decoder(byte[] bytes) throws Exception {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
T object = (T) objectInputStream.readObject();
objectInputStream.close();
byteArrayInputStream.close();
return object;
}
通用性
由于是Java内置序列化框架,所以本身是不支持跨语言序列化与反序列化。易用性作为Java内置序列化框架,无序引用任何外部依赖即可完成序列化任务。但是JDK Serializable在使用上相比开源框架难用许多,可以看到上面的编解码使用非常生硬,需要借助ByteArrayOutputStream和ByteArrayInputStream才可以完整字节的转换。可扩展性JDK Serializable中通过serialVersionUID控制序列化类的版本,如果序列化与反序列化版本不一致,则会抛出java.io.InvalidClassException异常信息,提示序列化与反序列化SUID不一致。
java.io.InvalidClassException: com.yjz.serialization.java.UserInfo; local class incompatible: stream classdesc serialVersionUID = -5548195544707231683, local class serialVersionUID = -5194320341014913710
上面这种情况,是由于我们没有定义serialVersionUID,而是由JDK自动hash生成的,所以序列化与反序列化前后结果不一致。但是我们可以通过自定义serialVersionUID方式来规避掉这种情况(序列化前后都是使用定义的serialVersionUID),这样JDK Serializable就可以支持字段扩展了。
private static final long serialVersionUID = 1L;
性能JDK Serializable是Java自带的序列化框架,但是在性能上其实一点不像亲生的。下面测试用例是我们贯穿全文的一个测试实体。
public class MessageInfo implements Serializable {
private String username;
private String password;
private int age;
private HashMap<String,Object> params;
...
public static MessageInfo buildMessage() {
MessageInfo messageInfo = new MessageInfo();
messageInfo.setUsername("abcdefg");
messageInfo.setPassword("123456789");
messageInfo.setAge(27);
Map<String,Object> map = new HashMap<>();
for(int i = 0; i< 20; i++) {
map.put(String.valueOf(i),"a");
}
return messageInfo;
}
}
使用JDK序列化后字节大小为:432。光看这组数字也许不会感觉到什么,之后我们会拿这个数据和其它序列化框架进行对比。对于新手小白想更轻松的学好Java提升,Java架构,web开发、大数据,数据分析,人工智能等技术,这里给大家分享系统教学资源,扩列下我尉(同英):CGMX9880 【教程/工具/方法/解疑】
我们对该测试用例进行1000万次序列化,然后计算时间总和:
同样我们之后会同其它序列化框架进行对比。
数据类型和语法结构支持性
由于JDK Serializable是Java语法原生序列化框架,所以基本都能够支持Java数据类型和语法。
WeakHashMap没有实现Serializable接口。
注1:但我们要序列化下面代码:
Runnable runnable = () -> System.out.println(“Hello”);
直接序列化会得到以下异常:
com.yjz.serialization.SerializerFunctionTest$$Lambda$1/189568618
原因就是我们Runnable的Lambda并没有实现Serializable接口。我们可以做如下修改,即可支持Lambda表达式序列化。
Runnable runnable = (Runnable & Serializable) () -> System.out.println(“Hello”);
2 FST序列化框架
FST(fast-serialization)是完全兼容JDK序列化协议的Java序列化框架,它在序列化速度上能达到JDK的10倍,序列化结果只有JDK的1/3。目前FST的版本为2.56,在2.17版本之后提供了对Android的支持。
下面是使用FST序列化的Demo,FSTConfiguration是线程安全的,但是为了防止频繁调用时其成为性能瓶颈,一般会使用TreadLocal为每个线程分配一个FSTConfiguration。
private final ThreadLocal conf = ThreadLocal.withInitial(() -> {
FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();
return conf;
});
public byte[] encoder(Object object) {
return conf.get().asByteArray(object);
}
public T decoder(byte[] bytes) {
Object ob = conf.get().asObject(bytes);
return (T)ob;
}
通用性
FST同样是针对Java而开发的序列化框架,所以也不存在跨语言特性。
易用性
在易用性上,FST可以说能够甩JDK Serializable几条街,语法极其简洁,FSTConfiguration封装了大部分方法。
可扩展性
FST通过@Version注解能够支持新增字段与旧的数据流兼容。对于新增的字段都需要通过@Version注解标识,没有版本注释意味着版本为0。
private String origiField;
@Version(1)
private String addField;
注意:
删除字段将破坏向后兼容性,但是如果我们在原始字段情况下删除字段是能够向后兼容的(没有新增任何字段)。但是如果新增字段后,再删除字段的话就会破坏其兼容性。
Version注解功能不能应用于自己实现的readObject/writeObject情况。
如果自己实现了Serializer,需要自己控制Version。
综合来看,FST在扩展性上面虽然支持,但是用起来还是比较繁琐的。
性能
使用FST序列化上面的测试用例,序列化后大小为:172,相比JDK序列化的432 ,将近减少了1/3。下面我们再看序列化与反序列化的时间开销。
我们可以优化一下FST,将循环引用判断关闭,并且对序列化类进行余注册。
private static final ThreadLocal conf = ThreadLocal.withInitial(() -> {
FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();
conf.registerClass(UserInfo.class);
conf.setShareReferences(false);
return conf;
});
通过上面的优化配置,得到的时间开销如下:
可以看到序列化时间将近提升了2倍,但是通过优化后的序列化数据大小增长到了191 。