几种Java常用序列化框架的选型与对比
通用性:通用性是指序列化框架是否支持跨语言、跨平台。
易用性:易用性是指序列化框架是否便于使用、调试,会影响开发效率。
可扩展性:随着业务的发展,传输实体可能会发生变化,但是旧实体有可能还会被使用。这时候就需要考虑所选择的序列化框架是否具有良好的扩展性。
性能:序列化性能主要包括时间开销和空间开销。序列化的数据通常用于持久化或网络传输,所以其大小是一个重要的指标。而编解码时间同样是影响序列化协议选择的重要指标,因为如今的系统都在追求高性能。
Java数据类型和语法支持:不同序列化框架所能够支持的数据类型以及语法结构是不同的。这里我们要对Java的数据类型和语法特性进行测试,来看看不同序列化框架对Java数据类型和语法结构的支持度。
/*** 编码*/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> 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.io.InvalidClassException: com.yjz.serialization.java.UserInfo; local class incompatible: stream classdesc serialVersionUID = -5548195544707231683, local class serialVersionUID = -5194320341014913710
private static final long serialVersionUID = 1L;
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;}}
| 1000万序列化耗时(ms) | 1000万反序列化耗时(ms) |
| 38952 | 96508 |
| JDK | |
| 8种基础类型 | 支持 |
| List集合类 | 支持 |
| Set集合类 | 支持 |
| Queue集合类 | 支持 |
| Map映射 | 大部分支持(WeakHashMap不支持) |
| 自定义类类型 | 支持 |
| 枚举类型 | 支持 |
| JDK | |
| 对象为null | 支持 |
| 没有无参构造函数 | 支持 |
| static内部类 | 支持(static内部类需要实现序列化接口) |
| 非static内部类 | 支持,但是外部类也需要实现序列化接口 |
| 局部内部类 | 支持 |
| 匿名内部类 | 支持 |
| Lambda表达式 | 修改代码可以支持,看注1 |
| 闭包 | 支持 |
| 异常类 | 支持 |
Runnable runnable = () -> System.out.println("Hello");
com.yjz.serialization.SerializerFunctionTest$$Lambda$1/189568618
Runnable runnable = (Runnable & Serializable) () -> System.out.println("Hello");
private final ThreadLocal<FSTConfiguration> conf = ThreadLocal.withInitial(() -> {FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();return conf;});public byte[] encoder(Object object) {return conf.get().asByteArray(object);}public <T> T decoder(byte[] bytes) {Object ob = conf.get().asObject(bytes);return (T)ob;}
private String origiField;(1)private String addField;
删除字段将破坏向后兼容性,但是如果我们在原始字段情况下删除字段是能够向后兼容的(没有新增任何字段)。但是如果新增字段后,再删除字段的话就会破坏其兼容性。
Version注解功能不能应用于自己实现的readObject/writeObject情况。
如果自己实现了Serializer,需要自己控制Version。
| 1000万序列化耗时(ms) | 1000万反序列化耗时(ms) |
| 13587 | 19031 |
private static final ThreadLocal<FSTConfiguration> conf = ThreadLocal.withInitial(() -> {FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();conf.registerClass(UserInfo.class);conf.setShareReferences(false);return conf;});
| 1000万序列化耗时(ms) | 1000万反序列化耗时(ms) |
| 7609 | 17792 |
| FST | |
| 8种基础类型 | 支持 |
| List集合类 | 支持 |
| Set集合类 | 支持 |
| Queue集合类 | 支持 |
| Map映射 | 大部门支持(WeakHashMap不支持) |
| 自定义类类型 | 支持 |
| 枚举类型 | 支持 |
| FST | |
| 对象为null | 支持 |
| 没有无参构造函数 | 支持 |
| static内部类 | 支持(static内部类需要实现序列化接口) |
| 非static内部类 | 支持,但是外部类也需要实现序列化接口 |
| 局部内部类 | 支持 |
| 匿名内部类 | 支持 |
| Lambda表达式 | 修改代码可以支持(同JDK) |
| 闭包 | 支持 |
| 异常类 | 支持 |
private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {Kryo kryo = new Kryo();kryo.setRegistrationRequired(false);//不需要提前预注册类return kryo;});public static byte[] encoder(Object object) {Output output = new Output();kryoLocal.get().writeObject(output,object);output.flush();return output.toBytes();}public static <T> T decoder(byte[] bytes) {Input input = new Input(bytes);Object ob = kryoLocal.get().readClassAndObject(input);return (T) ob;}
需要注意的是使用Output.writeXxx时候一定要用对应的Input.readxxx,比如Output.writeClassAndObject()要与Input.readClassAndObject()。
private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {Kryo kryo = new Kryo();kryo.setRegistrationRequired(false);kryo.setDefaultSerializer(TaggedFieldSerializer.class);return kryo;});
| 1000万序列化时间开销(ms) | 1000万反序列化时间开销(ms) |
| 13550 | 14315 |
| 1000万序列化时间开销(ms) | 1000万反序列化时间开销(ms) |
| 11799 | 11584 |
| Kryo | |
| 8种基础类型 | 支持 |
| List集合类 | 支持 |
| Set集合类 | 支持 |
| Queue集合类 | 部分支持(ArrayBlockingQueue不支持) |
| Map映射 | 支持 |
| 自定义类类型 | 支持 |
| 枚举类型 | 支持 |
| Kryo | |
| 对象为null | 支持 |
| 没有无参构造函数 | 不支持 |
| static内部类 | 支持 |
| 非static内部类 | 不支持 |
| 局部内部类 | 支持 |
| 匿名内部类 | 支持 |
| Lambda表达式 | 不支持 |
| 闭包 | 支持 |
| 异常类 | 不支持(StackOverflowError) |
syntax = "proto3";option java_package = "com.yjz.serialization.protobuf3";message MessageInfo{string username = 1;string password = 2;int32 age = 3;map<string,string> params = 4;}
protoc --java_out=./src/main/java message.proto
//编码byte[] bytes = MessageInfo.toByteArray()//解码MessageInfo messageInfo = Message.MessageInfo.parseFrom(bytes);
message userinfo{reserved 3,7; //在保留标签中,添加删除的字段标签reserved "age","sex" //在保留字段中,添加删除的字段}
| 1000万数据序列化耗时(ms) | 1000万数据反序列化耗时(ms) |
| 14235 | 30694 |
| Protobuf | |
| 8种基础类型 | 基本支持(无byte、shot、char) |
| List集合类 | 支持 |
| Set集合类 | 支持 |
| Queue集合类 | 支持 |
| Map映射 | 支持 |
| 自定义类类型 | 支持 |
| 枚举类型 | 支持 |
namespace java com.yjz.serialization.thriftstruct MessageInfo{1: string username;2: string password;3: i32 age;4: map<string,string> params;}
thrift --gen java message.thrift
public static byte[] encoder(MessageInfo messageInfo) throws Exception{TSerializer serializer = new TSerializer();return serializer.serialize(messageInfo);}public static MessageInfo decoder(byte[] bytes) throws Exception{TDeserializer deserializer = new TDeserializer();MessageInfo messageInfo = new MessageInfo();deserializer.deserialize(messageInfo,bytes);return messageInfo;}
修改字段名称:修改字段名称不影响序列化与反序列化,反序列化数据赋值到更新过的字段上。因为编解码过程利用的是编号对应。
修改字段类型:修改字段类型,如果修改的字段为optional类型字段,则返回数据为null或0(数据类型默认值)。如果修改是required类型字段,则会直接抛出异常,提示字段没有找到。
新增字段:如果新增字段是required类型,则需要为其设置默认值,负责在反序列化过程抛出异常。如果为optional类型字段,反序列化过程不会存在该字段(因为optional字段没有赋值的情况,不会参与序列化与反序列化)。如果为缺省类型,则反序列化值为null或0(和数据类型有关)。
删除字段:无论required类型字段还是optional类型字段,都可以删除,不会影响反序列化。
删除后的字段整数标签不要复用,负责会影响反序列化。
| 1000万序列化时间开销(ms) | 1000万反序列化时间开销(ms) |
| 28634 | 20722 |
8中基础数据类型,没有short、char,只能使用double和String代替。
集合类型,支持List、Set、Map,不支持Queue。
自定义类类型(struct类型)。
枚举类型。
字节数组。
public static <T> byte[] encoder2(T obj) throws Exception{ByteArrayOutputStream bos = new ByteArrayOutputStream();Hessian2Output hessian2Output = new Hessian2Output(bos);hessian2Output.writeObject(obj);return bos.toByteArray();}public static <T> T decoder2(byte[] bytes) throws Exception {ByteArrayInputStream bis = new ByteArrayInputStream(bytes);Hessian2Input hessian2Input = new Hessian2Input(bis);Object obj = hessian2Input.readObject();return (T) obj;}
修改字段名称:反序列化后新字段名称为null或0(受类型影响)。
新增字段:反序列化后新增字段为null或0(受类型影响)。
删除字段:能够正常反序列化。
修改字段类型:如果字段类型兼容能够正常反序列化,如果不兼容则直接抛出异常。
| 1000万序列化时间开销(ms) | 1000万反序列化时间开销(ms) | |
| Hessian1.0 | 57648 | 55261 |
| Hessian2.0 | 38823 | 17682 |
{"namespace": "com.yjz.serialization.avro","type": "record","name": "MessageInfo","fields": [{"name": "username","type": "string"},{"name": "password","type": "string"},{"name": "age","type": "int"},{"name": "params","type": {"type": "map","values": "string"}}]}
java -jar avro-tools-1.8.2.jar compile schema src/main/resources/avro/Message.avsc ./src/main/java
public static byte[] encoder(MessageInfo obj) throws Exception{DatumWriter<MessageInfo> datumWriter = new SpecificDatumWriter<>(MessageInfo.class);ByteArrayOutputStream outputStream = new ByteArrayOutputStream();BinaryEncoder binaryEncoder = EncoderFactory.get().directBinaryEncoder(outputStream,null);datumWriter.write(obj,binaryEncoder);return outputStream.toByteArray();}public static MessageInfo decoder(byte[] bytes) throws Exception{DatumReader<MessageInfo> datumReader = new SpecificDatumReader<>(MessageInfo.class);BinaryDecoder binaryDecoder = DecoderFactory.get().directBinaryDecoder(new ByteArrayInputStream(bytes),null);return datumReader.read(new MessageInfo(),binaryDecoder);}
给所有field定义default值。如果某field没有default值,以后将不能删除该field。
如果要新增field,必须定义default值。
不能修改field type。
不能修改field name,不过可以通过增加alias解决。
| 1000万序列化时间开销(ms) | 1000万序反列化时间开销(ms) | |
| 生成Java代码 | 26565 | 45383 |
| 序列化框架 | 通用性 |
| JDK Serializer | 只适用于Java |
| FST | 只适用于Java |
| Kryo | 主要适用于Java(可复杂支持跨语言) |
| Protocol buffer | 支持多种语言 |
| Thrift | 支持多种语言 |
| Hessian | 支持多种语言 |
| Avro | 支持多种语言 |
| 序列化框架 | 易用性 |
| JDK Serializer | 使用语法过于生硬 |
| FST | 使用简洁,FSTConfiguration提供了序列化与反序列化的方法 |
| Kryo | 使用简洁,Input/Output封装了几乎所有能有需要的流方法 |
| Protocol buffer | 稍微复杂。需要编写所需序列化类的proto文件,然后编译生成Java代码。但是自动生成Java类,包含了序列化与反序列化方法 |
| Thrift | 稍微复杂。需要编写所需的序列化类的thrift文件,然后编译生成Java代码。然后通过TSerializer和TDserializer进行序列化与反序列化 |
| Hessian | 使用简单,在跨语言的基础上不需要使用IDL |
| Avro | 使用较复杂。相较于Protobuf和Thrift来说,对于一些静态语言无序生成代码。但是对于Java来一般还需要生成代码,并且Avro提供的API不是很友好 |
| 序列化框架 | 可扩展性 |
| JDK Serializer | 自定义serialVersionUID,保证序列化前后VUID一致即可 |
| FST | 通过@Version控制版本,新增字段需要修改Version版本 |
| Kryo | 默认序列化器不支持字段扩展,需要修改默认序列化器或自己实现序列化器 |
| Protocol buffer | 支持字段扩展,只要保证新增id标识没有使用过即可 |
| Thrift | 支持字段扩展。新增字段为required类型时,需要设置默认值 |
| Hessian | 支持字段扩展 |
| Avro | 支持字段扩展。注意需要为字段设置默认值 |
List测试内容:ArrayList、LinkedList、Stack、CopyOnWriteArrayList、Vector。
Set测试内容:HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet。
Map测试内容:HashMap、LinkedHashMap、TreeMap、WeakHashMap、ConcurrentHashMap、Hashtable。
Queue测试内容:PriorityQueue、ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue、SynchronousQueue、ArrayDeque、LinkedBlockingDeque和ConcurrentLinkedDeque。
注1:static内部类需要实现序列化接口。
注2:外部类需要实现序列化接口。
注3:需要在Lambda表达式前添加(IXxx & Serializable)。
由于Protobuf、Thrift是IDL定义类文件,然后使用各自的编译器生成Java代码。IDL没有提供定义staic内部类、非static内部类等语法,所以这些功能无法测试。
阿里云开发者社区
世界读书日,来读书吧
4月23日是第26个世界读书日,阿里云开发者社区推出“记录阅读之路,影响同行之人”活动,6位阿里技术人为同学们分享他们看过的好书,开发者藏经阁也推出了最受大家欢迎的电子书。
点击“阅读原文”,推荐曾经影响你的书,来一起读书吧~
