1. 程式人生 > >分散式基礎之序列化與反序列化

分散式基礎之序列化與反序列化

 

目錄

序列化與反序列化,Why?

序列化的意義

一個簡單例項

JAVA序列化的高階認識

serialVersionUID 的作用

靜態變數序列化

父類的序列化

Transient 關鍵字

序列化的儲存規則

序列化實現深克隆

常見的序列化技術

XML 序列化框架

JSON 序列化框架

Hessian 序列化框架

Protobuf 序列化框架

序列化技術的選型

技術層面

選型建議


序列化與反序列化,Why?

序列化的意義

Java 平臺允許我們在記憶體中建立可複用的 Java 物件,但一般情況下, 只有當JVM 處於執行時,這些物件才可能存在,即,這些物件的生命週期不會比 JVM 的生命週期更長。但在現實應用中,就可能要求在 JVM 停止執行之後能夠儲存(持久化)指定的物件,並在將來重新讀取被儲存的物件。Java 物件序列化就能夠幫助我們實現該功能

簡單來說

序列化是把物件的狀態資訊轉化為可儲存或傳輸的形式過程,也就是把物件轉化為位元組序列的過程稱為物件的序列化

反序列化是序列化的逆向過程,把位元組陣列反序列化為物件,把位元組序列恢復為物件的過程成為物件的反序列化

一個簡單例項

在Java 中,只要一個類實現了 java.io.Serializable 介面,那麼它就可以被序列化。JDK 提供了Java物件的序列化方式, 主要通過物件輸出流java.io.ObjectOutputStream 和物件輸入流 java.io.ObjectInputStream 來實現。其中,被序列化的物件需要實現 java.io.Serializable 介面。

定義工具介面

實現工具介面

呼叫工具介面

 

JAVA序列化的高階認識

serialVersionUID 的作用

Java 的序列化機制是通過判斷類的serialVersionUID 來驗證版本一致性的。在進行反序列化時,JVM 會把傳來的位元組流中的 serialVersionUID 與本地相應實體類的 serialVersionUID 進行比較,如果相同就認為是一致的,可以進行反序列化,否則就會出現序列化版本不一致的異常,即是 InvalidCastException

如果沒有為指定的 class 配置 serialVersionUID,那麼 java 編譯器會自動給這個 class 進行一個摘要演算法

,類似於指紋演算法,只要這個檔案有任何改動,得到的 UID 就會截然不同的,可以保證在這麼多類中,這個編號是唯一的

serialVersionUID 有兩種顯示的生成方式:

一是預設的 1L,比如:private static final long serialVersionUID = 1L; 二是根據類名、介面名、成員方法及屬性等來生成一個 64 位的雜湊欄位

當 實 現 java.io.Serializable 接 口 的 類 沒 有 顯 式 地 定 義 一 個serialVersionUID 變數時候,Java 序列化機制會根據編譯的 Class 自動生成一個 serialVersionUID 作序列化版本比較用,這種情況下,如果Class 檔案(類名,方法明等)沒有發生變化(增加空格,換行,增加註釋等等),就算再編譯多次,serialVersionUID 也不會變化的。

 

靜態變數序列化

在 User 中新增一個全域性的靜態變數 num , 在執行序列化以後修改num 的值為 10, 然後通過反序列化以後得到的物件去輸出 num 的值

最後的輸出是 10,理論上列印的 num 是從讀取的物件裡獲得的,應該是儲存時的狀態才對。之所以列印 10 的原因在於序列化時,並不儲存靜態變數,這其實比較容易理解,序列化儲存的是物件的狀態,靜態變數屬於類的狀態,因此 序列化並不儲存靜態變數。

父類的序列化

一個子類實現了 Serializable 介面,它的父類都沒有實現 Serializable介面,在子類中設定父類的成員變數的值,接著序列化該子類物件。再反序列化出來以後輸出父類屬性的值。結果應該是什麼?

發現父類的sex 欄位的值為 null。也就是父類沒有實現序列化

結論:

  1. 當一個父類沒有實現序列化時,子類繼承該父類並且實現了序列化。在反序列化該子類後,是沒辦法獲取到父類的屬性值
  2. 當一個父類實現序列化,子類自動實現序列化,不需要再顯示實現Serializable 介面
  3. 當一個物件的例項變數引用了其他物件,序列化該物件時也會把引用物件進行序列化,但是前提是該引用物件必須實現序列化介面

Transient 關鍵字

Transient 關鍵字的作用是控制變數的序列化,在變數宣告前加上該關鍵字,可以阻止該變數被序列化到檔案中,在被反序列化後,transient 變數的值被設為初始值,如 int 型的是 0,物件型的是 null

繞開transient 機制的辦法

注意:writeObject  和readObject 這兩個私有的方法,既不屬於 Object、也不是Serializable, 為什麼能夠在序列化的時候被呼叫呢?   原因是,ObjectOutputStream 使用了反射來尋找是否聲明瞭這兩個方法。因為  ObjectOutputStream 使用 getPrivateMethod,所以這些方法必須宣告為 priate 以至於供ObjectOutputStream 來使用

序列化的儲存規則

同一物件兩次(開始寫入檔案到最終關閉流這個過程算一次,上面的演示效果是不關閉流的情況才能演示出效果)寫入檔案,打印出寫入一次物件後的儲存大小和寫入兩次後的儲存大小,第二次寫入物件時檔案只增加了 5 位元組

Java 序列化機制為了節省磁碟空間,具有特定的儲存規則,當寫入檔案的為同一物件時(未關閉流),並不會再將物件的內容進行儲存,而只是再次儲存一份引用,上面增加的 5  位元組的儲存空間就是新增引用和一些控制資訊的空間。反序列化時,恢復引用關係.該儲存規則極大的節省了儲存空間

 

序列化實現深克隆

在 Java 領域中,克隆分為深度克隆和淺克隆

淺克隆

在 Java 中存在一個 Cloneable 介面,通過實現這個介面的類都會具備clone 的能力,同時 clone 是在記憶體中進行,在效能方面會比我們直接通過 new 生成物件要高一些,特別是一些大的物件的生成,效能提升相對比較明顯。

被複制物件的所有變數都含有與原來的物件相同的值,而所有的對其他物件的引用仍然指向原來的物件。實現一個郵件通知功能,通過淺克隆實現如下

但是,當我們只希望,修改“黑白”的上課時間,調整為 20:30 分。通過結果發現,所有人的通知訊息都發生了改變。這是因為 p2 克隆的這個物件的 Email 引用地址指向的是同一個。這就是淺克隆

深克隆

被複制物件的所有變數都含有與原來的物件相同的值,除去那些引用其他物件的變數。那些引用其他物件的變數將指向被複制過的新物件,而不再是原有的那些被引用的物件。換言之,深拷貝把要複製的物件所引用的物件都複製了一遍

這樣就能實現深克隆效果,原理是把物件序列化輸出到一個流中,然後在把物件從序列化流中讀取出來,這個物件就不是原來的物件了

 

常見的序列化技術

使用JAVA 進行序列化有他的優點,也有他的缺點優點:JAVA 語言本身提供,使用比較方便和簡單

缺點:不支援跨語言處理、 效能相對不是很好,序列化以後產生的資料相對較大

 

XML 序列化框架

XML 序列化的好處在於可讀性好,方便閱讀和除錯。但是序列化以後的位元組碼檔案比較大,而且效率不高,適用於對效能不高,而且 QPS 較低的企業級內部系統之間的資料交換的場景,同時 XML 又具有語言無關性,所以還可以用於異構系統之間的資料交換和協議。比如我們熟知的Webservice,就是採用 XML 格式對資料進行序列化的

 

JSON 序列化框架

JSON(JavaScript Object Notation)是一種輕量級的資料交換格式,相對於XML 來說,JSON 的位元組流更小,而且可讀性也非常好。現在 JSON 資料格式在企業運用是最普遍的

JSON 序列化常用的開源工具有很多

  1. Jackson (https://github.com/FasterXML/jackson)
  2. 阿里開源的FastJson (https://github.com/alibaba/fastjon)
  3. Google 的GSON (https://github.com/google/gson)

這幾種 json 序列化工具中,Jackson 與 fastjson 要比 GSON 的效能要好,但是Jackson、GSON 的穩定性要比 Fastjson 好。而fastjson 的優勢在於提供的 api 非常容易使用

 

Hessian 序列化框架

Hessian 是一個支援跨語言傳輸的二進位制序列化協議,相對於 Java 預設的序列化機制來說,Hessian 具有更好的效能和易用性,而且支援多種不同的語言

實際上 Dubbo 採用的就是 Hessian 序列化來實現,只不過 Dubbo 對Hessian 進行了重構,效能更高

 

Protobuf 序列化框架

Protobuf 是Google 的一種資料交換格式,它獨立於語言、獨立於平臺

 

Google 提供了多種語言來實現,比如Java、C、Go、Python,每一種實現都包含了相應語言的編譯器和庫檔案

Protobuf 使用比較廣泛,主要是空間開銷小和效能比較好,非常適合用於公司內部對效能要求高的RPC 呼叫。 另外由於解析效能比較高,序列化以後資料量相對較少,所以也可以應用在物件的持久化場景中

但是但是要使用 Protobuf 會相對來說麻煩些,因為他有自己的語法, 有自己的編譯器

 

序列化技術的選型

技術層面

  1. 序列化空間開銷,也就是序列化產生的結果大小,這個影響到傳輸的效能
  2. 序列化過程中消耗的時長,序列化消耗時間過長影響到業務的響應時間
  3. 序列化協議是否支援跨平臺,跨語言。因為現在的架構更加靈活,如果存在異構系統通訊需求,那麼這個是必須要考慮的
  4. 可擴充套件性/相容性,在實際業務開發中,系統往往需要隨著需求的快速迭代來實現快速更新,這就要求我們採用的序列化協議基於良好的可擴充套件性/相容性,比如在現有的序列化資料結構中新增一個業務欄位,不會影響到現有的服務
  5. 技術的流行程度,越流行的技術意味著使用的公司多,那麼很多坑都已經淌過並且得到了解決,技術解決方案也相對成熟
  6. 學習難度和易用性

選型建議

  1. 效能要求不高的場景,可以採用基於 XML 的SOAP 協議
  2. 效能和間接性有比較高要求的場景,那麼 Hessian、Protobuf、Thrift、Avro 都可以。
  3. 於前後端分離,或者獨立的對外的 api 服務,選用 JSON 是比較好的,對於除錯、可讀性都很不錯
  4. Avro 設計理念偏於動態型別語言,那麼這類的場景使用 Avro 是可以的。PS:靜態語言如java、C++、C等;動態語言:JS、Python、Ruby等