熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

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

2022-06-13   來源: Java核心技術 

  對於一個存在於Java虛擬機中的對象來說其內部的狀態只保持在內存中JVM停止之後這些狀態就丟失了在很多情況下對象的內部狀態是需要被持久化下來的提到持久化最直接的做法是保存到文件系統或是數據庫之中這種做法一般涉及到自定義存儲格式以及繁瑣的數據轉換對象關系映射(Objectrelational 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 CallRPC)實現是一種分布式Java應用的實現方式它的目的在於對開發人員屏蔽橫跨不同JVM和網絡連接等細節使得分布在不同JVM上的對象像是存在於一個統一的JVM中一樣可以很方便的互相通訊之所以在介紹對象序列化之後來介紹RMI主要是因為對象序列化機制使得RMI非常簡單調用一個遠程服務器上的方法並不是一件困難的事情開發人員可以基於Apache MINA或是Netty這樣的框架來寫自己的網絡服務器亦或是可以采用REST架構風格來編寫HTTP服務但這些解決方案中不可回避的一個部分就是數據的編排和解排(marshal/unmarshal)需要在Java對象和傳輸格式之間進行互相轉換而且這一部分邏輯是開發人員無法回避的RMI的優勢在於依靠Java序列化機制對開發人員屏蔽了數據編排和解排的細節要做的事情非常少JDK 之後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參數debase來指定動態下載Java類文件的URL


From:http://tw.wingwit.com/Article/program/Java/hx/201311/25699.html
  • 上一篇文章:

  • 下一篇文章:
  • 推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.