序列化 ID 問題java
情境:兩個客戶端 A 和 B 試圖經過網絡傳遞對象數據,A 端將對象 C 序列化爲二進制數據再傳給 B,B 反序列化獲得 C。安全
問題:C 對象的全類路徑假設爲 com.inout.Test,在 A 和 B 端都有這麼一個類文件,功能代碼徹底一致。也都實現了 Serializable 接口,可是反序列化時老是提示不成功。服務器
解決:虛擬機是否容許反序列化,不只取決於類路徑和功能代碼是否一致,一個很是重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清單 1 中,雖然兩個類的功能代碼徹底一致,可是序列化 ID 不一樣,他們沒法相互序列化和反序列化。網絡
清單 1. 相同功能代碼不一樣序列化 ID 的類對比ide
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 能夠用來限制某些用戶的使用。函數
特性使用案例工具
讀者應該聽過 Façade 模式,它是爲應用程序提供統一的訪問接口,案例程序中的 Client 客戶端使用了該模式,案例程序結構圖如圖 1 所示。this
圖 1. 案例程序結構
加密
Client 端經過 Façade Object 才能夠與業務邏輯對象進行交互。而客戶端的 Façade Object 不能直接由 Client 生成,而是須要 Server 端生成,而後序列化後經過網絡將二進制對象數據傳給 Client,Client 負責反序列化獲得 Façade 對象。該模式可使得 Client 端程序的使用須要服務器端的許可,同時 Client 端和服務器端的 Façade Object 類須要保持一致。當服務器端想要進行版本更新時,只要將服務器端的 Façade Object 類的序列化 ID 再次生成,當 Client 端反序列化 Façade Object 就會失敗,也就是強制 Client 端從服務器端獲取最新程序。spa
回頁首
靜態變量序列化
情境:查看清單 2 的代碼。
清單 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 呢?
最後的輸出是 10,對於沒法理解的讀者認爲,打印的 staticVar 是從讀取的對象裏得到的,應該是保存時的狀態纔對。之因此打印 10 的緣由在於序列化時,並不保存靜態變量,這其實比較容易理解,序列化保存的是對象的狀態,靜態變量屬於類的狀態,所以 序列化並不保存靜態變量。
回頁首
父類的序列化與 Transient 關鍵字
情境:一個子類實現了 Serializable 接口,它的父類都沒有實現 Serializable 接口,序列化該子類對象,而後反序列化後輸出父類定義的某變量的數值,該變量數值與序列化時的數值不一樣。
解決:要想將父類對象也序列化,就須要讓父類也實現Serializable 接口。若是父類不實現的話的,就 須要有默認的無參的構造函數。在父類沒有實現 Serializable 接口時,虛擬機是不會序列化父對象的,而一個 Java 對象的構造必須先有父對象,纔有子對象,反序列化也不例外。因此反序列化時,爲了構造父對象,只能調用父類的無參構造函數做爲默認的父對象。所以當咱們取父對象的變量值時,它的值是調用父類無參構造函數後的值。若是你考慮到這種序列化的狀況,在父類無參構造函數中對變量進行初始化,不然的話,父類變量值都是默認聲明的值,如 int 型的默認是 0,string 型的默認是 null。
Transient 關鍵字的做用是控制變量的序列化,在變量聲明前加上該關鍵字,能夠阻止該變量被序列化到文件中,在被反序列化後,transient 變量的值被設爲初始值,如 int 型的是 0,對象型的是 null。
特性使用案例
咱們熟悉使用 Transient 關鍵字可使得字段不被序列化,那麼還有別的方法嗎?根據父類對象序列化的規則,咱們能夠將不須要被序列化的字段抽取出來放到父類中,子類實現 Serializable 接口,父類不實現,根據父類序列化規則,父類的字段數據將不被序列化,造成類圖如圖 2 所示。
圖 2. 案例程序類圖
上圖中能夠看出,attr一、attr二、attr三、attr5 都不會被序列化,放在父類中的好處在於當有另一個 Child 類時,attr一、attr二、attr3 依然不會被序列化,不用重複抒寫 transient,代碼簡潔。
回頁首
對敏感字段加密
情境:服務器端給客戶端發送序列化對象數據,對象中有一些數據是敏感的,好比密碼字符串等,但願對該密碼字段在序列化時,進行加密,而客戶端若是擁有解密的密鑰,只有在客戶端進行反序列化時,才能夠對密碼進行讀取,這樣能夠必定程度保證序列化對象的數據安全。
解決:在序列化過程當中,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,若是沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法能夠容許用戶控制序列化的過程,好比能夠在序列化的過程當中動態改變序列化的數值。基於這個原理,能夠在實際應用中獲得使用,用於敏感字段的加密工做,清單 3 展現了這個過程。
清單 3. 靜態變量序列化問題代碼
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 進行解密,只有擁有密鑰的客戶端,才能夠正確的解析出密碼,確保了數據的安全。執行清單 3 後控制檯輸出如圖 3 所示。
圖 3. 數據加密演示
特性使用案例
RMI 技術是徹底基於 Java 序列化技術的,服務器端接口調用所須要的參數對象來至於客戶端,它們經過網絡相互傳輸。這就涉及 RMI 的安全傳輸的問題。一些敏感的字段,如用戶名密碼(用戶登陸時須要對密碼進行傳輸),咱們但願對其進行加密,這時,就能夠採用本節介紹的方法在客戶端對密碼進行加密,服務器端進行解密,確保數據傳輸的安全性。
回頁首
序列化存儲規則
情境:問題代碼如清單 4 所示。
清單 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 纔對,可是最後結果輸出如圖 4 所示。
圖 4. 示例程序輸出
咱們看到,第二次寫入對象時文件只增長了 5 字節,而且兩個對象是相等的,這是爲何呢?
解答:Java 序列化機制爲了節省磁盤空間,具備特定的存儲規則,當寫入文件的爲同一對象時,並不會再將對象的內容進行存儲,而只是再次存儲一份引用,上面增長的 5 字節的存儲空間就是新增引用和一些控制信息的空間。反序列化時,恢復引用關係,使得清單 3 中的 t1 和 t2 指向惟一的對象,兩者相等,輸出 true。該存儲規則極大的節省了存儲空間。
特性案例分析
查看清單 5 的代碼。
清單 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); |
清單 4 的目的是但願將 test 對象兩次保存到 result.obj 文件中,寫入一次之後修改對象屬性值再次保存第二次,而後從 result.obj 中再依次讀出兩個對象,輸出這兩個對象的 i 屬性值。案例代碼的目的本來是但願一次性傳輸對象修改先後的狀態。
結果兩個輸出的都是 1, 緣由就是第一次寫入對象之後,第二次再試圖寫的時候,虛擬機根據引用關係知道已經有一個相同對象已經寫入文件,所以只保存第二次寫的引用,因此讀取時,都是第一次保存的對象。讀者在使用一個文件屢次 writeObject 須要特別注意這個問題。