對于一個存在于Java虛擬機中的對象來說,其內(nèi)部的狀態(tài)只保持在內(nèi)存中。JVM停止之后,這些狀態(tài)就丟失了。在很多情況下,對象的內(nèi)部狀態(tài)是需要被持久化下來的。提到持久化,最直接的做法是保存到文件系統(tǒng)或是數(shù)據(jù)庫之中。這種做法一般涉及到自定義存儲格式以及繁瑣的數(shù)據(jù)轉(zhuǎn)換。對象關(guān)系映射(Object-relational mapping)是一種典型的用關(guān)系數(shù)據(jù)庫來持久化對象的方式,也存在很多直接存儲對象的對象數(shù)據(jù)庫。對象序列化機制(object serialization)是Java語言內(nèi)建的一種對象持久化方式,可以很容易的在JVM中的活動對象和字節(jié)數(shù)組(流)之間進(jìn)行轉(zhuǎn)換。除了可以很簡單的實現(xiàn)持久化之外,序列化機制的另外一個重要用途是在遠(yuǎn)程方法調(diào)用中,用來對開發(fā)人員屏蔽底層實現(xiàn)細(xì)節(jié)。
由于Java提供了良好的默認(rèn)支持,實現(xiàn)基本的對象序列化是件比較簡單的事。待序列化的Java類只需要實現(xiàn)Serializable接口即可。Serializable僅是一個標(biāo)記接口,并不包含任何需要實現(xiàn)的具體方法。實現(xiàn)該接口只是為了聲明該Java類的對象是可以被序列化的。實際的序列化和反序列化工作是通過ObjectOuputStream和ObjectInputStream來完成的。ObjectOutputStream的writeObject方法可以把一個Java對象寫入到流中,ObjectInputStream的readObject方法可以從流中讀取一個Java對象。在寫入和讀取的時候,雖然用的參數(shù)或返回值是單個對象,但實際上操縱的是一個對象圖,包括該對象所引用的其它對象,以及這些對象所引用的另外的對象。Java會自動幫你遍歷對象圖并逐個序列化。除了對象之外,Java中的基本類型和數(shù)組也是可以通過 ObjectOutputStream和ObjectInputStream來序列化的。
try { User user = new User("Alex", "Cheng"); ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("user.bin")); output.writeObject(user); output.close(); } catch (IOException e) { e.printStackTrace(); } try { ObjectInputStream input = new ObjectInputStream(new FileInputStream("user.bin")); User user = (User) input.readObject(); System.out.println(user); } catch (Exception e) { e.printStackTrace(); }
上面的代碼給出了典型的把Java對象序列化之后保存到磁盤上,以及從磁盤上讀取的基本方式。 User類只是聲明了實現(xiàn)Serializable接口。
在默認(rèn)的序列化實現(xiàn)中,Java對象中的非靜態(tài)和非瞬時域都會被包括進(jìn)來,而與域的可見性聲明沒有關(guān)系。這可能會導(dǎo)致某些不應(yīng)該出現(xiàn)的域被包含在序列化之后的字節(jié)數(shù)組中,比如密碼等隱私信息。由于Java對象序列化之后的格式是固定的,其它人可以很容易的從中分析出其中的各種信息。對于這種情況,一種解決辦法是把域聲明為瞬時的,即使用transient關(guān)鍵詞。另外一種做法是添加一個serialPersistentFields? 域來聲明序列化時要包含的域。從這里可以看到在Java序列化機制中的這種僅在書面層次上定義的契約。聲明序列化的域必須使用固定的名稱和類型。在后面還可以看到其它類似這樣的契約。雖然Serializable只是一個標(biāo)記接口,但它其實是包含有不少隱含的要求。下面的代碼給出了 serialPersistentFields的聲明示例,即只有firstName這個域是要被序列化的。
private static final ObjectStreamField[] serialPersistentFields = { new ObjectStreamField("firstName", String.class) };
自定義對象序列化
基本的對象序列化機制讓開發(fā)人員可以在包含哪些域上進(jìn)行定制。如果想對序列化的過程進(jìn)行更加細(xì)粒度的控制,就需要在類中添加writeObject和對應(yīng)的 readObject方法。這兩個方法屬于前面提到的序列化機制的隱含契約的一部分。在通過ObjectOutputStream的 writeObject方法寫入對象的時候,如果這個對象的類中定義了writeObject方法,就會調(diào)用該方法,并把當(dāng)前 ObjectOutputStream對象作為參數(shù)傳遞進(jìn)去。writeObject方法中一般會包含自定義的序列化邏輯,比如在寫入之前修改域的值,或是寫入額外的數(shù)據(jù)等。對于writeObject中添加的邏輯,在對應(yīng)的readObject中都需要反轉(zhuǎn)過來,與之對應(yīng)。
在添加自己的邏輯之前,推薦的做法是先調(diào)用Java的默認(rèn)實現(xiàn)。在writeObject方法中通過ObjectOutputStream的defaultWriteObject來完成,在readObject方法則通過ObjectInputStream的defaultReadObject來實現(xiàn)。下面的代碼在對象的序列化流中寫入了一個額外的字符串。
private void writeObject(ObjectOutputStream output) throws IOException { output.defaultWriteObject(); output.writeUTF("Hello World"); } private void readObject(ObjectInputStream input) throws IOException, ClassNotFoundException { input.defaultReadObject(); String value = input.readUTF(); System.out.println(value); }
序列化時的對象替換
在有些情況下,可能會希望在序列化的時候使用另外一個對象來代替當(dāng)前對象。其中的動機可能是當(dāng)前對象中包含了一些不希望被序列化的域,比如這些域都是從另外一個域派生而來的;也可能是希望隱藏實際的類層次結(jié)構(gòu);還有可能是添加自定義的對象管理邏輯,如保證某個類在JVM中只有一個實例。相對于把無關(guān)的域都設(shè)成transient來說,使用對象替換是一個更好的選擇,提供了更多的靈活性。替換對象的作用類似于Java EE中會使用到的傳輸對象(Transfer Object)。
考慮下面的例子,一個訂單系統(tǒng)中需要把訂單的相關(guān)信息序列化之后,通過網(wǎng)絡(luò)來傳輸。訂單類Order引用了客戶類Customer。在默認(rèn)序列化的情況下,Order類對象被序列化的時候,其引用的Customer類對象也會被序列化,這可能會造成用戶信息的泄露。對于這種情況,可以創(chuàng)建一個另外的對象來在序列化的時候替換當(dāng)前的Order類的對象,并把用戶信息隱藏起來。
private static class OrderReplace implements Serializable { private static final long serialVersionUID = 4654546423735192613L; private String orderId; public OrderReplace(Order order) { this.orderId = order.getId(); } private Object readResolve() throws ObjectStreamException { //根據(jù)orderId查找Order對象并返回 } }
這個替換對象類OrderReplace只保存了Order的ID。在Order類的writeReplace方法中返回了一個OrderReplace對象。這個對象會被作為替代寫入到流中。同樣的,需要在OrderReplace類中定義一個readResolve方法,用來在讀取的時候再轉(zhuǎn)換回 Order類對象。這樣對調(diào)用者來說,替換對象的存在就是透明的。
private Object writeReplace() throws ObjectStreamException { return new OrderReplace(this); }
序列化與對象創(chuàng)建
在通過ObjectInputStream的readObject方法讀取到一個對象之后,這個對象是一個新的實例,但是其構(gòu)造方法是沒有被調(diào)用的,其中的域的初始化代碼也沒有被執(zhí)行。對于那些沒有被序列化的域,在新創(chuàng)建出來的對象中的值都是默認(rèn)的。也就是說,這個對象從某種角度上來說是不完備的。這有可能會造成一些隱含的錯誤。調(diào)用者并不知道對象是通過一般的new操作符來創(chuàng)建的,還是通過反序列化所得到的。解決的辦法就是在類的readObject方法里面,再執(zhí)行所需的對象初始化邏輯。對于一般的Java類來說,構(gòu)造方法中包含了初始化的邏輯。可以把這些邏輯提取到一個方法中,在readObject方法中調(diào)用此方法。
版本更新
把一個Java對象序列化之后,所得到的字節(jié)數(shù)組一般會保存在磁盤或數(shù)據(jù)庫之中。在保存完成之后,有可能原來的Java類有了更新,比如添加了額外的域。這個時候從兼容性的角度出發(fā),要求仍然能夠讀取舊版本的序列化數(shù)據(jù)。在讀取的過程中,當(dāng)ObjectInputStream發(fā)現(xiàn)一個對象的定義的時候,會嘗試在當(dāng)前JVM中查找其Java類定義。這個查找過程不能僅根據(jù)Java類的全名來判斷,因為當(dāng)前JVM中可能存在名稱相同,但是含義完全不同的Java 類。這個對應(yīng)關(guān)系是通過一個全局惟一標(biāo)識符serialVersionUID來實現(xiàn)的。通過在實現(xiàn)了Serializable接口的類中定義該域,就聲明了該Java類的一個惟一的序列化版本號。JVM會比對從字節(jié)數(shù)組中得出的類的版本號,與JVM中查找到的類的版本號是否一致,來決定兩個類是否是兼容的。對于開發(fā)人員來說,需要記得的就是在實現(xiàn)了Serializable接口的類中定義這樣的一個域,并在版本更新過程中保持該值不變。當(dāng)然,如果不希望維持這種向后兼容性,換一個版本號即可。該域的值一般是綜合Java類的各個特性而計算出來的一個哈希值,可以通過Java提供的serialver命令來生成。在Eclipse中,如果Java類實現(xiàn)了Serializable接口,Eclipse會提示并幫你生成這個serialVersionUID。
在類版本更新的過程中,某些操作會破壞向后兼容性。如果希望維持這種向后兼容性,就需要格外的注意。一般來說,在新的版本中添加?xùn)|西不會產(chǎn)生什么問題,而去掉一些域則是不行的。
序列化安全性
前面提到,Java對象序列化之后的內(nèi)容格式是公開的。所以可以很容易的從中提取出各種信息。從實現(xiàn)的角度來說,可以從不同的層次來加強序列化的安全性。
- 對序列化之后的流進(jìn)行加密。這可以通過CipherOutputStream來實現(xiàn)。
- 實現(xiàn)自己的writeObject和readObject方法,在調(diào)用defaultWriteObject之前,先對要序列化的域的值進(jìn)行加密處理。
- 使用一個SignedObject或SealedObject來封裝當(dāng)前對象,用SignedObject或SealedObject進(jìn)行序列化。
- 在從流中進(jìn)行反序列化的時候,可以通過ObjectInputStream的registerValidation方法添加ObjectInputValidation接口的實現(xiàn),用來驗證反序列化之后得到的對象是否合法。
RMI
RMI(Remote Method Invocation)是Java中的遠(yuǎn)程過程調(diào)用(Remote Procedure Call,RPC)實現(xiàn),是一種分布式Java應(yīng)用的實現(xiàn)方式。它的目的在于對開發(fā)人員屏蔽橫跨不同JVM和網(wǎng)絡(luò)連接等細(xì)節(jié),使得分布在不同JVM上的對象像是存在于一個統(tǒng)一的JVM中一樣,可以很方便的互相通訊。之所以在介紹對象序列化之后來介紹RMI,主要是因為對象序列化機制使得RMI非常簡單。調(diào)用一個遠(yuǎn)程服務(wù)器上的方法并不是一件困難的事情。開發(fā)人員可以基于Apache MINA或是Netty這樣的框架來寫自己的網(wǎng)絡(luò)服務(wù)器,亦或是可以采用REST架構(gòu)風(fēng)格來編寫HTTP服務(wù)。但這些解決方案中,不可回避的一個部分就是數(shù)據(jù)的編排和解排(marshal/unmarshal)。需要在Java對象和傳輸格式之間進(jìn)行互相轉(zhuǎn)換,而且這一部分邏輯是開發(fā)人員無法回避的。RMI的優(yōu)勢在于依靠Java序列化機制,對開發(fā)人員屏蔽了數(shù)據(jù)編排和解排的細(xì)節(jié),要做的事情非常少。JDK 5之后,RMI通過動態(tài)代理機制去掉了早期版本中需要通過工具進(jìn)行代碼生成的繁瑣方式,使用起來更加簡單。
RMI采用的是典型的客戶端-服務(wù)器端架構(gòu)。首先需要定義的是服務(wù)器端的遠(yuǎn)程接口,這一步是設(shè)計好服務(wù)器端需要提供什么樣的服務(wù)。對遠(yuǎn)程接口的要求很簡單,只需要繼承自RMI中的Remote接口即可。Remote和Serializable一樣,也是標(biāo)記接口。遠(yuǎn)程接口中的方法需要拋出RemoteException。定義好遠(yuǎn)程接口之后,實現(xiàn)該接口即可。如下面的Calculator是一個簡單的遠(yuǎn)程接口。
public interface Calculator extends Remote { String calculate(String expr) throws RemoteException; }
實現(xiàn)了遠(yuǎn)程接口的類的實例稱為遠(yuǎn)程對象。創(chuàng)建出遠(yuǎn)程對象之后,需要把它注冊到一個注冊表之中。這是為了客戶端能夠找到該遠(yuǎn)程對象并調(diào)用。
public class CalculatorServer implements Calculator { public String calculate(String expr) throws RemoteException { return expr; } public void start() throws RemoteException, AlreadyBoundException { Calculator stub = (Calculator) UnicastRemoteObject.exportObject(this, 0); Registry registry = LocateRegistry.getRegistry(); registry.rebind("Calculator", stub); } }
CalculatorServer是遠(yuǎn)程對象的Java類。在它的start方法中通過UnicastRemoteObject的exportObject把當(dāng)前對象暴露出來,使得它可以接收來自客戶端的調(diào)用請求。再通過Registry的rebind方法進(jìn)行注冊,使得客戶端可以查找到。
客戶端的實現(xiàn)就是首先從注冊表中查找到遠(yuǎn)程接口的實現(xiàn)對象,再調(diào)用相應(yīng)的方法即可。實際的調(diào)用雖然是在服務(wù)器端完成的,但是在客戶端看來,這個接口中的方法就好像是在當(dāng)前JVM中一樣。這就是RMI的強大之處。
public class CalculatorClient { public void calculate(String expr) { try { Registry registry = LocateRegistry.getRegistry("localhost"); Calculator calculator = (Calculator) registry.lookup("Calculator"); String result = calculator.calculate(expr); System.out.println(result); } catch (Exception e) { e.printStackTrace(); } } }
在運行的時候,需要首先通過rmiregistry命令來啟動RMI中用到的注冊表服務(wù)器。
為了通過Java的序列化機制來進(jìn)行傳輸,遠(yuǎn)程接口中的方法的參數(shù)和返回值,要么是Java的基本類型,要么是遠(yuǎn)程對象,要么是實現(xiàn)了 Serializable接口的Java類。當(dāng)客戶端通過RMI注冊表找到一個遠(yuǎn)程接口的時候,所得到的其實是遠(yuǎn)程接口的一個動態(tài)代理對象。當(dāng)客戶端調(diào)用其中的方法的時候,方法的參數(shù)對象會在序列化之后,傳輸?shù)椒?wù)器端。服務(wù)器端接收到之后,進(jìn)行反序列化得到參數(shù)對象。并使用這些參數(shù)對象,在服務(wù)器端調(diào)用實際的方法。調(diào)用的返回值Java對象經(jīng)過序列化之后,再發(fā)送回客戶端。客戶端再經(jīng)過反序列化之后得到Java對象,返回給調(diào)用者。這中間的序列化過程對于使用者來說是透明的,由動態(tài)代理對象自動完成。除了序列化之外,RMI還使用了動態(tài)類加載技術(shù)。當(dāng)需要進(jìn)行反序列化的時候,如果該對象的類定義在當(dāng)前JVM中沒有找到,RMI會嘗試從遠(yuǎn)端下載所需的類文件定義。可以在RMI程序啟動的時候,通過JVM參數(shù)java.rmi.server.codebase來指定動態(tài)下載Java類文件的URL。
參考資料
轉(zhuǎn)載自:infoq