ITEM 89: FOR INSTANCE CONTROL, PREFER ENUM TYPES TOREADRESOLVE
??item 3 描述了單例模式古沥,并給出了下面的單例類示例。這個類限制對其構(gòu)造函數(shù)的訪問,以確保只創(chuàng)建一個實例:
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { ... }
public void leaveTheBuilding() { ... } }
??如 item 3 所述恋谭,如果將實現(xiàn) Serializable 的單詞添加到它的聲明中扳缕,這個類將不再是一個單例。類使用默認的序列化形式還是自定義序列化形式(item 87)并不重要举畸,類是否提供顯式的 readObject 方法(item 88)也不重要查排。
??任何 readObject 方法,無論是顯式的還是默認的抄沮,都會返回一個新創(chuàng)建的實例跋核,它將不是在類初始化時創(chuàng)建的相同實例岖瑰。
??readResolve 特性允許您用另一個實例替換由 readObject [Serialization, 3.7]創(chuàng)建的實例。如果正在反序列化的對象的類使用適當?shù)穆暶鞫x了一個 readResolve 方法砂代,則在反序列化后在新創(chuàng)建的對象上調(diào)用該方法蹋订。然后,該方法返回的對象引用將代替新創(chuàng)建的對象返回刻伊。在此特性的大多數(shù)使用中露戒,不會保留對新創(chuàng)建對象的引用,因此它立即符合垃圾收集的條件捶箱。
如果 Elvis 類被用來實現(xiàn) Serializable智什,下面的讀取-解析方法足以保證單例屬性:
// readResolve for instance control - you can do better!
private Object readResolve() {
// Return the one true Elvis and let the garbage collector
// take care of the Elvis impersonator.
return INSTANCE;
}
??此方法忽略反序列化的對象,返回在初始化類時創(chuàng)建的 Elvis 實例丁屎。因此荠锭,Elvis 實例的序列化形式不需要包含任何實際數(shù)據(jù);所有實例字段都應該聲明為 transient晨川。實際上证九,如果您依賴于 readResolve 進行實例控制,那么具有對象引用類型的所有實例字段都必須聲明為 transient共虑。否則愧怜,一個確定的攻擊者可能會在反序列化對象的readResolve 方法運行之前保護對其的引用,使用的技術(shù)有點類似于item 88 中的MutablePeriod 攻擊妈拌。
??這種攻擊有點復雜拥坛,但基本原理很簡單。如果一個單例包含一個非瞬態(tài)對象引用字段供炎,該字段的內(nèi)容將在該單例的 readResolve 方法運行之前被反序列化渴逻。這允許一個精心設(shè)計的流在對象引用字段的內(nèi)容被反序列化時“偷取”一個對最初反序列化的單例的引用。
??下面是它的工作原理音诫。首先惨奕,編寫一個 “stealer” 類,該類擁有一個 readResolve 方法和一個實例字段竭钝,該實例字段引用了被 stealer “隱藏”的序列化單例梨撞。在序列化流中,用 stealer 的實例替換 singleton 的非瞬態(tài)字段∠愎蓿現(xiàn)在就有了一個循環(huán):單元素包含偷取者卧波,偷取者引用單元素。
??因為單例包含竊取器庇茫,所以竊取器的 readResolve 方法在反序列化單例時首先運行港粱。因此,當 stealer 的 readResolve 方法運行時,它的實例字段仍然引用部分反序列化(還沒有解析)的單例查坪。
??stealer 的 readResolve 方法將引用從它的實例字段復制到一個靜態(tài)字段中寸宏,以便在readResolve 方法運行后可以訪問該引用。然后偿曙,該方法為其隱藏的字段返回正確類型的值氮凝。如果不這樣做,那么當序列化系統(tǒng)試圖將 stealer 引用存儲到這個字段時望忆,VM 將拋出 ClassCastException罩阵。
??為了使其具體化,考慮以下破碎單例:
// Broken singleton - has nontransient object reference field!
public class Elvis implements Serializable {
public static final Elvis INSTANCE = new Elvis();
private Elvis() { }
private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
private Object readResolve() {
return INSTANCE;
}
}
??下面是一個“stealer”启摄,按照上面的描述構(gòu)造:
public class ElvisStealer implements Serializable {
static Elvis impersonator;
private Elvis payload;
private Object readResolve() {
// Save a reference to the "unresolved" Elvis instance
impersonator = payload;
// Return object of correct type for favoriteSongs field
return new String[] { "A Fool Such as I" };
}
private static final long serialVersionUID = 0;
}
??最后稿壁,這里有一個丑陋的程序,它反序列化一個手工生成的流鞋仍,以生成有缺陷的單例的兩個不同實例常摧。反序列化方法從這個程序中被省略了搅吁,因為它與第354頁上的方法相同:
public class ElvisImpersonator {
// Byte stream couldn't have come from a real Elvis instance!
private static final byte[] serializedForm = {
(byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x05, 0x45, 0x6c, 0x76, 0x69, 0x73, (byte)0x84, (byte)0xe6, (byte)0x93, 0x33, (byte)0xc3, (byte)0xf4, (byte)0x8b, 0x32, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x0d, 0x66, 0x61, 0x76, 0x6f, 0x72, 0x69, 0x74, 0x65, 0x53, 0x6f, 0x6e, 0x67, 0x73, 0x74, 0x00, 0x12, 0x4c, 0x6a, 0x61, 0x76, 0x61, 0x2f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x3b, 0x78, 0x70, 0x73, 0x72, 0x00, 0x0c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x53, 0x74, 0x65, 0x61, 0x6c, 0x65, 0x72, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0x4c, 0x00, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x74, 0x00, 0x07, 0x4c, 0x45, 0x6c, 0x76, 0x69, 0x73, 0x3b, 0x78, 0x70, 0x71, 0x00, 0x7e, 0x00, 0x02};
public static void main(String[] args) {
// Initializes ElvisStealer.impersonator and returns // the real Elvis (which is Elvis.INSTANCE)
Elvis elvis = (Elvis) deserialize(serializedForm);
Elvis impersonator = ElvisStealer.impersonator;
elvis.printFavorites();
impersonator.printFavorites();
}
}
??運行這個程序會產(chǎn)生以下輸出威创,最終證明有可能創(chuàng)建兩個不同的 Elvis 實例(具有不同的音樂品味):
[Hound Dog, Heartbreak Hotel]
[A Fool Such as I]
??你可以解決這個問題通過聲明 favoriteSongs 為 transient, 但你最好解決它通過使 Elvis 成為一個蛋元素的 enum類型(item 3)。ElvisStealer 為代表的攻擊,使用readResolve 方法防止“臨時”反序列化實例被攻擊者訪問是脆弱的,需要非常小心谎懦。
??如果您將可序列化的實例控制類編寫為枚舉肚豺,Java 保證除了聲明的常量之外不存在任何實例,除非攻擊者濫用了可訪問的特權(quán)方法(如AccessibleObject.setAccessible)界拦。任何可以這樣做的攻擊者已經(jīng)有足夠的特權(quán)來執(zhí)行任意的本地代碼吸申,所有的賭注都是無效的。
// Enum singleton - the preferred approach
public enum Elvis {
INSTANCE;
private String[] favoriteSongs = { "Hound Dog", "Heartbreak Hotel" };
public void printFavorites() {
System.out.println(Arrays.toString(favoriteSongs));
}
}
??對實例控件使用 readResolve 并沒有過時享甸。如果必須編寫一個可序列化的實例控制類截碴,其實例在編譯時是未知的,則不能將該類表示為 enum 類型蛉威。
??readResolve 的可訪問性很重要日丹。如果在 final 類上放置一個 readResolve 方法,那么它應該是私有的蚯嫌。如果將 readResolve 方法放在非 final 類上哲虾,則必須仔細考慮其可訪問性。如果它是私有的择示,它將不適用于任何子類束凑。如果它是包私有的,它將只應用于同一包中的子類栅盲。如果它是受保護的或公共的汪诉,它將應用于所有不覆蓋它的子類。如果 readResolve 方法是受保護的或公共的谈秫,而子類沒有覆蓋它扒寄,那么反序列化子類實例將產(chǎn)生超類實例拴签,這可能會導致 ClassCastException 異常。
??總之旗们,請盡可能使用枚舉類型來強制實例控制不變量蚓哩。如果這是不可能的,并且您需要一個既可序列化又受實例控制的類上渴,那么您必須提供一個 readResolve 方法岸梨,并確保類的所有實例字段都是原語或瞬態(tài)字段。