《手冊》第 9 頁 「OOP 規約」 部分有一段關於序列化的約定 1:前端
【強制】當序列化類新增屬性時,請不要修改 serialVersionUID 字段,以免反序列失敗;若是徹底不兼容升級,避免反序列化混亂,那麼請修改 serialVersionUID 值。
說明:注意 serialVersionUID 值不一致會拋出序列化運行時異常。java
咱們應該思考下面幾個問題:數據庫
序列化和反序列化究竟是什麼?
它的主要使用場景有哪些?
Java 序列化常見的方案有哪些?
各類常見序列化方案的區別有哪些?
實際的業務開發中有哪些坑點?
接下來將從這幾個角度去研究這個問題。編程
序列化是將內存中的對象信息轉化成能夠存儲或者傳輸的數據到臨時或永久存儲的過程。而反序列化正好相反,是從臨時或永久存儲中讀取序列化的數據並轉化成內存對象的過程。json
那麼爲何須要序列化和反序列化呢?segmentfault
咱們都知道,文本文件,圖片、視頻和安裝包等文件底層都被轉化爲二進制字節流來傳輸的,對方得文件就須要對文件進行解析,所以就須要有可以根據不一樣的文件類型來解碼出文件的內容的程序。緩存
若是要實現 Java 遠程方法調用,就須要將調用結果經過網路傳輸給調用方,若是調用方和服務提供方不在一臺機器上就很難共享內存,就須要將 Java 對象進行傳輸。而想要將 Java 中的對象進行網絡傳輸或存儲到文件中,就須要將對象轉化爲二進制字節流,這就是所謂的序列化。存儲或傳輸以後必然就須要將二進制流讀取並解析成 Java 對象,這就是所謂的反序列化。安全
序列化的主要目的是:方便存儲到文件系統、數據庫系統或網絡傳輸等。網絡
實際開發中經常使用到序列化和反序列化的場景有:框架
常見的序列化方式包括 Java 原生序列化、Hessian 序列化、Kryo 序列化、JSON 序列化等。
正如前面章節講到的,對於 JDK 中有的類,最好的學習方式之一就是直接看其源碼。
Serializable 的源碼很是簡單,只有聲明,沒有屬性和方法:
public interface Serializable { }
先思考一個問題:若是一個類序列化到文件以後,類的結構發生變化還可否保證正確地反序列化呢?
答案顯然是不肯定的。
因此每一個序列化類都有一個叫 serialVersionUID 的版本號,反序列化時會校驗待反射的類的序列化版本號和加載的序列化字節流中的版本號是否一致,若是序列化號不一致則會拋出 InvalidClassException 異常。
強烈推薦每一個序列化類都手動指定其 serialVersionUID,若是不手動指定,那麼編譯器會動態生成默認的序列化號,由於這個默認的序列化號和類的特徵以及編譯器的實現都有關係,很容易在反序列化時拋出 InvalidClassException 異常。建議將這個序列化版本號聲明爲私有,以免運行時被修改。
實現序列化接口的類能夠提供自定義的函數修改默認的序列化和反序列化行爲。
//自定義序列化方法: private void writeObject(ObjectOutputStream out) throws IOException; //自定義反序列化方法 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;
經過自定義這兩個函數,能夠實現序列化和反序列化不可序列化的屬性,也能夠對序列化的數據進行數據的加密和解密處理。
Hessian 是一個動態類型,二進制序列化,也是一個基於對象傳輸的網絡協議。Hessian 是一種跨語言的序列化方案,序列化後的字節數更少,效率更高。Hessian 序列化會把複雜對象的屬性映射到 Map 中再進行序列化。
Kryo 是一個快速高效的 Java 序列化和克隆工具。Kryo 的目標是快速、字節少和易用。Kryo 還能夠自動進行深拷貝或者淺拷貝。Kryo 的拷貝是對象到對象的拷貝而不是對象到字節,再從字節到對象的恢復。Kryo 爲了保證序列化的高效率,會提早加載須要的類,這會帶一些消耗,可是這是序列化後文件較小且反序列化很是快的重要緣由。
JSON (JavaScript Object Notation) 是一種輕量級的數據交換方式。JSON 序列化是基於 JSON 這種結構來實現的。JSON 序列化將對象轉化成 JSON 字符串,JSON 反序列化則是將 JSON 字符串轉回對象的過程。經常使用的 JSON 序列化和反序列化的庫有 Jackson、GSON、Fastjson 等。
咱們想要對比各類序列化方案的優劣無外乎兩點,一點是查資料,一點是本身寫代碼驗證。
Java 序列化的優勢是:對對象的結構描述清晰,反序列化更安全。主要缺點是:效率低,序列化後的二進制流較大。
Hession 序列化二進制流較 Java 序列化更小,且序列化和反序列化耗時更短。可是父類和子類有相同類型屬性時,因爲先序列化子類再序列化父類,所以反序列化時子類的同名屬性會被父類的值覆蓋掉,開發時要特別注意這種狀況。
Hession2.0 序列化二進制流大小是 Java 序列化的 50%,序列化耗時是 Java 序列化的 30%,反序列化的耗時是 Java 序列化的 20%。
Kryo 優勢是:速度快、序列化後二進制流體積小、反序列化超快。可是缺點是:跨語言支持複雜。註冊模式序列化更快,可是編程更加複雜。
JSON 序列化的優點在於可讀性更強。主要缺點是:沒有攜帶類型信息,只有提供了準確的類型信息才能準確地進行反序列化,這點也特別容易引起線上問題。
下面給出使用 Gson 框架模擬 JSON 序列化時遇到的反序列化問題的示例代碼:
/** * 驗證GSON序列化類型錯誤 */ @Test public void testGSON() { Map<String, Object> map = new HashMap<>(); final String name = "name"; final String id = "id"; map.put(name, "張三"); map.put(id, 20L); String jsonString = GSONSerialUtil.getJsonString(map); Map<String, Object> 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<String, Object> map = new HashMap<>(); final String name = "name"; final String id = "id"; map.put(name, "張三"); map.put(id, 20L); String fastJsonString = FastJsonUtil.getJsonString(map); Map<String, Object> 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; }
實驗的版本: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 序列化註冊模式耗時最短。
注:因爲所用的序列化框架版本不一樣,對象的複雜程度不一樣,環境和計算機性能差別等緣由結果可能會有出入。
接下來咱們看下面的一個案例:
前端調用服務 A,服務 A 調用服務 B,服務 B 首次接到請求會查 DB,而後緩存到 Redis(緩存 1 個小時)。服務 A 根據服務 B 返回的數據後執行一些處理邏輯,處理後造成新的對象存到 Redis(緩存 2 個小時)。
服務 A 經過 Dubbo 來調用服務 B,A 和 B 之間數據經過 Map<String,Object> 類型傳輸,服務 B 使用 Fastjson 來實現 JSON 的序列化和反序列化。
服務 B 的接口返回的 Map 值中存在一個 Long 類型的 id 字段,服務 A 獲取到 Map ,取出 id 字段並強轉爲 Long 類型使用。
執行的流程以下:
經過分析咱們發現,服務 A 和服務 B 的 RPC 調用使用 Java 序列化,所以類型信息不會丟失。
可是因爲服務 B 採用 JSON 序列化進行緩存,第一次訪問沒啥問題,其執行流程以下:
若是服務 A 開啓了緩存,服務 A 在第一次請求服務 B 後,緩存了運算結果,且服務 A 緩存時間比服務 B 長,所以不會出現錯誤。
若是服務 A 不開啓緩存,服務 A 會請求服務 B ,因爲首次請求時,服務 B 已經緩存了數據,服務 B 從 Redis(B)中反序列化獲得 Map。流程以下圖所示:
然而問題來了: 服務 A 從 Map 取出此 Id 字段,強轉爲 Long 時會出現類型轉換異常。
最後定位到緣由是 Json 反序列化 Map 時若是原始值小於 Int 最大值,反序列化後本來爲 Long 類型的字段,變爲了 Integer 類型,服務 B 的同窗緊急修復。
服務 A 開啓緩存時, 雖然採用了 JSON 序列化存入緩存,可是採用 DTO 對象而不是 Map 來存放屬性,因此 JSON 反序列化沒有問題。
所以你們使用二方或者三方服務時,當對方返回的是 Map<String,Object> 類型的數據時要特別注意這個問題。
做爲服務提供方,能夠採用 JDK 或者 Hessian 等序列化方式;
做爲服務的使用方,咱們不要從 Map 中一個字段一個字段獲取和轉換,可使用 JSON 庫直接將 Map 映射成所需的對象,這樣作不只代碼更簡潔還能夠避免強轉失敗。
代碼示例:
@Test public void testFastJsonObject() { Map<String, Object> 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<String, Object> 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()); }
給出一個 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<PersonTransit> friends; private Address address; } @Data @AllArgsConstructor public class Address { private String detail; }
1、序列化主要有兩個困難:
1 transient 關鍵字,序列化時默認不序列化該字段。(新加的,增長難度)
2 假設 Address 是第三方 jar 包中的類,不容許修改實現序列化接口。
2、分析
咱們經過專欄的介紹還有序列化接口java.io.Serializable的註釋可知,能夠自定義序列化方法和反序列化方法:
private void writeObject(java.io.ObjectOutputStream out) throws IOException private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
實現序列化和反序列化不可序列化的屬性,也能夠對序列化的數據進行數據的加密和解密處理。
3、參考代碼
@Data public class PersonTransit implements Serializable { private Long id; private String name; private Boolean male; private List<PersonTransit> friends; private transient Address address; /** * 自定義序列化寫方法 */ private void writeObject(ObjectOutputStream oos) throws IOException { oos.defaultWriteObject(); oos.writeObject(address.getDetail()); } /** * 自定義反序列化讀方法 */ private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { ois.defaultReadObject(); this.setAddress(new Address( (String) ois.readObject())); } }
單元測試
@Test public void testJDKSerialOverwrite() throws IOException, ClassNotFoundException { PersonTransit person = new PersonTransit(); person.setId(1L); person.setName("張三"); person.setMale(true); person.setFriends(new ArrayList<>()); Address address = new Address(); address.setDetail("某某小區xxx棟yy號"); person.setAddress(address); // 序列化 JdkSerialUtil.writeObject(file, person); // 反序列化 PersonTransit personTransit = JdkSerialUtil.readObject(file); // 判斷是否相等 Assert.assertEquals(personTransit.getName(), person.getName()); Assert.assertEquals(personTransit.getAddress().getDetail(), person.getAddress().getDetail()); }
用到的工具類:
public class JdkSerialUtil { public static <T> void writeObject(File file, T data) throws IOException { try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));) { objectOutputStream.writeObject(data); objectOutputStream.flush(); } } public static <T> void writeObject(ByteArrayOutputStream outputStream, T data) throws IOException { try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream);) { objectOutputStream.writeObject(data); objectOutputStream.flush(); } } public static <T> T readObject(File file) throws IOException, ClassNotFoundException { FileInputStream fin = new FileInputStream(file); ObjectInputStream objectInputStream = new ObjectInputStream(fin); return (T) objectInputStream.readObject(); } public static <T> T readObject(ByteArrayInputStream inputStream) throws IOException, ClassNotFoundException { ObjectInputStream objectInputStream = new ObjectInputStream(inputStream); return (T) objectInputStream.readObject(); } }
經過單元測試驗證了咱們編寫代碼的正確性。
參考資料
明明如月:https://www.imooc.com/article...阿里巴巴與 Java 社區開發者.《 Java 開發手冊 1.5.0》華山版. 2019. 9 ↩︎[美] Randal E.Bryant/ David O’Hallaron.《深刻理解計算機系統》. [譯] 龔奕利,賀蓮。機械工業出版社. 2016 ↩︎楊冠寶。高海慧.《碼出高效:Java 開發手冊》. 電子工業出版社. 2018 ↩︎