日韩久久久精品,亚洲精品久久久久久久久久久,亚洲欧美一区二区三区国产精品 ,一区二区福利

Java中的序列化Serialable高級詳解

系統(tǒng) 2182 0

引言

將 Java 對象序列化為二進制文件的 Java 序列化技術(shù)是 Java 系列技術(shù)中一個較為重要的技術(shù)點,在大部分情況下,開發(fā)人員只需要了解被序列化的類需要實現(xiàn) Serializable 接口,使用 ObjectInputStream 和 ObjectOutputStream 進行對象的讀寫。然而在有些情況下,光知道這些還遠(yuǎn)遠(yuǎn)不夠,文章列舉了筆者遇到的一些真實情境,它們與 Java 序列化相關(guān),通過分析情境出現(xiàn)的原因,使讀者輕松牢記 Java 序列化中的一些高級認(rèn)識。

文章結(jié)構(gòu)

本文將逐一的介紹幾個情境,順序如下面的列表。

  • 序列化 ID 的問題
  • 靜態(tài)變量序列化
  • 父類的序列化與 Transient 關(guān)鍵字
  • 對敏感字段加密
  • 序列化存儲規(guī)則

列表的每一部分講述了一個單獨的情境,讀者可以分別查看。

序列化 ID 問題

情境 :兩個客戶端 A 和 B 試圖通過網(wǎng)絡(luò)傳遞對象數(shù)據(jù),A 端將對象 C 序列化為二進制數(shù)據(jù)再傳給 B,B 反序列化得到 C。

問題 :C 對象的全類路徑假設(shè)為 com.inout.Test,在 A 和 B 端都有這么一個類文件,功能代碼完全一致。也都實現(xiàn)了 Serializable 接口,但是反序列化時總是提示不成功。

解決 虛擬機是否允許反序列化,不僅取決于類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L) 。清單 1 中,雖然兩個類的功能代碼完全一致,但是序列化 ID 不同,他們無法相互序列化和反序列化。

簡單來說,Java的序列化機制是通過在運行時判斷類的 serialVersionUID 來驗證版本一致性的。在進行反序列化時,JVM會把傳來的字節(jié)流中的serialVersionUID與本地相應(yīng)實體(類)的 serialVersionUID 進行比較,如果相同就認(rèn)為是一致的,可以進行反序列化,否則就會出現(xiàn)序列化版本不一致的異常。

當(dāng)實現(xiàn)java.io.Serializable接口的實體(類)沒有顯式地定義一個名為 serialVersionUID ,類型為 long 的變量時,Java序列化機制會根據(jù)編譯的class自動生成一個 serialVersionUID 作序列化版本比較用,這種情況下,只有同一次編譯生成的class才會生成相同的 serialVersionUID 。

如果我們不希望通過編譯來強制劃分軟件版本,即實現(xiàn)序列化接口的實體能夠兼容先前版本,未作更改的類,就需要顯式地定義一個名為 serialVersionUID ,類型為 long 的變量,不修改這個變量值的序列化實體都可以相互進行串行化和反串行化。
清單 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,一個是隨機生成一個不重復(fù)的 long 類型數(shù)據(jù)(實際上是使用 JDK 工具生成),在這里有一個建議,如果沒有特殊需求,就是用默認(rèn)的 1L 就可以,這樣可以確保代碼一致時反序列化成功。那么隨機生成的序列化 ID 有什么作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。

特性使用案例

讀者應(yīng)該聽過 Fa?ade 模式,它是為應(yīng)用程序提供統(tǒng)一的訪問接口,案例程序中的 Client 客戶端使用了該模式,案例程序結(jié)構(gòu)圖如圖 1 所示。

圖 1. 案例程序結(jié)構(gòu)
圖 1. 案例程序結(jié)構(gòu)

Client 端通過 Fa?ade Object 才可以與業(yè)務(wù)邏輯對象進行交互。而客戶端的 Fa?ade Object 不能直接由 Client 生成,而是需要 Server 端生成,然后序列化后通過網(wǎng)絡(luò)將二進制對象數(shù)據(jù)傳給 Client,Client 負(fù)責(zé)反序列化得到 Fa?ade 對象。該模式可以使得 Client 端程序的使用需要服務(wù)器端的許可,同時 Client 端和服務(wù)器端的 Fa?ade Object 類需要保持一致。當(dāng)服務(wù)器端想要進行版本更新時,只要將服務(wù)器端的 Fa?ade Object 類的序列化 ID 再次生成,當(dāng) Client 端反序列化 Fa?ade Object 就會失敗,也就是強制 Client 端從服務(wù)器端獲取最新程序。

靜態(tài)變量序列化

情境 :查看清單 2 的代碼。

清單 2. 靜態(tài)變量序列化問題代碼
       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 方法,將對象序列化后,修改靜態(tài)變量的數(shù)值,再將序列化對象讀取出來,然后通過讀取出來的對象獲得靜態(tài)變量的數(shù)值并打印出來。依照清單 2,這個 System.out.println(t.staticVar) 語句輸出的是 10 還是 5 呢?

最后的輸出是 10,對于無法理解的讀者認(rèn)為,打印的 staticVar 是從讀取的對象里獲得的,應(yīng)該是保存時的狀態(tài)才對。之所以打印 10 的原因在于序列化時,并不保存靜態(tài)變量,這其實比較容易理解,序列化保存的是對象的狀態(tài),靜態(tài)變量屬于類的狀態(tài),因此 序列化并不保存靜態(tài)變量 。

父類的序列化與 Transient 關(guān)鍵字

情境 :一個子類實現(xiàn)了 Serializable 接口,它的父類都沒有實現(xiàn) Serializable 接口,序列化該子類對象,然后反序列化后輸出父類定義的某變量的數(shù)值,該變量數(shù)值與序列化時的數(shù)值不同。

解決 要想將父類對象也序列化,就需要讓父類也實現(xiàn) Serializable 接口 。如果父類不實現(xiàn)的話的,就 需要有默認(rèn)的無參的構(gòu)造函數(shù) 。在父類沒有實現(xiàn) Serializable 接口時,虛擬機是不會序列化父對象的,而一個 Java 對象的構(gòu)造必須先有父對象,才有子對象,反序列化也不例外。所以反序列化時,為了構(gòu)造父對象,只能調(diào)用父類的無參構(gòu)造函數(shù)作為默認(rèn)的父對象。因此當(dāng)我們?nèi)「笇ο蟮淖兞恐禃r,它的值是調(diào)用父類無參構(gòu)造函數(shù)后的值。如果你考慮到這種序列化的情況,在父類無參構(gòu)造函數(shù)中對變量進行初始化,否則的話,父類變量值都是默認(rèn)聲明的值,如 int 型的默認(rèn)是 0,string 型的默認(rèn)是 null。

Transient 關(guān)鍵字的作用是控制變量的序列化,在變量聲明前加上該關(guān)鍵字,可以阻止該變量被序列化到文件中,在被反序列化后,transient 變量的值被設(shè)為初始值,如 int 型的是 0,對象型的是 null。

特性使用案例

我們熟悉使用 Transient 關(guān)鍵字可以使得字段不被序列化,那么還有別的方法嗎?根據(jù)父類對象序列化的規(guī)則,我們可以將不需要被序列化的字段抽取出來放到父類中,子類實現(xiàn) Serializable 接口,父類不實現(xiàn),根據(jù)父類序列化規(guī)則,父類的字段數(shù)據(jù)將不被序列化,形成類圖如圖 2 所示。

圖 2. 案例程序類圖
圖 2. 案例程序類圖

上圖中可以看出,attr1、attr2、attr3、attr5 都不會被序列化,放在父類中的好處在于當(dāng)有另外一個 Child 類時,attr1、attr2、attr3 依然不會被序列化,不用重復(fù)抒寫 transient,代碼簡潔。

對敏感字段加密

情境 :服務(wù)器端給客戶端發(fā)送序列化對象數(shù)據(jù),對象中有一些數(shù)據(jù)是敏感的,比如密碼字符串等,希望對該密碼字段在序列化時,進行加密,而客戶端如果擁有解密的密鑰,只有在客戶端進行反序列化時,才可以對密碼進行讀取,這樣可以一定程度保證序列化對象的數(shù)據(jù)安全。

解決 :在序列化過程中,虛擬機會試圖調(diào)用對象類里的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化,如果沒有這樣的方法,則默認(rèn)調(diào)用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在序列化的過程中動態(tài)改變序列化的數(shù)值。基于這個原理,可以在實際應(yīng)用中得到使用,用于敏感字段的加密工作,清單 3 展示了這個過程。

清單 3. 靜態(tài)變量序列化問題代碼
       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 進行解密,只有擁有密鑰的客戶端,才可以正確的解析出密碼,確保了數(shù)據(jù)的安全。執(zhí)行清單 3 后控制臺輸出如圖 3 所示。

圖 3. 數(shù)據(jù)加密演示
圖 3. 數(shù)據(jù)加密演示

特性使用案例

RMI 技術(shù)是完全基于 Java 序列化技術(shù)的,服務(wù)器端接口調(diào)用所需要的參數(shù)對象來至于客戶端,它們通過網(wǎng)絡(luò)相互傳輸。這就涉及 RMI 的安全傳輸?shù)膯栴}。一些敏感的字段,如用戶名密碼(用戶登錄時需要對密碼進行傳輸),我們希望對其進行加密,這時,就可以采用本節(jié)介紹的方法在客戶端對密碼進行加密,服務(wù)器端進行解密,確保數(shù)據(jù)傳輸?shù)陌踩浴?

序列化存儲規(guī)則

情境 :問題代碼如清單 4 所示。

清單 4. 存儲規(guī)則問題代碼
       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 中對同一對象兩次寫入文件,打印出寫入一次對象后的存儲大小和寫入兩次后的存儲大小,然后從文件中反序列化出兩個對象,比較這兩個對象是否為同一對象。一般的思維是,兩次寫入對象,文件大小會變?yōu)閮杀兜拇笮。葱蛄谢瘯r,由于從文件讀取,生成了兩個對象,判斷相等時應(yīng)該是輸入 false 才對,但是最后結(jié)果輸出如圖 4 所示。

圖 4. 示例程序輸出
圖 4. 示例程序輸出

我們看到,第二次寫入對象時文件只增加了 5 字節(jié),并且兩個對象是相等的,這是為什么呢?

解答 :Java 序列化機制為了節(jié)省磁盤空間,具有特定的存儲規(guī)則,當(dāng)寫入文件的為同一對象時,并不會再將對象的內(nèi)容進行存儲,而只是再次存儲一份引用,上面增加的 5 字節(jié)的存儲空間就是新增引用和一些控制信息的空間。反序列化時,恢復(fù)引用關(guān)系,使得清單 3 中的 t1 和 t2 指向唯一的對象,二者相等,輸出 true。該存儲規(guī)則極大的節(jié)省了存儲空間。

特性案例分析

查看清單 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 屬性值。案例代碼的目的原本是希望一次性傳輸對象修改前后的狀態(tài)。

結(jié)果兩個輸出的都是 1, 原因就是第一次寫入對象以后,第二次再試圖寫的時候,虛擬機根據(jù)引用關(guān)系知道已經(jīng)有一個相同對象已經(jīng)寫入文件,因此只保存第二次寫的引用,所以讀取時,都是第一次保存的對象。讀者在使用一個文件多次 writeObject 需要特別注意這個問題。

小結(jié)

本文通過幾個具體的情景,介紹了 Java 序列化的一些高級知識,雖說高級,并不是說讀者們都不了解,希望用筆者介紹的情景讓讀者加深印象,能夠更加合理的利用 Java 序列化技術(shù),在未來開發(fā)之路上遇到序列化問題時,可以及時的解決。由于本人知識水平有限,文章中倘若有錯誤的地方,歡迎聯(lián)系我批評指正。

Java中的序列化Serialable高級詳解


更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯(lián)系: 360901061

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦?。?!

發(fā)表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 巴彦淖尔市| 泗水县| 武宣县| 天长市| 葫芦岛市| 涿鹿县| 佛坪县| 聂拉木县| 乌鲁木齐市| 阳西县| 沐川县| 新宾| 务川| 科尔| 宣威市| 赤水市| 五华县| 嘉善县| 习水县| 通辽市| 垫江县| 大同县| 遂宁市| 应用必备| 德昌县| 交口县| 揭阳市| 永靖县| 临漳县| 治县。| 施甸县| 柏乡县| 吉林省| 海安县| 田林县| 蓬莱市| 谢通门县| 临猗县| 迁西县| 宁武县| 新兴县|