0基礎帶你精通Java對象序列化--以Hessian為例
一、概述
二、基礎編碼原理
1. 對象圖遍歷
2. 編碼格式
三、Hessian編碼格式
1. 數據塊
2. 數據塊標簽(tag)
3. POJO編碼
四、Hessian編碼細節
1. 重復對象復用
2. 小整數內聯(direct)
3. 字符串編碼
4. 整數壓縮
五、總結
一、概述
在高級編程語言的世界中,開發者始終與【object/struct】這類高度抽象的數據結構打交道。然而在分布式架構下,任何服務進程都不是數據孤島——跨進程數據交換是必然需求。
以Java為例,業務邏輯的輸入輸出都是【object】。但在RPC場景中,這些對象必須經由網絡傳輸。這里出現了一個根本性矛盾:網絡介質(網線/光纖)對面向對象編程(OOP)一無所知,它們只會用光和電忠實地傳輸扁平化的字節流(byte[])。
圖片
軟件工程經典的分層理論驅使我們去添加一個轉換層。
圖片
圖片
我們需要有個工具或者組件來協助進行【object】和【byte[]】之間的雙向轉換。這個過程包含兩個對稱的流程:
- 【object】->【byte[]】:業界一般稱為序列化/serialize,但是那個單詞念起來很拗口,本文我們都叫它【編碼/encode】好了。
- 【byte[]】->【object】:業界一般稱為反序列化/deserialize,但是那個單詞念起來很拗口,本文我們都叫它【解碼/decode】好了。
Hessian作為Java生態中久經考驗的對象編解碼器,相較于同類產品具有以下兩大核心優勢:
- 深度Java生態適配:與JSON、Protobuf等語言中立的通用協議不同,Hessian專為Java深度優化,對泛型、多態等Java特有語言特性提供原生支持。
- 高效二進制協議:相較JSON等文本協議,Hessian采用精心設計的二進制編碼方案,在編解碼效率和數據壓縮率方面表現更優。
需要強調的是,軟件工程沒有銀彈——業務場景的差異決定了編解碼器的選擇必然需要權衡取舍。但就Java RPC而言,Hessian應該是經過廣泛實踐驗證的穩健選擇。
本文將系統解析Hessian的編碼流程,重點揭示其實現【object】->【byte[]】轉換的核心機制。
二、基礎編碼原理
對象編碼過程主要包含如下兩大核心:
- 對象圖遍歷:遍歷高級數據結構
通過反射或元編程技術遍歷對象圖(Object Graph)。
是同類產品的通用邏輯,不管jackson、fastjson、hessian都需要用不同的方式做類似的事情。
- 編碼格式:將高級數據結構按協議拍平放到byte[]
同類產品百家爭鳴,各有各的思路。
是同類產品的競技場,各個產品在這里體現差異化的競爭力。
設計權衡包括:
二進制效率 vs 可讀性(如Hessian二進制 vs JSON文本)
編碼緊湊性 vs 擴展靈活性
跨語言支持 vs 語言特性深度優化
對象圖遍歷決定了編碼能力的下限(能否正確處理對象結構),而編碼格式決定了編碼能力的上限(傳輸效率、兼容性等)。
對象圖遍歷
圖片
對象圖遍歷的本質是按深度優先進行對象屬性導航。
舉個例子:
圖片
宏觀來看,A類型的對象其實是一棵樹(或圖),如果腦補不出來的話,我給你畫個圖:
圖片
可以看到這棵樹的葉子結點都一定是Java內置的基本數據類型。換句話說,Java的8種基礎數據類型和他們的數組變體,支撐了Java豐富的預定義/自定義數據結構。
八股文:Java的8種基礎數據類型是哪些?String算不算基礎數據類型?
編碼的本質就是深度優先的遍歷這棵樹,拍平它,然后放到byte[]里。
我舉個例子吧。
偽代碼
為降低偽代碼復雜度,我們假設Java只有1種基礎數據類型int,也就是說Java里只有int和只包含int字段的自定義POJO。
我們定義POJO指的是用于傳輸、存儲使用的簡單Java Bean或者常說的DTO。
從某種意義上來說,Integer也是基于int封裝的自定義POJO。
字節流抽象
我們使用標準庫里的java.io.DataOutput來進行偽代碼說理,這個類提供了一些語義化的編碼function。
圖片
java.io.DataOutput
對象圖遍歷
圖片
字節流布局
最終呈現出來的字節流層面的數據布局會是這樣:
圖片
看起來沒毛病,唯一的問題就是不好解碼。
當解碼端收到一個16字節的字節流以后,它分不清哪塊數據是A對象的,哪塊數據是B對象的。甚至都分不清這到底是4個int32還是2個int64。
這個問題需要編碼格式來解決。
編碼格式
上面遺留的問題,聰明的你肯定想到了答案。
就是因為編碼產物太太太簡陋了,整個過程中只是一股腦的把樹拍平,把葉子節點的值寫入字節流,缺少結構元數據。
最最最重要的結構元數據就是數據塊的邊界,上述4個數據塊,最起碼應該添加3個邊界標識。
圖片
我們先用我們耳熟能詳的JSON格式來理解下編碼格式這個事情。
偽代碼
JSON是這樣解決這個問題的:
圖片
JSON協議在嵌套的POJO上用{}來作為邊界,POJO內部的字段鍵值用 , 來做邊界,:拆分字段鍵值。
字節流布局
結果就變成這樣:
圖片
這樣在解碼的時候,可以通過{、}、,、: 等token來切割JSON字符串,判定數據塊邊界并恢復出對象圖。
三、Hessian編碼格式
接下來我們可以開始介紹Hessian的編碼魔法了。
需要強調的是:Hessian跟JSON不同,Hessian是二進制格式。如果一個字節流直接按字符集解碼不能得到一個完整的、有意義的字符串,那它就是二進制編碼數據。
Hessian在編碼時,按數據塊類型為每一個數據塊添加一個前綴字節(byte)作為結構元數據,這些元數據和數據塊一起,交給解碼端使用。
數據塊
對象圖里的每一個節點,都是一個數據塊。
圖片
如上圖所示,以A對象為根的對象圖,一共有6個數據塊。
數據塊標簽(tag)
Hessain在編碼每一個數據塊時,都會根據數據塊的類型在字節流中寫入一個前綴字節(0-255),這個字節說明了數據塊的語義和結構。
以int32為例,其最基礎的編碼格式如下:
圖片
除該基礎編碼格式外,int32的編碼還有其他變體。
上述 I 就是整數類型的tag。解碼端讀取tag后,按tag值來解碼數據。
圖片
com.alibaba.com.caucho.hessian.io.Hessian2Input#readObject(java.util.List<java.lang.Class<?>>)
由此延伸、拓展,其他的數據類型都是類似的模式。常見數據類型及其對應的tag值如下:
圖片
值得注意的是,N、F、T三個tag是自解釋的,和固定值映射、綁定。
POJO編碼
POJO是一種特殊的數據塊,Hessian將POJO的結構和值拆開,分別編碼。
POJO結構編碼
POJO結構的tag為C,對照int32的編碼格式,POJO結構的編碼格式如下:
圖片
舉個例子:
圖片
編碼POJO時,Hessain會將POJO的類名、字段名列表寫入字節流,供解碼端使用。后續編碼POJO字段值時,需要按照字段名列表(如上述bb、cc)的順序來編碼字段值。
圖片
POJO字段值編碼
POJO字段值的tag為O,對照int32的編碼格式,POJO字段值的編碼格式如下:
圖片
舉個例子:
圖片
可以看到,編碼POJO字段值的時候,在tag后面有一個POJO結構序號。
這是Hessian的一個數據復用的小技巧。
POJO結構復用
JSON協議有一個缺點,那就是重復數據帶來的存儲/傳輸開銷。舉個例子:
圖片
如上圖,B類型的字段名(dd、ee)在編碼產物中重復出現!
Hessian希望解決這個問題,同一類型的多個POJO對象在序列化時,只需要在第一次的時候編碼類名、字段名等元數據,后續可以被重復的引用、使用,無需重復編碼。
如果用Hessian來編碼,結果會是這樣:
數據布局
圖片
數據布局詳解
圖片
如上圖,APojo、BPojo的字段名只會編碼一次。多個BPojo對象在編碼時會通過結構引用序號(1)來引用它。相對JSON,Hessian避免了多次編碼BPojo字段名的開銷。
為什么APojo的序號是1、BPojo的序號是2?
Hessain在編碼過程中,每次遇到一個新的、沒有處理過的新POJO類型時,會給它分配一個從0開始、單調遞增的序號。
遙相呼應的,解碼側每次解碼一個tag為C的POJO結構數據塊時,也會按解碼順序維護好其索引序號。
四、Hessian編碼細節
到現在,我們已經對Hessian編碼有了一個的概括性的認識,接下來我們來看看一些值得注意的細節。
重復對象復用
圖片
A對象里有兩個字段(d、e)指向同一個對象B。如果不做處理,會因為重復編碼而帶來不必要的開銷。
圖片
相同的一個B對象,因為被兩個字段重復引用,導致2次編碼、產生2份數據空間占用!
如果只是有額外的開銷,沒有可用性問題那都還好。關鍵是在循環引用場景下,會因為引用成環導致遞歸進行對象圖遍歷時觸發方法棧溢出!
圖片
循環引用是重復引用的特例,只要將重復引用處理掉,循環引用也就沒問題了。
Hessian通過對象引用來解決這個問題。在對象圖遍歷過程中,遇到一個之前沒有遇到過、處理過的POJO對象時,會給它分配一個從0開始、單調遞增的序號。
后續再次需要序列化相同的對象時,直接跳過編碼流程,將這個對象的序號寫入字節流。

解碼時,解碼側按相同的順序來恢復出引用序號表,解碼后續的對象引用。
小整數內聯(direct)
很多編碼類型,都需要在tag后再維護一個整數類型的字段。比如:
- POJO的編碼tag O需要一個整數來引用POJO結構引用序號。
圖片
- 類似String的變長類型需要一個整數來標識變長數據的長度。
圖片
當字符串很短,就比如"hi"吧,短字符串編碼格式的長度字段可能比實際字符數據還大(用4字節存儲長度2),效率低下。
tag分段
Hessian將一些tag值的語義富化,讓它既體現數據類型,也體現小數值。
因為tag是一個byte(int8),取值范圍是0-255,每個tag標識一種特定的數據類型(int、boolean等),但是這些數據類型最多幾十種,取值范圍內還有很大的數值區間沒有被使用,其實比較浪費。那我們就可以把這些空閑的tag值,挪作他用,提升tag數值空間利用率。
我舉個例子,注意這個是參考Hessian思路的一個簡單示意,具體的Tag值和Hessian無關。
圖片
長度內聯
對于長度≤31的字符串,Hessian用tag同時編碼類型和長度。
- 當0 <= tag <= 31 時,標識后續的數據塊為字符串。
- tag的數值即為后續數據塊的長度。
示例如下:
圖片
序號內聯
當結構引用序號<=16時,Hessain用tag同時編碼類型和序號。
1. 當0x60 <= tag <= 0x70 時,標識后續的數據塊為POJO字段值。
2. tag - 0x60的值,即為POJO結構(類名+字段名)引用序號。
示例如下:
圖片
相關源碼如下:
圖片
com.alibaba.com.caucho.hessian.io.Hessian2Output#writeObjectBegin
字符串編碼
Hessian編碼字符串的關鍵流程是:字符串分段+不同長度的子串使用不同的tag。
- 分段原則
字符串會被分割為若干塊,每塊最大長度為32768(0x8000)。前N-1塊均為完整長度的子串(32768字節),使用固定tag R標識;最后一塊為剩余部分,長度范圍為0-32768字節,根據實際長度選擇動態tag。
- 尾段tag的選擇基于尾塊的長度決定
長度≤31(0x1F):使用單字節tag 0x00-0x1F直接內聯長度值。
32≤長度≤1023(0x3FF):使用tag 0后跟1字節長度(大端序),10bit的計數空間由tag字節和長度字節共同提供。這個地方有點繞,看下代碼吧。
長度≥1024:使用tag S后跟2字節長度(大端序)。
- 相關源碼
圖片
com.alibaba.com.caucho.hessian.io.Hessian2Output#writeString(java.lang.String)
這種設計通過減少長字符串的冗余長度標記,在保持兼容性的同時顯著提升了編碼效率。
整數壓縮
基礎編碼
整數(int32)的的取值范圍很大(-23^31 - 2^31),保守的編碼格式會用4個byte來編碼整數。
圖片
但是日常使用中,我們會大量使用小整數,比如1、31。這時候如果還用4字節編碼就很不劃算啦~
變長編碼
Hessian根據整數的值范圍,動態的選擇不同的編碼方式,且不同的編碼方式有不同的tag:
- 單字節整數編碼:類似【長度壓縮】,tag中直接內聯數值
適用范圍:-16 到 47(共64個值)
編碼方式:使用單字節,值為 value + 0x90(144)
例如:0 編碼為 0x90,-1 編碼為 0x8f,47 編碼為 0xbf
- 雙字節整數編碼
適用范圍:-2048 到 2047
編碼方式:首字節為 0xc8 + (value >> 8),后跟一個字節存儲value剩下的bit。
這種編碼可以表示12bit有符號整數
- 三字節整數編碼
適用范圍:-262144 到 262143
編碼方式:首字節為 0xd4 + (value >> 16),后跟兩個字節存儲 value 的高8位和低8位。
這種編碼可以表示19bit有符號整數。
- 五字節整數編碼
適用范圍:超出上述范圍的所有32位整數
編碼方式:以 'I'(0x49)開頭,后跟4個字節表示完整的32位整數值。
- 相關源碼:
圖片
com.alibaba.com.caucho.hessian.io.Hessian2Output#writeInt
收益
- 小整數(如 0、-1)僅需 1字節 ,而傳統 int32 固定4字節。
- 大整數動態擴展,避免固定長度浪費(如 1000 僅需2字節)。
其他的數值類型比如int64也有類似的機制。
五、總結
Hessian專為Java優化,采用高效二進制協議,通過對象圖遍歷和編碼協議實現對象與字節流的轉換,利用數據塊標簽、重復對象復用、數據壓縮等機制,提升編解碼效率和數據壓縮率。
本文沒有去展開Hessian的代碼細節,而是盡可能深入淺出的介紹了Hessain的核心編碼原理,以幫助讀者建立對Hessian的宏觀認知,從而可以更好的去理解和使用它。
盡管不同語言/生態的序列化框架選型讓人眼花繚亂,但是各自需要解決的問題和解決問題的思路都大同小異;我們對Hessain原理的認識可以遷移到其他序列化框架,甚至自己寫一個領域特定的序列化框架。
相關內容均為筆者走讀源碼整理而來,如有疏漏,歡迎指正。
參考:
- Hessian 2.0 Serialization Protocol(http://hessian.caucho.com/doc/hessian-serialization.html)
- Hessian 2.0 序列化協議(中文版)(https://www.diguage.com/post/hessian-serialization-protocol/)
- Hessian 協議解釋與實戰(一):布爾、日期、浮點數與整數(https://www.diguage.com/post/hessian-protocol-interpretation-and-practice-1/)
- Hessian 協議解釋與實戰(二):長整型、二進制數據與 Null(https://www.diguage.com/post/hessian-protocol-interpretation-and-practice-2/)
- Hessian 協議解釋與實戰(三):字符串(https://www.diguage.com/post/hessian-protocol-interpretation-and-practice-3/)
- Hessian 協議解釋與實戰(四):數組與集合(https://www.diguage.com/post/hessian-protocol-interpretation-and-practice-4/)
- Hessian 協議解釋與實戰(五):對象與映射(https://www.diguage.com/post/hessian-protocol-interpretation-and-practice-5/)
- Hessian 源碼分析(Java)(https://www.diguage.com/post/hessian-source-analysis-for-java/)





















