??正如在第85和86項(xiàng)中提到并貫穿本章的討論,實(shí)現(xiàn)Serializable的決定增加了出現(xiàn)bug和安全問題的可能性,因?yàn)樗试S使用一種超語言機(jī)制來創(chuàng)建實(shí)例,而不是使用普通的構(gòu)造函數(shù)。然而,有一種技術(shù)可以大大降低這些風(fēng)險(xiǎn)。這種技術(shù)稱為序列化代理模式。
??序列化代理模式相當(dāng)簡(jiǎn)單。首先,設(shè)計(jì)一個(gè)私有靜態(tài)嵌套類,它簡(jiǎn)潔地表示封閉類實(shí)例的邏輯狀態(tài)。這個(gè)嵌套類稱為封閉類的序列化代理。它應(yīng)該有一個(gè)構(gòu)造函數(shù),其參數(shù)類型是封閉類。這個(gè)構(gòu)造函數(shù)只是從它的參數(shù)復(fù)制數(shù)據(jù):它不需要做任何一致性檢查或防御性復(fù)制。按照設(shè)計(jì),序列化代理的默認(rèn)序列化形式是封閉類的完美序列化形式。必須聲明所包含的類及其序列化代理才能實(shí)現(xiàn)Serializable。
??例如,考慮第50項(xiàng)中編寫的不可變周期類,并在第88項(xiàng)中使其可序列化。這是該類的序列化代理。句點(diǎn)非常簡(jiǎn)單,它的序列化代理具有與類完全相同的字段:

??接下來,將以下writeReplace方法添加到所包含的類中。這個(gè)方法可以逐字復(fù)制到任何類與序列化代理:

??該方法在封閉類上的存在導(dǎo)致序列化系統(tǒng)發(fā)出SerializationProxy實(shí)例,而不是封閉類的實(shí)例。換句話說,writeReplace方法在序列化之前將封閉類的實(shí)例轉(zhuǎn)換為其序列化代理。
??有了這個(gè)writeReplace方法,序列化系統(tǒng)將永遠(yuǎn)不會(huì)生成封閉類的序列化實(shí)例,但是攻擊者可能會(huì)創(chuàng)建一個(gè)實(shí)例,試圖違反類的不變量。為了保證這樣的攻擊會(huì)失敗,只需將這個(gè)readObject方法添加到封閉的類中:

??最后,在SerializationProxy類上提供一個(gè)readResolve方法,該方法返回封閉類的邏輯等效實(shí)例。此方法的存在導(dǎo)致序列化系統(tǒng)在反序列化時(shí)將序列化代理轉(zhuǎn)換回封閉類的實(shí)例
??這個(gè)readResolve方法僅使用其公共API創(chuàng)建了一個(gè)封閉類的實(shí)例,這就是該模式的美妙之處。它在很大程度上消除了序列化的語言外特性,因?yàn)榉葱蛄谢瘜?shí)例是使用與任何其他實(shí)例相同的構(gòu)造函數(shù)、靜態(tài)工廠和方法創(chuàng)建的。這使您不必單獨(dú)確保反序列化的實(shí)例遵從類的不變量。如果類的靜態(tài)工廠或構(gòu)造函數(shù)建立了這些不變量,而它的實(shí)例方法維護(hù)它們,那么您就確保了這些不變量也將通過序列化來維護(hù)。
??下面是Period的readResolve方法。SerializationProxy上圖:

??與防御性復(fù)制方法(第357頁)類似,序列化代理方法阻止偽字節(jié)流攻擊(第354頁)和內(nèi)部字段盜竊攻擊(第356頁)。與前面的兩種方法不同,此方法允許句點(diǎn)字段為final,這是
句點(diǎn)類是真正不可變的(第17項(xiàng))。與前兩種方法不同,這一種方法不需要太多的思考。你不用不得不找出哪些字段可能受到惡意序列化攻擊的危害,也不需要顯式地執(zhí)行有效性檢查作為反序列化的一部分。
??序列化代理模式還有另一種方式比readObject中的防御性復(fù)制更強(qiáng)大。序列化代理模式允許反序列化實(shí)例具有與初始序列化實(shí)例不同的類。您可能不認(rèn)為這在實(shí)踐中有用,但它確實(shí)有用.
??考慮EnumSet的情況(項(xiàng)目36)。該類沒有公共構(gòu)造函數(shù),只有靜態(tài)工廠。從客戶機(jī)的角度來看,它們返回EnumSet實(shí)例,但是在當(dāng)前OpenJDK實(shí)現(xiàn)中,它們返回兩個(gè)子類中的一個(gè),具體取決于底層enum類型的大小。如果底層enum類型有64個(gè)或更少的元素,則靜態(tài)工廠返回一個(gè)RegularEnumSet;否則,它們返回一個(gè)JumboEnumSet。
??現(xiàn)在考慮一下,如果序列化一個(gè)枚舉類型有60個(gè)元素的枚舉集,然后向枚舉類型添加5個(gè)以上的元素,然后反序列化枚舉集,會(huì)發(fā)生什么情況。序列化時(shí)它是RegularEnumSet實(shí)例,但反序列化后最好是JumboEnumSet實(shí)例。事實(shí)上正是這樣,因?yàn)镋numSet使用序列化代理模式。如果您好奇,這里是EnumSet的序列化代理。其實(shí)很簡(jiǎn)單:

??序列化代理模式有兩個(gè)限制。它與用戶可擴(kuò)展的類不兼容(第19項(xiàng))。而且,它與一些對(duì)象圖包含循環(huán)的類不兼容:如果您試圖從對(duì)象的序列化代理的readResolve方法中調(diào)用對(duì)象上的方法,您將得到一個(gè)ClassCastException,因?yàn)槟€沒有對(duì)象,只有對(duì)象的序列化代理。
??最后,序列化代理模式的附加功能和安全性不是免費(fèi)的。在我的機(jī)器上,序列化和反序列化要貴14%
使用序列化代理的句點(diǎn)實(shí)例比使用防御性復(fù)制的句點(diǎn)實(shí)例要多。
??總之,當(dāng)您發(fā)現(xiàn)必須在客戶端不可擴(kuò)展的類上編寫readObject或writeObject方法時(shí),請(qǐng)考慮序列化代理模式。這種模式可能是用非平凡不變量對(duì)對(duì)象進(jìn)行魯棒序列化的最簡(jiǎn)單方法。
本文寫于2019.7.24,歷時(shí)1天