序列化是我們在日常開發(fā)中經(jīng)常會使用到的技術(shù),比如需要將內(nèi)存對象持久化存儲、需要將對象通過網(wǎng)絡(luò)傳輸?shù)竭h端。目前市面上序列化框架非常多,開發(fā)團隊在進行技術(shù)選型時通常難以抉擇,甚至會踩坑。
今天選擇幾款市面上常用的序列化框架進行測試對比,幫助開發(fā)團隊搞清楚不同場景該采用哪種序列化框架。
測試對比的框架有四款:
JDK原生、fastjson、Kryo、Protobuf
接下來會從以下這四個方面給出詳細的測試對比結(jié)果:
(1)是否通用:是否支持跨語言、跨平臺;
(2)是否容易使用:是否編譯使用和調(diào)試;
(3)性能好不好:序列化性能主要包括時間開銷和空間開銷,時間開銷是指序列化和反序列化對象所耗費的時間,空間開銷是指序列化生成數(shù)據(jù)大小;
(4)可擴展強不強:隨著業(yè)務(wù)發(fā)展,傳輸?shù)臉I(yè)務(wù)對象可能會發(fā)生變化,比如說新增字段,這個時候就要看所選用的序列化框架是否有良好的擴展性;
框架1:JDK原生
是否通用?
JDK 原生是 Java 自帶的序列化框架,與 Java 語言是強綁定的,通過 JDK 將對象序列化后是無法通過其他語言進行返序列化的,所以它的通用性比較差。
是否容易使用?
一個類實現(xiàn)了java.io.Serializable序列化接口就代表這個類的對象可以被序列化,否則就會報錯。
簡單認識一下Serializable這個類,通過看源碼我們知道Serializable僅僅是一個空接口,沒有定義任何方法。
public interface Serializable { }
這說明Serializable僅僅是一個標識的作用,用來告訴 JVM 這個對象可以被序列化。
想真正完成對象序列化和反序列化還得借助 IO 核心操作類:ObjectOutputStream和ObjectInputStream。
/** * 序列化 * * @param obj 待序列化對象 * @return 二進制字節(jié)數(shù)組 * @throws IOException */ public static byte[] serialize(Object obj) throws IOException { // 字節(jié)輸出流 ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 將對象序列化為二進制字節(jié)流 ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); objectOutputStream.writeObject(obj); // 獲取二進制字節(jié)數(shù)組 byte[] bytes = byteArrayOutputStream.toByteArray(); // 關(guān)閉流 objectOutputStream.close(); byteArrayOutputStream.close(); return bytes; }
ObjectInputStream類的readObject()方法用于從 IO 流中讀取對象,完成對象反序列化:
/** * 反序列化 * * @param bytes 待反序列化二進制字節(jié)數(shù)組 * @param反序列對象類型 * @return 反序列對象 * @throws IOException * @throws ClassNotFoundException */ public static <T> T deSerialize(byte[] bytes) throws IOException, ClassNotFoundException { // 字節(jié)輸入流 final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes); // 將二進制字節(jié)流反序列化為對象 final ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream); final T object = (T) objectInputStream.readObject(); // 關(guān)閉流 objectInputStream.close(); byteArrayInputStream.close(); return object; }
從上面的代碼可以看出,JDK 原生框架使用起來還是有點麻煩的,首先要求對象必須實現(xiàn)java.io.Serializable接口,其次需要借助 IO 流操作來完成序列化和反序列化。與市面上其他開源框架比起來,上面的代碼寫起來非常生硬。
一句話總結(jié):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("微信公眾號:愛笑的架構(gòu)師"); userDTO.setJob("優(yōu)秀碼農(nóng)");
序列化和反序列化測試:
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);
打印出來的結(jié)果:
--- 1. jdk 原生測試 --- 序列化成功:[-84, -19, 0, 5, 115, 114, 0, 39, …… byte size=182 反序列化成功:UserDTO[name='雷小帥', wechatPub='微信公眾號:愛笑的架構(gòu)師', job='優(yōu)秀碼農(nóng)']
一個 UserDTO 序列化完之后是 182 個字節(jié),待會對比其他框架就知道,這個水平太差了,Java 原生是自帶的序列化工具,親兒子也不給力啊。
(2)序列化速度測試
接下來我們再測試一下序列化和反序列化的速度,總共循環(huán) 100 萬次:
- JDK 序列化耗時:2314 毫秒
- JDK 反序列化耗時:4170 毫秒
這個成績怎么樣,后面揭曉。
可擴展強不強?
JDK 原生序列化工具通過在類中定義 serialVersionUID 常量來控制版本:
private static final long serialVersionUID = 7982581299541067770L;
上面這個serialVersionUID是通過 IDEA 工具自動生成的長整形。其實你也可以不用聲明這個值,JDK 會根據(jù) hash 算法自動生成一個。
如果序列化的時候版本號是當前這個值,反序列化前你將值改變了,那么反序列化的時候就會報錯,提示 ID 不一致。
假如需要在 UserDTO 這個類再加一個字段,那如何支持擴展呢?
你可以改變一下serialVersionUID值就可以了。
框架2:fastjson
是否通用?
fastjson 是阿里巴巴出品的一款序列化框架,可以將對象序列化為 JSON 字符串,類似的框架還有 jackson, gson 等。
由于 JSON 是與語言和平臺無關(guān),因此它的通用性還是很好的。
是否容易使用?
UserDTO 類不需要實現(xiàn) Serializable 接口,也不需要加 serialVersionUID 版本號,使用起來非常簡單。
將一個對象序列化為 json 字符串:
com.alibaba.fastjson.JSON.toJSONString(obj);
將 json 字符串反序列化為指定類型:
com.alibaba.fastjson.JSON.parseObject(jsonString, clazz);
另外 fastjson 框架還提供了很多注解,可以在 UserDTO 類進行配置,實現(xiàn)一些定制化的功能需求。
性能好不好?
(1)序列化體積測試
跟 JDK 原生框架一樣,假設(shè)我們已經(jīng)實例化好了一個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);
上面的代碼是將序列化和反序列化代碼封裝到了一個工具類中。運行輸出結(jié)果:
--- 2. fastjson 測試 --- 序列化成功: {"job":"優(yōu)秀碼農(nóng)","name":"雷小帥","wechatPub":"微信公眾號:愛笑的架構(gòu)師"} byte size=54 反序列化成功:UserDTO[name='雷小帥', wechatPub='微信公眾號:愛笑的架構(gòu)師', job='優(yōu)秀碼農(nóng)']
可以看到序列化之后有 54 個字節(jié),而上面 JDK 原生框架是182 個字節(jié),對比下來發(fā)現(xiàn) fastjson 確實比 JDK 原生框架強了不少,親兒子真不行。
(2)序列化速度測試
序列化體積測試完了之后,我們再測試一下序列化和反序列化速度,經(jīng)過漫長的等待,循環(huán)跑了 100 萬次之后實測結(jié)果如下:
- fastjson 序列化耗時:287 毫秒
- fastjson 反序列化耗時:365 毫秒
這個結(jié)果簡直,人如其名啊,真快~ 你看看隔壁 JDK 原生框架的速度,慘不忍睹,哎……
可擴展強不強?
fastjson 沒有版本控制機制,如果對類進行修改,比如新增熟悉字段,反序列時可以進行配置,忽略不認識的熟悉字段就可以正常進行反序列化。
所以說 fastjson 的擴展性還是很靈活的。
框架3:Kryo
是否通用?
Kryo 是一個快速高效的二進制序列化框架,號稱是 Java 領(lǐng)域最快的。它的特點是序列化速度快、體積小、接口易使用。
Kryo支持自動深/淺拷貝,它是直接通過對象->對象的深度拷貝,而不是對象->字節(jié)->對象的過程。
關(guān)于 Kryo 更多的介紹可以去 Github 查看:
https://github.com/EsotericSoftware/kryo
關(guān)于通用性,Kryo 是一款針對 Java 語言開發(fā)的框架,基本很難跨語言使用,因此通用性比較差。
是否容易使用?
先引入 Kryo 依賴:
<dependency> <groupId>com.esotericsoftwaregroupId> <artifactId>kryoartifactId> <version>5.3.0version> dependency>
Kryo 提供的 API 非常簡潔,Output 類封裝了輸出流操作,使用 writeObject 方法將對象寫入 output 輸出流程即可完成二進制序列化過程。
下面代碼封裝了一個簡單的工具方法:
/** * 序列化 * * @param obj 待序列化對象 * @param kryo kryo 對象 * @return 字節(jié)數(shù)組 */ 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 待反序列化二進制字節(jié)數(shù)組 * @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 提供了豐富的配置項,可以在創(chuàng)建 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);
運行結(jié)果:
序列化成功:[-123, -28, -68, -104, -25, ……] byte size=60 反序列化成功:UserDTO[name='雷小帥', wechatPub='微信公眾號:愛笑的架構(gòu)師', job='優(yōu)秀碼農(nóng)']
從結(jié)果來看,序列化后總共是 60 字節(jié)。
(2)序列化速度測試
序列化體積測試完了之后,我們再測試一下序列化和反序列化速度,經(jīng)過漫長的等待,循環(huán)跑了 100 萬次之后實測結(jié)果如下:
- 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 編譯工具,執(zhí)行編譯命令:
protoc --java_out=./ user-message.proto
編譯成功后會生成一個 UserMessage 類。
UserMessage 類包含了很多內(nèi)容:
首先有一個 Builder 內(nèi)部類,可以用于實例化對象;
另外還提供了toByteArray(),可以很方便將對象序列化為二進制字節(jié)數(shù)組;提供了parseFrom()方法可以將對象反序列化為對象。
在接口使用上非常簡單,開箱即用。
性能好不好?
(1)序列化體積測試
使用上面生成的UserMessage類創(chuàng)建一個對象,然后再進行序列化和反序列化測試:
System.out.println("--- 4. protobuf 測試 ---"); UserMessage.User user = UserMessage.User.newBuilder() .setName("雷小帥") .setWechatPub("微信公眾號:愛笑的架構(gòu)師") .setJob("優(yōu)秀碼農(nóng)") .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);
運行結(jié)果:
序列化成功:[-123, -28, -68, -104, -25, ……] byte size=63 反序列化成功:UserDTO[name='雷小帥', wechatPub='微信公眾號:愛笑的架構(gòu)師', job='優(yōu)秀碼農(nóng)']
序列化后是 63 字節(jié),比 Kryo 稍微多一點點,有點吃驚。
(2)序列化速度測試
序列化體積測試完了之后,我們再測試一下序列化和反序列化速度,經(jīng)過漫長的等待,循環(huán)跑了 100 萬次之后實測結(jié)果如下:
- protobuf 序列化耗時:93 毫秒
- protobuf 反序列化耗時:341 毫秒
序列化速度很強,但是反序列化為什么慢這么多?
可擴展強不強?
可擴展性是 Protobuf 設(shè)計目標之一,我們可以很方便進行字段增刪,新舊協(xié)議都可以進行解析。
總結(jié):
本文對常用的框架進行了測試對比,通過觀察 是否通用、是否容易使用、性能好不好、可擴展強不強 這四種維度,我們發(fā)現(xiàn)它們各有優(yōu)劣,大家在進行技術(shù)選型時一定要慎重。
最后針對性能測試這一塊,簡單總結(jié)一下,給每種框架排個序。
(1)序列化體積
fastjson 54 bytes < Kryo 60 bytes < Protobuf 63 bytes < Java 原生 182 bytes
體積越小,傳輸效率越高,性能更優(yōu)。Java 親兒子真慘!
(2)序列化速度
protobuf 93 毫秒 < fastjson 289 毫秒 < kryo 295 毫秒 < Java 原生 2247 毫秒
Protobuf 真牛逼,王者!Java 親兒子繼續(xù)輸~
(3)反序列化速度
kryo 211 毫秒 < protobuf 341 毫秒 < fastjson 396 毫秒 < Java 原生 4061 毫秒
Kryo 成績比較穩(wěn)定,序列化和反序列用時接近。Java 親兒子輸麻了!
原文地址:https://mp.weixin.qq.com/s/Z9ahavCHPxQRfBzjuVJwSg