??項目3描述了單例模式,并給出了下面的單例類示例。該類限制對其構造函數的訪問,以確保只創建一個實例:
??如第3項所述,如果將單詞implementation Serializable添加到該類的聲明中,該類將不再是單例的。類使用默認序列化表單還是自定義序列化表單并不重要 類是否提供顯式的readObject方法并不重要( item88)。任何readObject方法,不管是顯式的還是默認的,都會返回一個新創建的實例,這個實例與類初始化時創建的實例不同。
??readResolve特性允許您用另一個實例替換readObject [Serialization, 3.7]創建的實例。如果正在反序列化的對象的類使用正確的聲明定義了readResolve方法,則在新創建的對象反序列化之后,將在該對象上調用該方法。該方法返回的對象引用將代替新創建的對象返回。在該特性的大多數使用中,不保留對新創建對象的引用,因此它立即就有資格進行垃圾收集。
??如果Elvis類是用來實現序列化的,下面的readResolve方法就足以保證單例屬性:
??此方法忽略反序列化對象,返回初始化類時創建的區分的Elvis實例。因此,Elvis實例的序列化形式不需要包含任何實際數據;所有實例字段都應該聲明為transient。事實上,你依賴于readResolve控制實例,所有具有對象引用類型的實例字段都必須聲明為transient。否則,有決心的攻擊者有可能在運行反序列化對象的readResolve方法之前保護對該對象的引用,使用的技術有點類似于第88項中的MutablePeriod攻擊
??攻擊有點復雜,但其基本思想很簡單。如果單例包含一個nontransient對象引用字段,則在運行單例的readResolve方法之前,將對該字段的內容進行反序列化。這允許一個精心設計的流在對象引用字段的內容被反序列化時“竊取”對原來反序列化的單例對象的引用。
??下面是它的工作原理。首先,編寫一個“stealer”類,該類具有readResolve方法和一個實例字段,該實例字段引用序列化的單例,其中stealer“隱藏”在其中。在序列化流中,用一個stealer實例替換單例的非瞬態字段。現在您有了一個循環性:單例包含了竊取器,而竊取器引用了單例。
??:因為單例包含竊取器,所以當反序列化單例時,竊取器的readResolve方法首先運行。因此,當偷取器的readResolve方法運行時,它的實例字段仍然引用部分反序列化(且尚未解析)的單例。
??:竊取者的readResolve方法將引用從其實例字段復制到靜態字段,以便在readResolve方法運行后訪問引用。然后,該方法為其隱藏的字段返回正確類型的值。如果不這樣做,當序列化系統試圖將竊取器引用存儲到該字段時,VM將拋出ClassCastException。
??要使其具體化,請考慮以下破損的單例:
??這里是一個“小偷”類,按照上面的描述構造:
??最后,這是一個丑陋的程序,它反序列化了一個手工制作的流,以生成有缺陷的單例的兩個不同實例。這個程序省略了反序列化方法,因為它與第354頁的方法相同:
??運行此程序將生成以下輸出,最終證明可以創建兩個不同的Elvis實例(具有不同的音樂品味):
??您可以通過聲明favoriteSongs字段transient來修復這個問題,但是您最好通過將Elvis設置為單元素枚舉類型來修復它
(第3項).正如ElvisStealer攻擊所證明的,使用readResolve方法來防止攻擊者訪問“臨時”反序列化實例是脆弱的,需要非常小心。
??如果您將可序列化的實例控制類編寫為enum, Java保證除了聲明的常量之外不能有任何實例,除非攻擊者濫用了特權方法,如AccessibleObject.setAccessible。
任何能夠做到這一點的攻擊者都已經擁有足夠的特權來執行任意的本地代碼,所有的賭注都是不可能的。
??使用readResolve(例如實例控件)并不過時。如果必須編寫一個可序列化的實例控制類,而該類的實例在編譯時是未知的,則不能將該類表示為enum類型。
??重新解析的可訪問性非常重要。如果您將readResolve方法放在最后一個類上,那么它應該是私有的。如果將readResolve方法放在非final類上,必須仔細考慮其可訪問性。如果它是私有的,它將不應用于任何子類。如果它是包私有的,它將只應用于同一包中的子類。如果它是受保護的或公共的,它將應用于不覆蓋它的所有子類。如果readResolve方法是受保護的或公共的,而子類沒有覆蓋它,反序列化子類實例將生成超類實例,這可能會導致ClassCastException
??總之,使用枚舉類型在可能的情況下強制實例控制不變量。如果這是不可能的,并且您需要一個既可序列化又實例控制的類,那么您必須提供一個readResolve方法,并確保該類的所有實例字段都是原始的或transient的。
本文寫于2019.7.24,歷時1天