??正如在第85和86項中提到并貫穿本章的討論,實現Serializable的決定增加了出現bug和安全問題的可能性,因為它允許使用一種超語言機制來創建實例,而不是使用普通的構造函數。然而,有一種技術可以大大降低這些風險。這種技術稱為序列化代理模式。
??序列化代理模式相當簡單。首先,設計一個私有靜態嵌套類,它簡潔地表示封閉類實例的邏輯狀態。這個嵌套類稱為封閉類的序列化代理。它應該有一個構造函數,其參數類型是封閉類。這個構造函數只是從它的參數復制數據:它不需要做任何一致性檢查或防御性復制。按照設計,序列化代理的默認序列化形式是封閉類的完美序列化形式。必須聲明所包含的類及其序列化代理才能實現Serializable。
??例如,考慮第50項中編寫的不可變周期類,并在第88項中使其可序列化。這是該類的序列化代理。句點非常簡單,它的序列化代理具有與類完全相同的字段:
??接下來,將以下writeReplace方法添加到所包含的類中。這個方法可以逐字復制到任何類與序列化代理:
??該方法在封閉類上的存在導致序列化系統發出SerializationProxy實例,而不是封閉類的實例。換句話說,writeReplace方法在序列化之前將封閉類的實例轉換為其序列化代理。
??有了這個writeReplace方法,序列化系統將永遠不會生成封閉類的序列化實例,但是攻擊者可能會創建一個實例,試圖違反類的不變量。為了保證這樣的攻擊會失敗,只需將這個readObject方法添加到封閉的類中:
??最后,在SerializationProxy類上提供一個readResolve方法,該方法返回封閉類的邏輯等效實例。此方法的存在導致序列化系統在反序列化時將序列化代理轉換回封閉類的實例
??這個readResolve方法僅使用其公共API創建了一個封閉類的實例,這就是該模式的美妙之處。它在很大程度上消除了序列化的語言外特性,因為反序列化實例是使用與任何其他實例相同的構造函數、靜態工廠和方法創建的。這使您不必單獨確保反序列化的實例遵從類的不變量。如果類的靜態工廠或構造函數建立了這些不變量,而它的實例方法維護它們,那么您就確保了這些不變量也將通過序列化來維護。
??下面是Period的readResolve方法。SerializationProxy上圖:
??與防御性復制方法(第357頁)類似,序列化代理方法阻止偽字節流攻擊(第354頁)和內部字段盜竊攻擊(第356頁)。與前面的兩種方法不同,此方法允許句點字段為final,這是
句點類是真正不可變的(第17項)。與前兩種方法不同,這一種方法不需要太多的思考。你不用不得不找出哪些字段可能受到惡意序列化攻擊的危害,也不需要顯式地執行有效性檢查作為反序列化的一部分。
??序列化代理模式還有另一種方式比readObject中的防御性復制更強大。序列化代理模式允許反序列化實例具有與初始序列化實例不同的類。您可能不認為這在實踐中有用,但它確實有用.
??考慮EnumSet的情況(項目36)。該類沒有公共構造函數,只有靜態工廠。從客戶機的角度來看,它們返回EnumSet實例,但是在當前OpenJDK實現中,它們返回兩個子類中的一個,具體取決于底層enum類型的大小。如果底層enum類型有64個或更少的元素,則靜態工廠返回一個RegularEnumSet;否則,它們返回一個JumboEnumSet。
??現在考慮一下,如果序列化一個枚舉類型有60個元素的枚舉集,然后向枚舉類型添加5個以上的元素,然后反序列化枚舉集,會發生什么情況。序列化時它是RegularEnumSet實例,但反序列化后最好是JumboEnumSet實例。事實上正是這樣,因為EnumSet使用序列化代理模式。如果您好奇,這里是EnumSet的序列化代理。其實很簡單:
??序列化代理模式有兩個限制。它與用戶可擴展的類不兼容(第19項)。而且,它與一些對象圖包含循環的類不兼容:如果您試圖從對象的序列化代理的readResolve方法中調用對象上的方法,您將得到一個ClassCastException,因為您還沒有對象,只有對象的序列化代理。
??最后,序列化代理模式的附加功能和安全性不是免費的。在我的機器上,序列化和反序列化要貴14%
使用序列化代理的句點實例比使用防御性復制的句點實例要多。
??總之,當您發現必須在客戶端不可擴展的類上編寫readObject或writeObject方法時,請考慮序列化代理模式。這種模式可能是用非平凡不變量對對象進行魯棒序列化的最簡單方法。
本文寫于2019.7.24,歷時1天