java 將3變為03,03 Java序列化引發的血案

1、前言

《手冊》第 9 頁 “OOP 規約” 部分有一段關于序列化的約定

【強制】當序列化類新增屬性時,請不要修改 serialVersionUID 字段,以避免反序列失敗;如果完全不兼容升級,避免反序列化混亂,那么請修改 serialVersionUID 值。

說明:注意 serialVersionUID 值不一致會拋出序列化運行時異常。

我們應該思考下面幾個問題:

序列化和反序列化到底是什么?

它的主要使用場景有哪些?

Java 序列化常見的方案有哪些?

各種常見序列化方案的區別有哪些?

實際的業務開發中有哪些坑點?

接下來將從這幾個角度去研究這個問題。

2. 序列化和反序列化是什么?為什么需要它?

序列化是將內存中的對象信息轉化成可以存儲或者傳輸的數據到臨時或永久存儲的過程。而反序列化正好相反,是從臨時或永久存儲中讀取序列化的數據并轉化成內存對象的過程。

AAffA0nNPuCLAAAAAElFTkSuQmCC

那么為什么需要序列化和反序列化呢?

希望大家能夠養成從本源上思考這個問題的思維方式,即思考它為什么會出現,而不是單純記憶。

大家可以回憶一下,平時都是如果將文字文件、圖片文件、視頻文件、軟件安裝包等傳給小伙伴時,這些資源在計算機中存儲的方式是怎樣的。

進而再思考,Java 中的對象如果需要存儲或者傳輸應該通過什么形式呢?

我們都知道,一個文件通常是一個 m 個字節的序列:B0, B1, …, Bk, …, Bm-1。所有的 I/O 設備(例如網絡、磁盤和終端)都被模型化為文件,而所有的輸入和輸出都被當作對應文件的讀和寫來執行。

因此本質上講,文本文件,圖片、視頻和安裝包等文件底層都被轉化為二進制字節流來傳輸的,對方得文件就需要對文件進行解析,因此就需要有能夠根據不同的文件類型來解碼出文件的內容的程序。

大家試想一個典型的場景:如果要實現 Java 遠程方法調用,就需要將調用結果通過網路傳輸給調用方,如果調用方和服務提供方不在一臺機器上就很難共享內存,就需要將 Java 對象進行傳輸。而想要將 Java 中的對象進行網絡傳輸或存儲到文件中,就需要將對象轉化為二進制字節流,這就是所謂的序列化。存儲或傳輸之后必然就需要將二進制流讀取并解析成 Java 對象,這就是所謂的反序列化。

序列化的主要目的是:方便存儲到文件系統、數據庫系統或網絡傳輸等。

實際開發中常用到序列化和反序列化的場景有:

遠程方法調用(RPC)的框架里會用到序列化。

將對象存儲到文件中時,需要用到序列化。

將對象存儲到緩存數據庫(如 Redis)時需要用到序列化。

通過序列化和反序列化的方式實現對象的深拷貝。

3. 常見的序列化方式

常見的序列化方式包括 Java 原生序列化、Hessian 序列化、Kryo 序列化、JSON 序列化等。

3.1 Java 原生序列化

正如前面章節講到的,對于 JDK 中有的類,最好的學習方式之一就是直接看其源碼。

Serializable 的源碼非常簡單,只有聲明,沒有屬性和方法:

// 注釋太長,省略

public interface Serializable {

}

在學習源碼注釋之前,希望大家可以站在設計者的角度,先思考一個問題:如果一個類序列化到文件之后,類的結構發生變化還能否保證正確地反序列化呢?

答案顯然是不確定的。

那么如何判斷文件被修改過了呢? 通常可以通過加密算法對其進行簽名,文件作出任何修改簽名就會不一致。但是 Java 序列化的場景并不適合使用上述的方案,因為類文件的某些位置加個空格,換行等符號類的結構沒有發生變化,這個簽名就不應該發生變化。還有一個類新增一個屬性,之前的屬性都是有值的,之前都被序列化到對象文件中,有些場景下還希望反序列化時可以正常解析,怎么辦呢?

那么是否可以通過約定一個唯一的 ID,通過 ID 對比,不一致就認為不可反序列化呢?

實現序列化接口后,如果開發者不手動指定該版本號 ID 怎么辦?

既然 Java 序列化場景下的 “簽名” 應該根據類的特點生成,我們是否可以不指定序列化版本號就默認根據類名、屬性和函數等計算呢?

如果針對某個自己定義的類,想自定義序列化和反序列化機制該如何實現呢?支持嗎?

帶著這些問題我們繼續看序列化接口的注釋。

Serializable 的源碼注釋特別長,其核心大致作了下面的說明:

Java 原生序列化需要實現 Serializable 接口。序列化接口不包含任何方法和屬性等,它只起到序列化標識作用。

一個類實現序列化接口則其子類型也會繼承序列化能力,但是實現序列化接口的類中有其他對象的引用,則其他對象也要實現序列化接口。序列化時如果拋出 NotSerializableException 異常,說明該對象沒有實現 Serializable 接口。

每個序列化類都有一個叫 serialVersionUID 的版本號,反序列化時會校驗待反射的類的序列化版本號和加載的序列化字節流中的版本號是否一致,如果序列化號不一致則會拋出 InvalidClassException 異常。

強烈推薦每個序列化類都手動指定其 serialVersionUID,如果不手動指定,那么編譯器會動態生成默認的序列化號,因為這個默認的序列化號和類的特征以及編譯器的實現都有關系,很容易在反序列化時拋出 InvalidClassException 異常。建議將這個序列化版本號聲明為私有,以避免運行時被修改。

實現序列化接口的類可以提供自定義的函數修改默認的序列化和反序列化行為。

自定義序列化方法:

private void writeObject(ObjectOutputStream out) throws IOException;

自定義反序列化方法:

private void readObject(ObjectInputStream in)

throws IOException, ClassNotFoundException;

通過自定義這兩個函數,可以實現序列化和反序列化不可序列化的屬性,也可以對序列化的數據進行數據的加密和解密處理。

3.2 Hessian 序列化

Hessian 是一個動態類型,二進制序列化,也是一個基于對象傳輸的網絡協議。Hessian 是一種跨語言的序列化方案,序列化后的字節數更少,效率更高。Hessian 序列化會把復雜對象的屬性映射到 Map 中再進行序列化。

3.3 Kryo 序列化

Kryo 是一個快速高效的 Java 序列化和克隆工具。Kryo 的目標是快速、字節少和易用。Kryo 還可以自動進行深拷貝或者淺拷貝。Kryo 的拷貝是對象到對象的拷貝而不是對象到字節,再從字節到對象的恢復。Kryo 為了保證序列化的高效率,會提前加載需要的類,這會帶一些消耗,但是這是序列化后文件較小且反序列化非常快的重要原因。

3.4 JSON 序列化

JSON (JavaScript Object Notation) 是一種輕量級的數據交換方式。JSON 序列化是基于 JSON 這種結構來實現的。JSON 序列化將對象轉化成 JSON 字符串,JSON 反序列化則是將 JSON 字符串轉回對象的過程。常用的 JSON 序列化和反序列化的庫有 Jackson、GSON、Fastjson 等。

4.Java 常見的序列化方案對比

我們想要對比各種序列化方案的優劣無外乎兩點,一點是查資料,一點是自己寫代碼驗證。

4.1 Java 原生序列化

Java 序列化的優點是:對對象的結構描述清晰,反序列化更安全。主要缺點是:效率低,序列化后的二進制流較大。

4.2 Hessian 序列化

Hession 序列化二進制流較 Java 序列化更小,且序列化和反序列化耗時更短。但是父類和子類有相同類型屬性時,由于先序列化子類再序列化父類,因此反序列化時子類的同名屬性會被父類的值覆蓋掉,開發時要特別注意這種情況。

Hession2.0 序列化二進制流大小是 Java 序列化的 50%,序列化耗時是 Java 序列化的 30%,反序列化的耗時是 Java 序列化的 20%。

編寫待測試的類:

@Data

public class PersonHessian implements Serializable {

private Long id;

private String name;

private Boolean male;

}

@Data

public class Male extends PersonHessian {

private Long id;

}

編寫單測來模擬序列化繼承覆蓋問題:

/**

* 驗證Hessian序列化繼承覆蓋問題

*/

@Test

public void testHessianSerial() throws IOException {

HessianSerialUtil.writeObject(file, male);

Male maleGet = HessianSerialUtil.readObject(file);

// 相等

Assert.assertEquals(male.getName(), maleGet.getName());

// male.getId()結果是1,maleGet.getId()結果是null

Assert.assertNull(maleGet.getId());

Assert.assertNotEquals(male.getId(), maleGet);

}

上述單測示例驗證了:反序列化時子類的同名屬性會被父類的值覆蓋掉的問題。

4.3 Kryo 序列化

Kryo 優點是:速度快、序列化后二進制流體積小、反序列化超快。但是缺點是:跨語言支持復雜。注冊模式序列化更快,但是編程更加復雜。

4.4 JSON 序列化

JSON 序列化的優勢在于可讀性更強。主要缺點是:沒有攜帶類型信息,只有提供了準確的類型信息才能準確地進行反序列化,這點也特別容易引發線上問題。

下面給出使用 Gson 框架模擬 JSON 序列化時遇到的反序列化問題的示例代碼:

/**

* 驗證GSON序列化類型錯誤

*/

@Test

public void testGSON() {

Map map = new HashMap<>();

final String name = "name";

final String id = "id";

map.put(name, "張三");

map.put(id, 20L);

String jsonString = GSONSerialUtil.getJsonString(map);

Map mapGSON = GSONSerialUtil.parseJson(jsonString, Map.class);

// 正確

Assert.assertEquals(map.get(name), mapGSON.get(name));

// 不等 map.get(id)為Long類型 mapGSON.get(id)為Double類型

Assert.assertNotEquals(map.get(id).getClass(), mapGSON.get(id).getClass());

Assert.assertNotEquals(map.get(id), mapGSON.get(id));

}

下面給出使用 fastjson 模擬 JSON 反序列化問題的示例代碼:

/**

* 驗證FatJson序列化類型錯誤

*/

@Test

public void testFastJson() {

Map map = new HashMap<>();

final String name = "name";

final String id = "id";

map.put(name, "張三");

map.put(id, 20L);

String fastJsonString = FastJsonUtil.getJsonString(map);

Map mapFastJson = FastJsonUtil.parseJson(fastJsonString, Map.class);

// 正確

Assert.assertEquals(map.get(name), mapFastJson.get(name));

// 錯誤 map.get(id)為Long類型 mapFastJson.get(id)為Integer類型

Assert.assertNotEquals(map.get(id).getClass(), mapFastJson.get(id).getClass());

Assert.assertNotEquals(map.get(id), mapFastJson.get(id));

}

大家還可以通過單元測試構造大量復雜對象對比各種序列化方式或框架的效率。

如定義下列測試類為 User,包括以下多種類型的屬性:

@Data

public class User implements Serializable {

private Long id;

private String name;

private Integer age;

private Boolean sex;

private String nickName;

private Date birthDay;

private Double salary;

}

4.5 各種常見的序列化性能排序

實驗的版本:kryo-shaded 使用 4.0.2 版本,gson 使用 2.8.5 版本,hessian 用 4.0.62 版本。

實驗的數據:構造 50 萬 User 對象運行多次。

大致得出一個結論:

從二進制流大小來講:JSON 序列化 > Java 序列化 > Hessian2 序列化 > Kryo 序列化 > Kryo 序列化注冊模式;

從序列化耗時而言來講:GSON 序列化 > Java 序列化 > Kryo 序列化 > Hessian2 序列化 > Kryo 序列化注冊模式;

從反序列化耗時而言來講:GSON 序列化 > Java 序列化 > Hessian2 序列化 > Kryo 序列化注冊模式 > Kryo 序列化;

從總耗時而言:Kryo 序列化注冊模式耗時最短。

注:由于所用的序列化框架版本不同,對象的復雜程度不同,環境和計算機性能差異等原因結果可能會有出入。

5. 序列化引發的一個血案

接下來我們看下面的一個案例:

前端調用服務 A,服務 A 調用服務 B,服務 B 首次接到請求會查 DB,然后緩存到 Redis(緩存 1 個小時)。服務 A 根據服務 B 返回的數據后執行一些處理邏輯,處理后形成新的對象存到 Redis(緩存 2 個小時)。

服務 A 通過 Dubbo 來調用服務 B,A 和 B 之間數據通過 Map 類型傳輸,服務 B 使用 Fastjson 來實現 JSON 的序列化和反序列化。

服務 B 的接口返回的 Map 值中存在一個 Long 類型的 id 字段,服務 A 獲取到 Map ,取出 id 字段并強轉為 Long 類型使用。

執行的流程如下:

AAffA0nNPuCLAAAAAElFTkSuQmCC通過分析我們發現,服務 A 和服務 B 的 RPC 調用使用 Java 序列化,因此類型信息不會丟失。

但是由于服務 B 采用 JSON 序列化進行緩存,第一次訪問沒啥問題,其執行流程如下:

AAffA0nNPuCLAAAAAElFTkSuQmCC

如果服務 A 開啟了緩存,服務 A 在第一次請求服務 B 后,緩存了運算結果,且服務 A 緩存時間比服務 B 長,因此不會出現錯誤。

AAffA0nNPuCLAAAAAElFTkSuQmCC

如果服務 A 不開啟緩存,服務 A 會請求服務 B ,由于首次請求時,服務 B 已經緩存了數據,服務 B 從 Redis(B)中反序列化得到 Map。流程如下圖所示:

AAffA0nNPuCLAAAAAElFTkSuQmCC

然而問題來了: 服務 A 從 Map 取出此 Id 字段,強轉為 Long 時會出現類型轉換異常。

最后定位到原因是 Json 反序列化 Map 時如果原始值小于 Int 最大值,反序列化后原本為 Long 類型的字段,變為了 Integer 類型,服務 B 的同學緊急修復。

服務 A 開啟緩存時, 雖然采用了 JSON 序列化存入緩存,但是采用 DTO 對象而不是 Map 來存放屬性,所以 JSON 反序列化沒有問題。

因此大家使用二方或者三方服務時,當對方返回的是 Map 類型的數據時要特別注意這個問題。

作為服務提供方,可以采用 JDK 或者 Hessian 等序列化方式;

作為服務的使用方,我們不要從 Map 中一個字段一個字段獲取和轉換,可以使用 JSON 庫直接將 Map 映射成所需的對象,這樣做不僅代碼更簡潔還可以避免強轉失敗。

代碼示例:

@Test

public void testFastJsonObject() {

Map map = new HashMap<>();

final String name = "name";

final String id = "id";

map.put(name, "張三");

map.put(id, 20L);

String fastJsonString = FastJsonUtil.getJsonString(map);

// 模擬拿到服務B的數據

Map mapFastJson = FastJsonUtil.parseJson(fastJsonString,map.getClass());

// 轉成強類型屬性的對象而不是使用map 單個取值

User user = new JSONObject(mapFastJson).toJavaObject(User.class);

// 正確

Assert.assertEquals(map.get(name), user.getName());

// 正確

Assert.assertEquals(map.get(id), user.getId());

}

6. 總結

本節的主要講解了序列化的主要概念、主要實現方式,以及序列化和反序列化的幾個坑點,希望大家在實際業務開發中能夠注意這些細節,避免趟坑。

下一節將講述淺拷貝和深拷貝的相關知識。

7. 課后題

給出一個 PersonTransit 類,一個 Address 類,假設 Address 是其它 jar 包中的類,沒實現序列化接口。請使用今天講述的自定義的函數 writeObject 和 readObject 函數實現 PersonTransit 對象的序列化,要求反序列化后 address 的值正常。

@Data

public class PersonTransit implements Serializable {

private Long id;

private String name;

private Boolean male;

private List friends;

private Address address;

}

@Data

@AllArgsConstructor

public class Address {

private String detail;

}

參考資料

阿里巴巴與 Java 社區開發者.《 Java 開發手冊 1.5.0》華山版. 2019. 9 ??

[美] Randal E.Bryant/ David O’Hallaron.《深入理解計算機系統》. [譯] 龔奕利,賀蓮。機械工業出版社. 2016 ??

楊冠寶。高海慧.《碼出高效:Java 開發手冊》. 電子工業出版社. 2018 ??}

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/397559.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/397559.shtml
英文地址,請注明出處:http://en.pswp.cn/news/397559.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

《The Pomodoro Technique》

番茄工作法&#xff0c;專注當下&#xff0c;遠離拖延焦慮癥 簡介What to solveHow to useSome applications自我總結簡介 番茄工作法是簡單易行的時間管理方法&#xff0c;是由弗朗西斯科西里洛于1992年創立的一種相對于GTD更微觀的時間管理方法。 What to solve 各種Deadline…

XCoreRedux框架:Android UI組件化與Redux實踐

XCoreRedux框架:Android UI組件化與Redux實踐 author: 莫川 https://github.com/nuptboyzhb/XCoreRedux源碼Demo&#xff1a;https://github.com/nuptboyzhb/XCoreRedux使用android studio打開該項目。 目錄結構 demo 基于xcore框架寫的一個小demoxcore XCoreRedux核心代碼庫…

Gigaset ME/pure/pro體驗:就是這個德味

Gigaset是何方神圣&#xff1f;可能大多數人都沒有聽過。但如果說起西門子&#xff0c;那各位肯定就會“哦”地一聲明白了。實際上&#xff0c;Gigaset就是西門子旗下的手機品牌&#xff0c;當年世界上第一部數字無繩電話就是該品牌的產物&#xff0c;所以這次Gigaset在智能手機…

java獨步尋花,小班語言《江畔獨步尋花》

小班語言《江畔獨步尋花》活動目標&#xff1a;1、學習古詩&#xff0c;感知和理解古詩描繪的景象。2、感受古詩的文學語言。活動準備&#xff1a;1、古詩《江畔獨步尋花》PPT課件。2、柳條兩枝(一條葉子多的&#xff0c;一條葉子少的)活動過程&#xff1a;一、導入&#xff1a…

linux-shell——02

Linux命令的通用命令格式 :命令字 【選項】 【參數】 選項&#xff1a; 作用&#xff1a;用于調節命令的具體功能"-"引導短格式選項&#xff08;單個字符&#xff09; EX&#xff1a;“-l”"--"引導長格式選項&#xff08;多個字符&#xff09; EX: "…

IOS 資料備份

2019獨角獸企業重金招聘Python工程師標準>>> 利用本地服務器邊下載視頻邊播放 目前還沒有做好&#xff0c;下面是參考資料&#xff0c;做個備份&#xff1b; 參考資料&#xff1a; http://blog.csdn.net/wxw55/article/details/17557295 http://www.code4app.com/io…

BZOJ 1854: [Scoi2010]游戲( 二分圖最大匹配 )

匈牙利算法..從1~10000依次找增廣路, 找不到就停止, 輸出答案. ----------------------------------------------------------------------------#include<bits/stdc.h>using namespace std;const int MAXL 10009, MAXR 1000009;struct edge {int to;edge* next;} E[MA…

linux adduser mysql,linux獨享初始配置方法(ftp、apache、mysql)

在此我們對您購買的linux獨享服務器的配置方法進行簡單說明&#xff0c;內容涉及ftp、apache、mysql相關配置&#xff0c;希望給您使用中帶來方便。該文章為指導性說明。☆獨立服務器linux系統ftp帳戶的設置方法&#xff1a;1、首先服務器端已經安裝vsftp。2、您可以直接登陸服…

Android下文件的壓縮和解壓(Zip格式)

Zip文件結構 ZIP文件結構如下圖所示&#xff0c; File Entry表示一個文件實體,一個壓縮文件中有多個文件實體。 文件實體由一個頭部和文件數據組&#xff0c;Central Directory由多個File header組成&#xff0c;每個File header都保存一個文件實體的偏移&#xff0c;文件最后由…

快速理解和使用 ES7 await/async

await/async 是 ES7 最重要特性之一&#xff0c;它是目前為止 JS 最佳的異步解決方案了。雖然沒有在 ES2016 中錄入&#xff0c;但很快就到來&#xff0c;目前已經在 ES-Next Stage 4 階段。 直接上例子&#xff0c;比如我們需要按順序獲取&#xff1a;產品數據>用戶數據>…

jdeveloper優化:

D:\jdevstudio10133\jdev\bin\jdev.conf末尾加上下面的AddVMOption -Dsun.java2d.noddrawtrueAddVMOption -Dsun.java2d.ddoffscreenfalse 轉載于:https://www.cnblogs.com/sprinng/p/4780112.html

linux make java版本,告訴make是否在Windows或Linux上運行

更新請閱讀這個類似但更好的答案&#xff1a;https&#xff1a;//stackoverflow.com/a/14777895/938111make (和 gcc )可以使用Cygwin或MinGW在MS-Windows上輕松安裝 .正如ldigas所說&#xff0c; make 可以使用 UNAME:$(shell uname) 檢測平臺(命令 uname 也由Cygwin或MinGW安…

MPI多機器實現并行計算

最近使用一個系統的分布式版本搭建測試環境&#xff0c;該系統是基于MPI實現的并行計算&#xff0c;MPI是傳統基于msg的系統&#xff0c;這個框架非常靈活&#xff0c;對程序的結構沒有太多約束&#xff0c;高效實用簡單&#xff0c;下面是MPI在多臺機器上實現并行計算的過程。…

Jenkins_獲取源碼編譯并啟動服務(二)

一、創建Maven項目二、設置SVN信息三、設置構建觸發器四、設置Maven命令五、設置構建后發郵件信息&#xff08;參考文章一&#xff09;六、設置構建后拷貝文件到遠程機器并執行命令來自為知筆記(Wiz)

php 判斷頁面加載完,所有ajax執行完且頁面加載完判斷

jquery ajax&load 方法導致 js效果不顯示或顯示后由于加載后ajax 重新布局頁面導致效果錯誤。解決思路&#xff1a;需要在ajax get post 或 load 等執行完后再去執行方法就不會由于他們沒執行完導致的最終錯誤。那么首先看load 方法定義&#xff1a;jQuery ajax - load() 方…

正確理解ThreadLocal

想必很多朋友對 ThreadLocal并不陌生&#xff0c;今天我們就來一起探討下ThreadLocal的使用方法和實現原理。首先&#xff0c;本文先談一下對ThreadLocal的理 解&#xff0c;然后根據ThreadLocal類的源碼分析了其實現原理和使用需要注意的地方&#xff0c;最后給出了兩個應用場…

2018.7.10 個人博客文章=利用ORM創建分類和ORM的內置函數

昨天的注冊收尾工作 其實就差了和MySql聯系起來的部分&#xff0c;這部分很簡單&#xff0c;首先要做的就是保存用戶通過from傳送過來的頭像文件&#xff1a; """ 保存頭像文件 """ file request.FILES.get(avatar) file_path os.path.join(st…

python 列表與元組的操作簡介

上一篇&#xff1a;Python 序列通用操作介紹 列表 列表是可變的(mutable)——可以改變列表的內容&#xff0c;這不同于字符串和元組&#xff0c;字符串和元組都是不可變的。接下來討論一下列表所提供的方法。 list函數 可以使用list函數來創建列表&#xff1a; list(Hello) [H,…

mfc嵌入matlab繪圖窗口,將matlab的圖嵌入MFC

【實例簡介】VS調用matlab畫圖模塊編譯成的動態鏈接庫&#xff0c;并在MFC顯示。【實例截圖】【核心代碼】3b0582a3-4ea8-4a61-ba33-e448be563b88└── 將matlab的圖嵌入MFC├── matlab_2010b與VS2008_混合編程的實現.pdf├── TestWithData│ ├── Debug│ │ ├─…

python multiprocessing 和tcp

#用類方法 服務端from socket import *from multiprocessing import Processimport osclass Myprocess(Process): def __init__(self, conn): self.conn conn super().__init__() def run(self): conn self.conn start True whil…