歡迎您光臨本站 註冊首頁

Java深度歷險:Java對象序列化與RMI

←手機掃碼閱讀     火星人 @ 2014-03-09 , reply:0

  對於一個存在於Java虛擬機中的對象來說,其內部的狀態只保持在內存中.JVM停止之後,這些狀態就丟失了.在很多情況下,對象的內部狀態是需要被持久化下來的.提到持久化,最直接的做法是保存到文件系統或是資料庫之中.這種做法一般涉及到自定義存儲格式以及繁瑣的數據轉換.對象關係映射(Object-relational mapping)是一種典型的用關係資料庫來持久化對象的方式,也存在很多直接存儲對象的對象資料庫.對象序列化機制(object serialization)是Java語言內建的一種對象持久化方式,可以很容易的在JVM中的活動對象和位元組數組(流)之間進行轉換.除了可以很簡單的實現持久化之外,序列化機制的另外一個重要用途是在遠程方法調用中,用來對開發人員屏蔽底層實現細節.
  基本的對象序列化
  由於Java提供了良好的默認支持,實現基本的對象序列化是件比較簡單的事.待序列化的Java類只需要實現Serializable介面即可.Serializable僅是一個標記介面,並不包含任何需要實現的具體方法.實現該介面只是為了聲明該Java類的對象是可以被序列化的.實際的序列化和反序列化工作是通過ObjectOuputStream和ObjectInputStream來完成的.ObjectOutputStream的writeObject方法可以把一個Java對象寫入到流中,ObjectInputStream的readObject方法可以從流中讀取一個Java對象.在寫入和讀取的時候,雖然用的參數或返回值是單個對象,但實際上操縱的是一個對象圖,包括該對象所引用的其它對象,以及這些對象所引用的另外的對象.Java會自動幫你遍歷對象圖並逐個序列化.除了對象之外,Java中的基本類型和數組也是可以通過 ObjectOutputStream和ObjectInputStream來序列化的.

  上面的代碼給出了典型的把Java對象序列化之後保存到磁碟上,以及從磁碟上讀取的基本方式. User類只是聲明了實現Serializable介面.
  在默認的序列化實現中,Java對象中的非靜態和非瞬時域都會被包括進來,而與域的可見性聲明沒有關係.這可能會導致某些不應該出現的域被包含在序列化之後的位元組數組中,比如密碼等隱私信息.由於Java對象序列化之後的格式是固定的,其它人可以很容易的從中分析出其中的各種信息.對於這種情況,一種解決辦法是把域聲明為瞬時的,即使用transient關鍵詞.另外一種做法是添加一個serialPersistentFields? 域來聲明序列化時要包含的域.從這裡可以看到在Java序列化機制中的這種僅在書面層次上定義的契約.聲明序列化的域必須使用固定的名稱和類型.在後面還可以看到其它類似這樣的契約.雖然Serializable只是一個標記介面,但它其實是包含有不少隱含的要求.下面的代碼給出了 serialPersistentFields的聲明示例,即只有firstName這個域是要被序列化的.




  自定義對象序列化
  基本的對象序列化機制讓開發人員可以在包含哪些域上進行定製.如果想對序列化的過程進行更加細粒度的控制,就需要在類中添加writeObject和對應的 readObject方法.這兩個方法屬於前面提到的序列化機制的隱含契約的一部分.在通過ObjectOutputStream的 writeObject方法寫入對象的時候,如果這個對象的類中定義了writeObject方法,就會調用該方法,並把當前 ObjectOutputStream對象作為參數傳遞進去.writeObject方法中一般會包含自定義的序列化邏輯,比如在寫入之前修改域的值,或是寫入額外的數據等.對於writeObject中添加的邏輯,在對應的readObject中都需要反轉過來,與之對應.
  在添加自己的邏輯之前,推薦的做法是先調用Java的默認實現.在writeObject方法中通過ObjectOutputStream的defaultWriteObject來完成,在readObject方法則通過ObjectInputStream的defaultReadObject來實現.下面的代碼在對象的序列化流中寫入了一個額外的字元串.

  序列化時的對象替換
  在有些情況下,可能會希望在序列化的時候使用另外一個對象來代替當前對象.其中的動機可能是當前對象中包含了一些不希望被序列化的域,比如這些域都是從另外一個域派生而來的;也可能是希望隱藏實際的類層次結構;還有可能是添加自定義的對象管理邏輯,如保證某個類在JVM中只有一個實例.相對於把無關的域都設成transient來說,使用對象替換是一個更好的選擇,提供了更多的靈活性.替換對象的作用類似於Java EE中會使用到的傳輸對象(Transfer Object).
  考慮下面的例子,一個訂單系統中需要把訂單的相關信息序列化之後,通過網路來傳輸.訂單類Order引用了客戶類Customer.在默認序列化的情況下,Order類對象被序列化的時候,其引用的Customer類對象也會被序列化,這可能會造成用戶信息的泄露.對於這種情況,可以創建一個另外的對象來在序列化的時候替換當前的Order類的對象,並把用戶信息隱藏起來.

  這個替換對象類OrderReplace只保存了Order的ID.在Order類的writeReplace方法中返回了一個OrderReplace對象.這個對象會被作為替代寫入到流中.同樣的,需要在OrderReplace類中定義一個readResolve方法,用來在讀取的時候再轉換回 Order類對象.這樣對調用者來說,替換對象的存在就是透明的.



  序列化與對象創建
  在通過ObjectInputStream的readObject方法讀取到一個對象之後,這個對象是一個新的實例,但是其構造方法是沒有被調用的,其中的域的初始化代碼也沒有被執行.對於那些沒有被序列化的域,在新創建出來的對象中的值都是默認的.也就是說,這個對象從某種角度上來說是不完備的.這有可能會造成一些隱含的錯誤.調用者並不知道對象是通過一般的new操作符來創建的,還是通過反序列化所得到的.解決的辦法就是在類的readObject方法裡面,再執行所需的對象初始化邏輯.對於一般的Java類來說,構造方法中包含了初始化的邏輯.可以把這些邏輯提取到一個方法中,在readObject方法中調用此方法.
  版本更新
  把一個Java對象序列化之後,所得到的位元組數組一般會保存在磁碟或資料庫之中.在保存完成之後,有可能原來的Java類有了更新,比如添加了額外的域.這個時候從兼容性的角度出發,要求仍然能夠讀取舊版本的序列化數據.在讀取的過程中,當ObjectInputStream發現一個對象的定義的時候,會嘗試在當前JVM中查找其Java類定義.這個查找過程不能僅根據Java類的全名來判斷,當前JVM中可能存在名稱相同,但是含義完全不同的Java 類.這個對應關係是通過一個全局惟一標識符serialVersionUID來實現的.通過在實現了Serializable介面的類中定義該域,就聲明了該Java類的一個惟一的序列化版本號.JVM會比對從位元組數組中得出的類的版本號,與JVM中查找到的類的版本號是否一致,來決定兩個類是否是兼容的.對於開發人員來說,需要記得的就是在實現了Serializable介面的類中定義這樣的一個域,並在版本更新過程中保持該值不變.當然,如果不希望維持這種向後兼容性,換一個版本號即可.該域的值一般是綜合Java類的各個特性而計算出來的一個哈希值,可以通過Java提供的serialver命令來生成.在Eclipse中,如果Java類實現了Serializable介面,Eclipse會提示並幫你生成這個serialVersionUID.
  在類版本更新的過程中,某些操作會破壞向後兼容性.如果希望維持這種向後兼容性,就需要格外的注意.一般來說,在新的版本中添加東西不會產生什麼問題,而去掉一些域則是不行的.
  序列化安全性
  前面提到,Java對象序列化之後的內容格式是公開的.可以很容易的從中提取出各種信息.從實現的角度來說,可以從不同的層次來加強序列化的安全性.
  對序列化之後的流進行加密.這可以通過CipherOutputStream來實現.


  實現自己的writeObject和readObject方法,在調用defaultWriteObject之前,先對要序列化的域的值進行加密處理.
  使用一個SignedObject或SealedObject來封裝當前對象,用SignedObject或SealedObject進行序列化.
  在從流中進行反序列化的時候,可以通過ObjectInputStream的registerValidation方法添加ObjectInputValidation介面的實現,用來驗證反序列化之後得到的對象是否合法.
  RMI
  RMI(Remote Method Invocation)是Java中的遠程過程調用(Remote Procedure Call,RPC)實現,是一種分散式Java應用的實現方式.它的目的在於對開發人員屏蔽橫跨不同JVM和網路連接等細節,是的分佈在不同JVM上的對象像是存在於一個統一的JVM中一樣,可以很方便的互相通訊.之在介紹對象序列化之後來介紹RMI,主要是對象序列化機制是的RMI非常簡單.調用一個遠程伺服器上的方法並不是一件困難的事情.開發人員可以基於Apache MINA或是Netty這樣的框架來寫自己的網路伺服器,亦或是可以採用REST架構風格來編寫HTTP服務.但這些解決方案中,不可迴避的一個部分就是數據的編排和解排(marshal/unmarshal).需要在Java對象和傳輸格式之間進行互相轉換,這一部分邏輯是開發人員無法迴避的.RMI的優勢在於依靠Java序列化機制,對開發人員屏蔽了數據編排和解排的細節,要做的事情非常少.JDK 5之後,RMI通過動態代理機制去掉了早期版本中需要通過工具進行代碼生成的繁瑣方式,使用起來更加簡單.
  RMI採用的是典型的客戶端-伺服器端架構.需要定義的是伺服器端的遠程介面,這一步是設計好伺服器端需要提供什麼樣的服務.對遠程介面的要求很簡單,只需要繼承自RMI中的Remote介面即可.Remote和Serializable一樣,也是標記介面.遠程介面中的方法需要拋出RemoteException.定義好遠程介面之後,實現該介面即可.如下面的Calculator是一個簡單的遠程介面.

  實現了遠程介面的類的實例稱為遠程對象.創建出遠程對象之後,需要把它註冊到一個註冊表之中.這是為了客戶端能夠找到該遠程對象並調用.

  CalculatorServer是遠程對象的Java類.在它的start方法中通過UnicastRemoteObject的exportObject把當前對象暴露出來,是的它可以接收來自客戶端的調用請求.再通過Registry的rebind方法進行註冊,是的客戶端可以查找到.


  客戶端的實現就是從註冊表中查找到遠程介面的實現對象,再調用相應的方法即可.實際的調用雖然是在伺服器端完成的,但是在客戶端看來,這個介面中的方法就好像是在當前JVM中一樣.這就是RMI的強大之處.

  在運行的時候,需要通過rmiregistry命令來啟動RMI中用到的註冊表伺服器.
  為了通過Java的序列化機制來進行傳輸,遠程介面中的方法的參數和返回值,要麼是Java的基本類型,要麼是遠程對象,要麼是實現了 Serializable介面的Java類.當客戶端通過RMI註冊表找到一個遠程介面的時候,所得到的其實是遠程介面的一個動態代理對象.當客戶端調用其中的方法的時候,方法的參數對象會在序列化之後,傳輸到伺服器端.伺服器端接收到之後,進行反序列化得到參數對象.並使用這些參數對象,在伺服器端調用實際的方法.調用的返回值Java對象經過序列化之後,再發送回客戶端.客戶端再經過反序列化之後得到Java對象,返回給調用者.這中間的序列化過程對於使用者來說是透明的,由動態代理對象自動完成.除了序列化之外,RMI還使用了動態類載入技術.當需要進行反序列化的時候,如果該對象的類定義在當前JVM中沒有找到,RMI會嘗試從遠端下載所需的類文件定義.可以在RMI程序啟動的時候,通過JVM參數java.rmi.server.codebase來指定動態下載Java類文件的URL.


[火星人 ] Java深度歷險:Java對象序列化與RMI已經有411次圍觀

http://coctec.com/docs/java/show-post-59931.html