將 Java 對象序列化爲二進制文件的 Java 序列化技術是 Java 系列技術中一個較爲重要的技術點,在大部分狀況下,開發人員只須要了解被序列化的類須要實現 Serializable 接口,使用 ObjectInputStream 和 ObjectOutputStream 進行對象的讀寫。然而在有些狀況下,光知道這些還遠遠不夠,文章列舉了筆者遇到的一些真實情境,它們與 Java 序列化相關,經過分析情境出現的緣由,使讀者輕鬆牢記 Java 序列化中的一些高級認識。html
序列化 ID 問題
情境:兩個客戶端 A 和 B 試圖經過網絡傳遞對象數據,A 端將對象 C 序列化爲二進制數據再傳給 B,B 反序列化獲得 C。java
問題:C 對象的全類路徑假設爲 com.inout.Test,在 A 和 B 端都有這麼一個類文件,功能代碼徹底一致。也都實現了 Serializable 接口,可是反序列化時老是提示不成功。安全
解決:虛擬機是否容許反序列化,不只取決於類路徑和功能代碼是否一致,一個很是重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清單 1 中,雖然兩個類的功能代碼徹底一致,可是序列化 ID 不一樣,他們沒法相互序列化和反序列化。
清單 1. 相同功能代碼不一樣序列化 ID 的類對比服務器
package com.inout; import java.io.Serializable; public class A implements Serializable { private static final long serialVersionUID = 1L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } package com.inout; import java.io.Serializable; public class A implements Serializable { private static final long serialVersionUID = 2L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
序列化 ID 在 Eclipse 下提供了兩種生成策略,一個是固定的 1L,一個是隨機生成一個不重複的 long 類型數據(其實是使用 JDK 工具生成),在這裏有一個建議,若是沒有特殊需求,就是用默認的 1L 就能夠,這樣能夠確保代碼一致時反序列化成功。那麼隨機生成的序列化 ID 有什麼做用呢,有些時候,經過改變序列化 ID 能夠用來限制某些用戶的使用。網絡
靜態變量序列化
清單 2. 靜態變量序列化問題代碼工具
public class Test implements Serializable { private static final long serialVersionUID = 1L; public static int staticVar = 5; public static void main(String[] args) { try { //初始時staticVar爲5 ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("result.obj")); out.writeObject(new Test()); out.close(); //序列化後修改成10 Test.staticVar = 10; ObjectInputStream oin = new ObjectInputStream(new FileInputStream( "result.obj")); Test t = (Test) oin.readObject(); oin.close(); //再讀取,經過t.staticVar打印新的值 System.out.println(t.staticVar); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } }
清單 2 中的 main 方法,將對象序列化後,修改靜態變量的數值,再將序列化對象讀取出來,而後經過讀取出來的對象得到靜態變量的數值並打印出來。依照清單 2,這個 System.out.println(t.staticVar) 語句輸出的是 10 仍是 5 呢?this
最後的輸出是 10,對於沒法理解的讀者認爲,打印的 staticVar 是從讀取的對象裏得到的,應該是保存時的狀態纔對。之因此打印 10 的緣由在於序列化時,並不保存靜態變量,這其實比較容易理解,序列化保存的是對象的狀態,靜態變量屬於類的狀態,所以 序列化並不保存靜態變量。加密
對敏感字段加密
情境:服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,好比密碼字符串等,但願對該密碼字段在序列化時,進行加密,而客戶端若是擁有解密的密鑰,只有在客戶端進行反序列化時,才能夠對密碼進行讀取,這樣能夠必定程度保證序列化對象的數據安全。spa
解決:在序列化過程當中,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,若是沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法能夠容許用戶控制序列化的過程,好比能夠在序列化的過程當中動態改變序列化的數值。基於這個原理,能夠在實際應用中獲得使用,用於敏感字段的加密工做,清單 3 展現了這個過程。
清單 3. 靜態變量序列化問題代碼code
private static final long serialVersionUID = 1L; private String password = "pass"; public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } private void writeObject(ObjectOutputStream out) { try { PutField putFields = out.putFields(); System.out.println("原密碼:" + password); password = "encryption";//模擬加密 putFields.put("password", password); System.out.println("加密後的密碼" + password); out.writeFields(); } catch (IOException e) { e.printStackTrace(); } } private void readObject(ObjectInputStream in) { try { GetField readFields = in.readFields(); Object object = readFields.get("password", ""); System.out.println("要解密的字符串:" + object.toString()); password = "pass";//模擬解密,須要得到本地的密鑰 } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static void main(String[] args) { try { ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("result.obj")); out.writeObject(new Test()); out.close(); ObjectInputStream oin = new ObjectInputStream(new FileInputStream( "result.obj")); Test t = (Test) oin.readObject(); System.out.println("解密後的字符串:" + t.getPassword()); oin.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }
在清單 3 的 writeObject 方法中,對密碼進行了加密,在 readObject 中則對 password 進行解密,只有擁有密鑰的客戶端,才能夠正確的解析出密碼,確保了數據的安全。執行控制檯輸出如圖所示。
特性使用案例
RMI 技術是徹底基於 Java 序列化技術的,服務器端接口調用所須要的參數對象來至於客戶端,它們經過網絡相互傳輸。這就涉及 RMI 的安全傳輸的問題。一些敏感的字段,如用戶名密碼(用戶登陸時須要對密碼進行傳輸),咱們但願對其進行加密,這時,就能夠採用本節介紹的方法在客戶端對密碼進行加密,服務器端進行解密,確保數據傳輸的安全性。
序列化存儲規則
清單 4. 存儲規則問題代碼
ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("result.obj")); Test test = new Test(); //試圖將對象兩次寫入文件 out.writeObject(test); out.flush(); System.out.println(new File("result.obj").length()); out.writeObject(test); out.close(); System.out.println(new File("result.obj").length()); ObjectInputStream oin = new ObjectInputStream(new FileInputStream( "result.obj")); //從文件依次讀出兩個文件 Test t1 = (Test) oin.readObject(); Test t2 = (Test) oin.readObject(); oin.close(); //判斷兩個引用是否指向同一個對象 System.out.println(t1 == t2);
清單 3 中對同一對象兩次寫入文件,打印出寫入一次對象後的存儲大小和寫入兩次後的存儲大小,而後從文件中反序列化出兩個對象,比較這兩個對象是否爲同一對象。通常的思惟是,兩次寫入對象,文件大小會變爲兩倍的大小,反序列化時,因爲從文件讀取,生成了兩個對象,判斷相等時應該是輸入 false 纔對,可是最後結果輸出如圖所示。
31 36 true
咱們看到,第二次寫入對象時文件只增長了 5 字節,而且兩個對象是相等的,這是爲何呢?
解答:Java 序列化機制爲了節省磁盤空間,具備特定的存儲規則,當寫入文件的爲同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用,上面增長的 5 字節的存儲空間就是新增引用和一些控制信息的空間。反序列化時,恢復引用關係,使得清單 3 中的 t1 和 t2 指向惟一的對象,兩者相等,輸出 true。該存儲規則極大的節省了存儲空間。
特性案例分析
清單 5. 案例代碼
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("result.obj")); Test test = new Test(); test.i = 1; out.writeObject(test); out.flush(); test.i = 2; out.writeObject(test); out.close(); ObjectInputStream oin = new ObjectInputStream(new FileInputStream( "result.obj")); Test t1 = (Test) oin.readObject(); Test t2 = (Test) oin.readObject(); System.out.println(t1.i); System.out.println(t2.i);
本案例的目的是但願將 test 對象兩次保存到 result.obj 文件中,寫入一次之後修改對象屬性值再次保存第二次,而後從 result.obj 中再依次讀出兩個對象,輸出這兩個對象的 i 屬性值。案例代碼的目的本來是但願一次性傳輸對象修改先後的狀態。
結果兩個輸出的都是 1, 緣由就是第一次寫入對象之後,第二次再試圖寫的時候,虛擬機根據引用關係知道已經有一個相同對象已經寫入文件,所以只保存第二次寫的引用,因此讀取時,都是第一次保存的對象。讀者在使用一個文件屢次 writeObject 須要特別注意這個問題。
小結
本文經過幾個具體的情景,介紹了 Java 序列化的一些高級知識,雖然說高級,並非說讀者們都不瞭解,但願用筆者介紹的情景讓讀者加深印象,可以更加合理的利用 Java 序列化技術,在將來開發之路上遇到序列化問題時,能夠及時的解決。因爲本人知識水平有限,文章中假若有錯誤的地方,歡迎聯繫我批評指正。
原文出處:https://www.cnblogs.com/java-chen-hao/p/10401826.html