赞
踩
序列化是我们在日常开发中经常会使用到的技术,比如需要将内存对象持久化存储、需要将对象通过网络传输到远端。目前市面上序列化框架非常多,开发团队在进行技术选型时通常难以抉择,甚至会踩坑。
今天选择几款市面上常用的序列化框架进行测试对比,帮助开发团队搞清楚不同场景该采用哪种序列化框架。
测试对比的框架有四款:
JDK原生、fastjson、Kryo、Protobuf
接下来会从以下这四个方面给出详细的测试对比结果:
(1)是否通用:是否支持跨语言、跨平台;
(2)是否容易使用:是否编译使用和调试;
(3)性能好不好:序列化性能主要包括时间开销和空间开销,时间开销是指序列化和反序列化对象所耗费的时间,空间开销是指序列化生成数据大小;
(3)可扩展强不强:随着业务发展,传输的业务对象可能会发生变化,比如说新增字段,这个时候就要看所选用的序列化框架是否有良好的扩展性;
JDK 原生是 Java 自带的序列化框架,与 Java 语言是强绑定的,通过 JDK 将对象序列化后是无法通过其他语言进行返序列化的,所以它的通用性比较差。
一个类实现了java.io.Serializable
序列化接口就代表这个类的对象可以被序列化,否则就会报错。
简单认识一下Serializable
这个类,通过看源码我们知道Serializable
仅仅是一个空接口,没有定义任何方法。
- public interface Serializable {
- }
这说明Serializable
仅仅是一个标识的作用,用来告诉 JVM 这个对象可以被序列化。
想真正完成对象序列化和反序列化还得借助 IO 核心操作类:ObjectOutputStream
和ObjectInputStream
。
ObjectOutputStream
类的writeObject()
方法用于将对象写入 IO 流,完成对象序列化:
- /**
- * 序列化
- *
- * @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 <T> 反序列对象类型
- * @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值就可以了。
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 的扩展性还是很灵活的。
Kryo 是一个快速高效的二进制序列化框架,号称是 Java 领域最快的。它的特点是序列化速度快、体积小、接口易使用。
Kryo支持自动深/浅拷贝,它是直接通过对象->对象的深度拷贝,而不是对象->字节->对象的过程。
关于 Kryo 更多的介绍可以去 Github 查看:
https://github.com/EsotericSoftware/kryo
关于通用性,Kryo 是一款针对 Java 语言开发的框架,基本很难跨语言使用,因此通用性比较差。
先引入 Kryo 依赖:
- <dependency>
- <groupId>com.esotericsoftware</groupId>
- <artifactId>kryo</artifactId>
- <version>5.3.0</version>
- </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 <T> 反序列对象类型
- * @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 是不支持字段扩展的,如果想要使用扩展序列化器则需要配置其它默认序列化器。
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 亲儿子输麻了!
(需要说明一下,这些测试数据是在我个人电脑上跑的,可能不够准确,仅供参考)
推荐:
PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。点“在看”支持我们吧!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。