序列化是我们在日常开发中经常会使用到的技术,比如需要将内存对象持久化存储、需要将对象通过网络传输到远端。目前市面上序列化框架非常多,开发团队在进行技术选型时通常难以抉择,甚至会踩坑。
今天选择几款市面上常用的序列化框架进行测试对比,帮助开发团队搞清楚不同场景该采用哪种序列化框架。
测试对比的框架有四款:
JDK原生、fastjson、Kryo、Protobuf
接下来会从以下这四个方面给出详细的测试对比结果:
(1)是否通用:是否支持跨语言、跨平台;
(2)是否容易使用:是否编译使用和调试;
(3)性能好不好:序列化性能主要包括时间开销和空间开销,时间开销是指序列化和反序列化对象所耗费的时间,空间开销是指序列化生成数据大小;
(4)可扩展强不强:随着业务发展,传输的业务对象可能会发生变化,比如说新增字段,这个时候就要看所选用的序列化框架是否有良好的扩展性;
框架1:JDK原生
是否通用?
JDK 原生是 Java 自带的序列化框架,与 Java 语言是强绑定的,通过 JDK 将对象序列化后是无法通过其他语言进行返序列化的,所以它的通用性比较差。
是否容易使用?
一个类实现了java.io.Serializable序列化接口就代表这个类的对象可以被序列化,否则就会报错。
简单认识一下Serializable这个类,通过看源码我们知道Serializable仅仅是一个空接口,没有定义任何方法。
public interface Serializable { }
这说明Serializable仅仅是一个标识的作用,用来告诉 JVM 这个对象可以被序列化。
想真正完成对象序列化和反序列化还得借助 IO 核心操作类:ObjectOutputStream和ObjectInputStream。
/** * 序列化 * * @param obj 待序列化对象 * @return 二进制字节数组 * @throws IOException */ public static byte[] serialize(Object obj) throws IOException { // 字节输出流 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 将对象序列化为二进制字节流 ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); // 获取二进制字节数组 byte[] bytes = byteArrayOutputStream.toByteArray(); // 关闭流 objectOutputStream.close(); byteArrayOutputStream.close(); return bytes; }
ObjectInputStream类的readObject()方法用于从 IO 流中读取对象,完成对象反序列化:
/** * 反序列化 * * @param bytes 待反序列化二进制字节数组 * @param反序列对象类型 * @return 反序列对象 * @throws IOException * @throws ClassNotFoundException */ public static <T> T deSerialize(byte[] bytes) throws IOException, ClassNotFoundException { // 字节输入流 final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); // 将二进制字节流反序列化为对象 final ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); final T object = (T) objectInputStream.readObject(); // 关闭流 objectInputStream.close(); byteArrayInputStream.close(); return object; }
从上面的代码可以看出,JDK 原生框架使用起来还是有点麻烦的,首先要求对象必须实现java.io.Serializable接口,其次需要借助 IO 流操作来完成序列化和反序列化。与市面上其他开源框架比起来,上面的代码写起来非常生硬。
一句话总结:JDK 原生框架易用性稍差。
性能好不好?
(1)序列化体积测试
为了方便测试对比,我定义了一个普通 java 类,后面其他框架的测试基本上也是用这个类:
public class UserDTO implements Serializable { private String name; private String wechatPub; private String job; …… }
将 UserDTO 类进行实例化:
UserDTO userDTO = new UserDTO(); userDTO.setName("雷小帅"); userDTO.setWechatPub("微信公众号:爱笑的架构师"); userDTO.setJob("优秀码农");
序列化和反序列化测试:
System.out.println("--- 1. jdk 原生测试 ---"); byte[] bytes = JDKSerializationUtil.serialize(userDTO); System.out.println("序列化成功:" + Arrays.toString(bytes)); System.out.println("byte size=" + bytes.length); UserDTO userDTO1 = JDKSerializationUtil.deSerialize(bytes); System.out.println("反序列化成功:" + userDTO1);
打印出来的结果:
--- 1. jdk 原生测试 --- 序列化成功:[-84, -19, 0, 5, 115, 114, 0, 39, …… byte size=182 反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农']
一个 UserDTO 序列化完之后是 182 个字节,待会对比其他框架就知道,这个水平太差了,Java 原生是自带的序列化工具,亲儿子也不给力啊。
(2)序列化速度测试
接下来我们再测试一下序列化和反序列化的速度,总共循环 100 万次:
- JDK 序列化耗时:2314 毫秒
- JDK 反序列化耗时:4170 毫秒
这个成绩怎么样,后面揭晓。
可扩展强不强?
JDK 原生序列化工具通过在类中定义 serialVersionUID 常量来控制版本:
private static final long serialVersionUID = 7982581299541067770L;
上面这个serialVersionUID是通过 IDEA 工具自动生成的长整形。其实你也可以不用声明这个值,JDK 会根据 hash 算法自动生成一个。
如果序列化的时候版本号是当前这个值,反序列化前你将值改变了,那么反序列化的时候就会报错,提示 ID 不一致。
假如需要在 UserDTO 这个类再加一个字段,那如何支持扩展呢?
你可以改变一下serialVersionUID值就可以了。
框架2:fastjson
是否通用?
fastjson 是阿里巴巴出品的一款序列化框架,可以将对象序列化为 JSON 字符串,类似的框架还有 jackson, gson 等。
由于 JSON 是与语言和平台无关,因此它的通用性还是很好的。
是否容易使用?
UserDTO 类不需要实现 Serializable 接口,也不需要加 serialVersionUID 版本号,使用起来非常简单。
将一个对象序列化为 json 字符串:
com.alibaba.fastjson.JSON.toJSONString(obj);
将 json 字符串反序列化为指定类型:
com.alibaba.fastjson.JSON.parseObject(jsonString, clazz);
另外 fastjson 框架还提供了很多注解,可以在 UserDTO 类进行配置,实现一些定制化的功能需求。
性能好不好?
(1)序列化体积测试
跟 JDK 原生框架一样,假设我们已经实例化好了一个UserDTO 对象,分别进行序列化和反序列化测试:
System.out.println("--- 2. fastjson 测试 ---"); String jsonString = FastjsonSerializationUtil.serialize(userDTO); System.out.println("序列化成功: " + jsonString); System.out.println("byte size=" + jsonString.length()); UserDTO userDTO2 = FastjsonSerializationUtil.deSerialize(jsonString, UserDTO.class); System.out.println("反序列化成功:" + userDTO2);
上面的代码是将序列化和反序列化代码封装到了一个工具类中。运行输出结果:
--- 2. fastjson 测试 --- 序列化成功: {"job":"优秀码农","name":"雷小帅","wechatPub":"微信公众号:爱笑的架构师"} byte size=54 反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农']
可以看到序列化之后有 54 个字节,而上面 JDK 原生框架是182 个字节,对比下来发现 fastjson 确实比 JDK 原生框架强了不少,亲儿子真不行。
(2)序列化速度测试
序列化体积测试完了之后,我们再测试一下序列化和反序列化速度,经过漫长的等待,循环跑了 100 万次之后实测结果如下:
- fastjson 序列化耗时:287 毫秒
- fastjson 反序列化耗时:365 毫秒
这个结果简直,人如其名啊,真快~ 你看看隔壁 JDK 原生框架的速度,惨不忍睹,哎……
可扩展强不强?
fastjson 没有版本控制机制,如果对类进行修改,比如新增熟悉字段,反序列时可以进行配置,忽略不认识的熟悉字段就可以正常进行反序列化。
所以说 fastjson 的扩展性还是很灵活的。
框架3:Kryo
是否通用?
Kryo 是一个快速高效的二进制序列化框架,号称是 Java 领域最快的。它的特点是序列化速度快、体积小、接口易使用。
Kryo支持自动深/浅拷贝,它是直接通过对象->对象的深度拷贝,而不是对象->字节->对象的过程。
关于 Kryo 更多的介绍可以去 Github 查看:
https://github.com/EsotericSoftware/kryo
关于通用性,Kryo 是一款针对 Java 语言开发的框架,基本很难跨语言使用,因此通用性比较差。
是否容易使用?
先引入 Kryo 依赖:
<dependency> <groupId>com.esotericsoftwaregroupId> <artifactId>kryoartifactId> <version>5.3.0version> dependency>
Kryo 提供的 API 非常简洁,Output 类封装了输出流操作,使用 writeObject 方法将对象写入 output 输出流程即可完成二进制序列化过程。
下面代码封装了一个简单的工具方法:
/** * 序列化 * * @param obj 待序列化对象 * @param kryo kryo 对象 * @return 字节数组 */ public static byte[] serialize(Object obj, Kryo kryo) { Output output = new Output(1024); kryo.writeObject(output, obj); output.flush(); return output.toBytes(); }
Kryo 反序列化也非常简单,Input 封装了输入流操作,通过 readObject 方法从输入流读取二进制反序列化成对象。
/** * 反序列化 * * @param bytes 待反序列化二进制字节数组 * @param反序列对象类型 * @return 反序列对象 */ public static <T> T deSerialize(byte[] bytes, Class<T> clazz, Kryo kryo) { Input input = new Input(bytes); return kryo.readObject(input, clazz); }
另外 Kryo 提供了丰富的配置项,可以在创建 Kryo 对象时进行配置。
总体而言,Kryo 使用起来还是非常简单的,接口易用性也是非常不错的。
性能好不好?
(1)序列化体积测试
Kryo 框架与其他框架不同,在实例化的时候可以选择提前注册类,这样序列化反序列化的速度会更快,当然也可以选择不注册。
System.out.println("--- 3. kryo 测试 ---"); Kryo kryo = new Kryo(); kryo.setRegistrationRequired(false); // kryo.register(UserDTO.class); byte[] kryoBytes = KryoSerializationUtil.serialize(userDTO, kryo); System.out.println("序列化成功:" + Arrays.toString(kryoBytes)); System.out.println("byte size=" + kryoBytes.length); UserDTO userDTO3 = KryoSerializationUtil.deSerialize(kryoBytes, UserDTO.class, kryo); System.out.println("反序列化成功:" + userDTO3);
运行结果:
序列化成功:[-123, -28, -68, -104, -25, ……] byte size=60 反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农']
从结果来看,序列化后总共是 60 字节。
(2)序列化速度测试
序列化体积测试完了之后,我们再测试一下序列化和反序列化速度,经过漫长的等待,循环跑了 100 万次之后实测结果如下:
- kryo 序列化耗时:295 毫秒
- kryo 反序列化耗时:211 毫秒
这个成绩还不错。
可扩展强不强?
Kryo默认序列化器 FiledSerializer 是不支持字段扩展的,如果想要使用扩展序列化器则需要配置其它默认序列化器。
框架4:Protobuf
是否通用?
Protobuf 是谷歌开源的一款二进制序列化框架。
Protobuf 要求先写schema描述文件,然后通过编译器编译成具体的编程语言(Java、C++、Go 等),因此它是一种语言中立、跨平台的框架,通用性非常好。
是否容易使用?
先编写 schema 文件,定义了一个 User 类,拥有三个属性字段:
syntax = "proto3"; option java_package = "com.example.demo2.serialization.protobuf"; message User { string name = 1; string wechatPub = 2; string job = 3; }
接着在电脑上安装好 Protobuf 编译工具,执行编译命令:
protoc --java_out=./ user-message.proto
编译成功后会生成一个 UserMessage 类。
UserMessage 类包含了很多内容:
首先有一个 Builder 内部类,可以用于实例化对象;
另外还提供了toByteArray(),可以很方便将对象序列化为二进制字节数组;提供了parseFrom()方法可以将对象反序列化为对象。
在接口使用上非常简单,开箱即用。
性能好不好?
(1)序列化体积测试
使用上面生成的UserMessage类创建一个对象,然后再进行序列化和反序列化测试:
System.out.println("--- 4. protobuf 测试 ---"); UserMessage.User user = UserMessage.User.newBuilder() .setName("雷小帅") .setWechatPub("微信公众号:爱笑的架构师") .setJob("优秀码农") .build(); final byte[] protoBufBytes = user.toByteArray(); System.out.println("序列化成功:" + Arrays.toString(protoBufBytes)); System.out.println("byte size=" + protoBufBytes.length); final UserMessage.User user1 = UserMessage.User.parseFrom(protoBufBytes); System.out.println("反序列化成功:" + user1);
运行结果:
序列化成功:[-123, -28, -68, -104, -25, ……] byte size=63 反序列化成功:UserDTO[name='雷小帅', wechatPub='微信公众号:爱笑的架构师', job='优秀码农']
序列化后是 63 字节,比 Kryo 稍微多一点点,有点吃惊。
(2)序列化速度测试
序列化体积测试完了之后,我们再测试一下序列化和反序列化速度,经过漫长的等待,循环跑了 100 万次之后实测结果如下:
- protobuf 序列化耗时:93 毫秒
- protobuf 反序列化耗时:341 毫秒
序列化速度很强,但是反序列化为什么慢这么多?
可扩展强不强?
可扩展性是 Protobuf 设计目标之一,我们可以很方便进行字段增删,新旧协议都可以进行解析。
总结:
本文对常用的框架进行了测试对比,通过观察 是否通用、是否容易使用、性能好不好、可扩展强不强 这四种维度,我们发现它们各有优劣,大家在进行技术选型时一定要慎重。
最后针对性能测试这一块,简单总结一下,给每种框架排个序。
(1)序列化体积
fastjson 54 bytes < Kryo 60 bytes < Protobuf 63 bytes < Java 原生 182 bytes
体积越小,传输效率越高,性能更优。Java 亲儿子真惨!
(2)序列化速度
protobuf 93 毫秒 < fastjson 289 毫秒 < kryo 295 毫秒 < Java 原生 2247 毫秒
Protobuf 真牛逼,王者!Java 亲儿子继续输~
(3)反序列化速度
kryo 211 毫秒 < protobuf 341 毫秒 < fastjson 396 毫秒 < Java 原生 4061 毫秒
Kryo 成绩比较稳定,序列化和反序列用时接近。Java 亲儿子输麻了!
原文地址:https://mp.weixin.qq.com/s/Z9ahavCHPxQRfBzjuVJwSg