Hessian 序列化、反序列化
背景
問題和思考:
-
序列化參數有枚舉屬性,序列化端增加一個枚舉,能否正常反序列化?
-
序列化子類,它和父類有同名參數,反序列化時,同名參數能否能正常賦值?
-
序列化對象增加參數,反序列化類不增加參數,能否正常反序列化?
-
用於序列化傳輸的屬性,用包裝器比較好,還是基本類型比較好?
爲什麼要使用序列化和反序列化
-
程序在運行過程中,產生的數據,不能一直保存在內存中,需要暫時或永久存儲到介質(如磁盤、數據庫、文件)裏進行保存,也可能通過網絡發送給協作者。程序獲取原數據,需要從介質,或網絡傳輸獲得。傳輸的過程中,只能使用二進制流進行傳輸。
-
簡單的場景,基本類型數據傳輸。通過雙方約定好參數類型,數據接收方按照既定規則對二進制流進行反序列化。
- 複雜的場景,傳輸數據的參數類型可能包括:基本類型、包裝器類型、自定義類、枚舉、時間類型、字符串、容器等。很難簡單通過約定來反序列化二進制流。需要一個通用的協議,共雙方使用,進行序列化和反序列化。
三種序列化協議及對比
對比
Father father = new Father();
father.name = "廚師";
father.comment = "川菜館";
father.simpleInt = 1;
father.boxInt = new Integer(10);
father.simpleDouble = 1;
father.boxDouble = new Double(10);
father.bigDecimal = new BigDecimal(11.5);
運行結果:
jdk序列化結果長度:626,耗時:55
jdk反序列化結果:Father{version=0, name='廚師', comment='川菜館', boxInt=10, simpleInt=1, boxDouble=10.0, simpleDouble=1.0, bigDecimal=11.5}耗時:87
hessian序列化結果長度:182,耗時:56
hessian反序列化結果:Father{version=0, name='廚師', comment='川菜館', boxInt=10, simpleInt=1, boxDouble=10.0, simpleDouble=1.0, bigDecimal=11.5}耗時:7
Fastjson序列化結果長度:119,耗時:225
Fastjson反序列化結果:Father{version=0, name='廚師', comment='川菜館', boxInt=10, simpleInt=1, boxDouble=10.0, simpleDouble=1.0, bigDecimal=11.5}耗時:69
分析:
-
jdk 序列化耗時最短,但是序列化結果長度最長,是其它兩種的 3 ~ 5 倍。
-
fastjson 序列化結果長度最短,但是耗時是其它兩種的 4 倍左右。
-
hessian 序列化耗時與 jdk 差別不大,遠小於 fastjson 序列化耗時。且與 jdk 相比,序列化結果佔用空間非常有優勢。另外,hessian 的反序列化速度最快,耗時是其它兩種的 1/10。
-
綜上比較,hessian 在序列化和反序列化表現中,性能最優。
Hessian 序列化實戰
實驗準備
父類
public class Father implements Serializable {
/**
* 靜態類型不會被序列化
*/
private static final long serialVersionUID = 1L;
/**
* transient 不會被序列化
*/
transient int version = 0;
/**
* 名稱
*/
public String name;
/**
* 備註
*/
public String comment;
/**
* 包裝器類型1
*/
public Integer boxInt;
/**
* 基本類型1
*/
public int simpleInt;
/**
* 包裝器類型2
*/
public Double boxDouble;
/**
* 基本類型2
*/
public double simpleDouble;
/**
* BigDecimal
*/
public BigDecimal bigDecimal;
public Father() {
}
@Override
public String toString() {
return "Father{" +
"version=" + version +
", + name + '\'' +
", comment='" + comment + '\'' +
", boxInt=" + boxInt +
", simpleInt=" + simpleInt +
", boxDouble=" + boxDouble +
", simpleDouble=" + simpleDouble +
", bigDecimal=" + bigDecimal +
'}';
}
}
子類
public class Son extends Father {
/**
* 名稱,與father同名屬性
*/
public String name;
/**
* 自定義類
*/
public Attributes attributes;
/**
* 枚舉
*/
public Color color;
public Son() {
}
}
屬性 - 自定義類
public class Attributes implements Serializable {
private static final long serialVersionUID = 1L;
public int value;
public String msg;
public Attributes() {
}
public Attributes(int value, String msg) {
this.value = value;
this.msg = msg;
}
}
枚舉
public enum Color {
RED(1, "red"),
YELLOW(2, "yellow")
;
public int value;
public String msg;
Color() {
}
Color(int value, String msg) {
this.value = value;
this.msg = msg;
}
}
使用到的對象及屬性設置
Son son = new Son();
son.name = "廚師"; // 父子類同名字段,只給子類屬性賦值
son.comment = "川菜館";
son.simpleInt = 1;
son.boxInt = new Integer(10);
son.simpleDouble = 1;
son.boxDouble = new Double(10);
son.bigDecimal = new BigDecimal(11.5);
son.color = Color.RED;
son.attributes = new Attributes(11, "hello");
運行結果分析
使用 Hessian 序列化,結果寫入文件,使用 vim 打開。使用 16 進制方式查看,查看命令:%!xxd
00000000: 4307 6474 6f2e 536f 6e9a 046e 616d 6504 C.dto.Son..name.
00000010: 6e61 6d65 0763 6f6d 6d65 6e74 0662 6f78 name.comment.box
00000020: 496e 7409 7369 6d70 6c65 496e 7409 626f Int.simpleInt.bo
00000030: 7844 6f75 626c 650c 7369 6d70 6c65 446f xDouble.simpleDo
00000040: 7562 6c65 0a61 7474 7269 6275 7465 7305 uble.attributes.
00000050: 636f 6c6f 720a 6269 6744 6563 696d 616c color.bigDecimal
00000060: 6002 e58e a8e5 b888 4e03 e5b7 9de8 8f9c `.......N.......
00000070: e9a6 869a 915d 0a5c 430e 6474 6f2e 4174 .....].\C.dto.At
00000080: 7472 6962 7574 6573 9205 7661 6c75 6503 tributes..value.
00000090: 6d73 6761 9b05 6865 6c6c 6f43 0964 746f msga..helloC.dto
000000a0: 2e43 6f6c 6f72 9104 6e61 6d65 6203 5245 .Color..nameb.RE
000000b0: 4443 146a 6176 612e 6d61 7468 2e42 6967 DC.java.math.Big
000000c0: 4465 6369 6d61 6c91 0576 616c 7565 6304 Decimal..valuec.
000000d0: 3131 2e35 0a 11.5.
對其中的十六進制數逐個分析,可以拆解爲一下結構:參考 hessian 官方文檔,鏈接:http://hessian.caucho.com/doc/hessian-serialization.html
序列化原理
序列化規則:
- 被序列化的類必須實現了 Serializable 接口
- 靜態屬性和 transient 變量,不會被序列化。
-
枚舉類型在序列化後,存儲的是枚舉變量的名字
-
序列化結果的結構:類定義開始標識 C -> 類名長度 + 類名 -> 屬性數量 -> (逐個)屬性名長度 + 屬性名 -> 開始實例化標識 -> (按照屬性名順序,逐個設置)屬性值(發現某個屬性是一個對象,循環這個過程)
反序列化
通俗原理圖:
解釋:這是前邊的序列化文件,可以對着這個結構理解反序列化的過程。
解釋:讀取到 “C” 之後,它就知道接下來是一個類的定義,接着就開始讀取類名,屬性個數和每個屬性的名稱。並把這個類的定義緩存到一個_classDefs 的 list 裏。
解釋:通過讀取序列化文件,獲得類名後,會加載這個類,並生成這個類的反序列化器。這裏會生成一個_fieldMap,key 爲反序列化端這個類所有屬性的名稱,value 爲屬性對應的反序列化器。
解釋:讀到 6 打頭的 2 位十六進制數時,開始類的實例化和賦值。
遺留問題解答:
- 增加枚舉類型,反序列化不能正常讀取。
-
原因:枚舉類型序列化結果中,枚舉屬性對應的值是枚舉名。反序列化時,通過枚舉類類名 + 枚舉名反射生成枚舉對象。枚舉名找不到就會報錯。
-
反序列化爲子類型,同名屬性值無法正常賦值。
- 序列化對象增加參數,反序列化可以正常運行。
-
原因:反序列化時,是先通過類名加載同名類,並生成同名類的反序列化器,同名類每個屬性對應的反序列化器存儲在一個 map 中。在反序列化二進制文件時,通過讀取到的屬性名,到 map 中獲取對應的反序列化器。若獲取不到,默認是 NullFieldDeserializer.DESER。待到讀值的時候,僅讀值,不作 set 操作
-
序列化和反序列化雙方都使用對象類型時,更改屬性類型,若序列化方不傳輸數據,序列化結果是‘N’,能正常反序列化。但是對於一方是基本類型,更改屬性類型後,因爲 hessian 對於基本類型使用不同範圍的值域,所以無法正常序列化。
參考文檔:
-
https://zhuanlan.zhihu.com/p/44787200
-
https://paper.seebug.org/1131/
-
hessian 官方文檔:序列化規則 http://hessian.caucho.com/doc/hessian-serialization.html#anchor10
-
ASCII 編碼對照表 http://ascii.911cha.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/JGQuAln23dm90W9w7QwhDw