Java中的序列化

主要談一談Java中的序列化問題,包括Serializable與Externalizable介紹以及一些項目中的用法。

1、序列化是什麼意思?用來幹嘛的

      序列化:將對象的狀態信息轉換爲可以存儲或傳輸的形式的過程。不過存儲倒是很少見,工作中大多都是傳輸。比如遠程
方法調用就是用到的特別多。差不多就相當於科幻片中的那種將固體液化,順着水管流到某個地方然後在固化。那麼液化的過
程就是類似於序列化,固化的過程就是反序列化。
      通常開發人員只需要瞭解被序列化的類需要實現 Serializable 接口,使用 ObjectInputStream 和 ObjectOutputStream 進行
對象的讀寫。然而在有些情況下,光知道這些還遠遠不夠。因爲,你的眼界決定了項目的高度!

2、Java中提供的默認序列化Serializable

我們先寫一個User類,然後主要通過代碼說明下。User類的主要結構如下,主要就三個字段,id,name,passwd
package com.ztesoft.ser;

public class User {
	private int id;
	
	private String name;

	private String passwd;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getPasswd() {
		return passwd;
	}

	public void setPasswd(String passwd) {
		this.passwd = passwd;
	}
	
}
接着我們寫個測試類,將這個對象序列化到磁盤中
@Test
public void serializWrite(){
	User user = new User(1, "dgh", "123456");
		
	File file = new File("D:/user.info");
	ObjectOutputStream oos = null;
	try {
		oos = new ObjectOutputStream(new FileOutputStream(file));
			
		oos.writeObject(user);
			
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		IOUtils.closeQuietly(oos);
	}
}
接着就報了一個異常:java.io.NotSerializableException: com.ztesoft.ser.User
所以我們得到第一個結論:如果使用默認的序列化,則需要序列化的對象就需要直接或者間接的實現Serializable接口。
現在我們給User類添加接口實現,然後在調用方法,則正常運行。User這個對象被序列化到硬盤上了。但是有個警告:

下面就主要說說這個序列化ID。

2.1、序列化ID的問題 serialVersionUID

      有一個異常叫做:java.io.InvalidClassException:  stream classdesc serialVersionUID = 5835067920559730690, 
local class serialVersionUID = 583506792055970690這個異常發生在反序列化的時候,爲什麼有這個異常呢
      因爲:虛擬機是否允許反序列化,不僅取決於類路徑和功能代碼是否一致,一個非常重要的一點是兩個類的序列化 ID 是
否一致(就是 private static final long serialVersionUID = 1L)。由於序列化 ID 不同,他們無法相互序列化和反序列化。也
就導致了上面類似的異常拋出。
      序列化 ID 在 Eclipse 下提供了兩種生成策略,一個是固定的 1L,一個是隨機生成一個不重複的 long 類型數據(實際上
是使用 JDK 工具生成),在這裏有一個建議,如果沒有特殊需求,就是用默認的 1L 就可以,這樣可以確保代碼一致時反序
列化成功。那麼隨機生成的序列化 ID 有什麼作用呢,有些時候,通過改變序列化 ID 可以用來限制某些用戶的使用。比如:
修改了服務端類的序列化id之後,只有與服務端保持一致序列化id的那些客戶端才能調用。不過一般沒有人這麼做。

2.2、靜態變量的序列化

第一步:我們修改User,添加一個成員變量。就叫做state吧,static修飾的哦。

第二步:寫一個測試類,先序列化到硬盤,在反序列化到回來但是發現了一個嚴重問題:

      這是爲什麼呢?對於無法理解的讀者認爲,state 是從讀取的對象裏獲得的,應該是保存時的狀態纔對啊,我保存的時
候是error那麼序列化回來應該也是error,爲什麼就成了ok了呢?之所以變成ok的原因在於序列化時,並不保存靜態變量,
這其實比較容易理解,序列化保存的是對象的狀態,靜態變量屬於類的狀態,因此 序列化並不保存靜態變量。

2.3、父類序列化與transient關鍵字

我們先說一下transient關鍵字,我們在passwd字段前面加上這個關鍵字修飾,然後在反序列化回來

      很神奇吧,passwd應該是123456的,但是這裏卻沒有了。其實transient這個關鍵字的作用就是使字段不被序列化
我們熟悉使用 Transient 關鍵字可以使得字段不被序列化,那麼還有別的方法嗎?根據父類對象序列化的規則,我們可以
將不需要被序列化的字段抽取出來放到父類中,子類實現 Serializable 接口,父類不實現,根據父類序列化規則,父類的
字段數據將不被序列化。所以父類序列化的代碼就不演示了。

2.4、對敏感字段的加密

      我們可不可以有什麼辦法自定義序列化的方式呢?比如序列化時給字段加密,這樣別人也不容易破解了。其實是有的:
在序列化過程中,虛擬機會試圖調用對象類裏的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化。
      如果沒有這樣的方法,則默認調用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 
defaultReadObject 方法。用戶自定義的 writeObject 和 readObject 方法可以允許用戶控制序列化的過程,比如可以在
序列化的過程中動態改變序列化的數值。基於這個原理,可以在實際應用中得到使用,用於敏感字段的加密工作。
修改User代碼,添加兩個方法:writeObject 和readObject
private void writeObject(ObjectOutputStream out) {
	try {
		// 這裏可以加密
		out.writeObject(passwd);
	} catch (IOException e) {
		e.printStackTrace();
	}
}

private void readObject(ObjectInputStream in) {
	try {
		// 這裏就可以解密了
		passwd = (String) in.readObject();
	} catch (Exception e) {
		e.printStackTrace();
	}
}
之後我們調用測試方法,發現結果是:
      state因爲是靜態的,有值是正常的,id與name因爲我們自定義的方法中麼有處理,因此就沒有值。也就是說如果我們寫了
writeObject與readObject,那麼序列化與反序列話就會使用我們自定義的方法。
      RMI 技術是完全基於 Java 序列化技術的,服務器端接口調用所需要的參數對象來至於客戶端,它們通過網絡相互傳輸。這就
涉及 RMI 的安全傳輸的問題。一些敏感的字段,如用戶名密碼(用戶登錄時需要對密碼進行傳輸),我們希望對其進行加密,
這時,就可以採用本節介紹的方法在客戶端對密碼進行加密,服務器端進行解密,確保數據傳輸的安全性。

2.6、單例模式與序列化

第一步:首先寫一個單例類
package com.ztesoft.ser;

public class Singleton implements java.io.Serializable {
	/** */
	private static final long serialVersionUID = 780762366800963430L;

	public static Singleton INSTANCE = new Singleton();

	// 私有構造器
	private Singleton() { }
}
第二步:寫個測試方法,測試一下看看結果
public void singletonTest(){
	Singleton s1 = Singleton.INSTANCE;
		
	File file = new File("D:/singleton.info");
	ObjectOutputStream oos = null;
	ObjectInputStream ois = null;
	try {
		// 序列化對象到硬盤
		oos = new ObjectOutputStream(new FileOutputStream(file));
		oos.writeObject(s1);
			
		// 反序列化
		byte[] bytes = IOUtils.toByteArray(new FileInputStream(file));
	        ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes));
	        Singleton s2 = (Singleton) in.readObject();
	        
		System.out.println(s1 == s2);
	} catch (Exception e) {
		e.printStackTrace();
	} finally {
		IOUtils.closeQuietly(oos);
		IOUtils.closeQuietly(ois);
	}
}
第三步:發現控制檯輸出的是false,也就是說單例類被破壞了。
爲了避免這中情況,我們需要在需要序列化的類中添加一個方法 readResolve
這個時候在跑一下測試代碼,看看是否有問題

3、Externalizable接口的使用

jdk還給我們提供了另外一種自定義序列化的藉口。那就是Externalizable啦,首先我們看看源碼:
public interface Externalizable extends java.io.Serializable {
	
    void writeExternal(ObjectOutput out) throws IOException;
    
    void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}

      這個接口繼承了Serializable接口,並有兩個方法,一個write一個read。其實和我們的readObject與writeObject一樣的啦。
個人覺得只是更方便了而已,有興趣可以研究下他們之間的差別。我是沒研究。

4、題外話

      java雖然提供了很好的序列化,但是序列化之後的文件還是比較大的。目前很多開源的項目爲了提高效率都使用更好的
替代方法,比如protobuf等。以後有機會在介紹這個東西吧。
發佈了54 篇原創文章 · 獲贊 67 · 訪問量 48萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章