單例模式,我想大家再熟悉不過了,不過本文不是介紹單例模式該怎麼寫的。
本文來說說怎麼破壞一個單例,讓你寫的單例變成一個假的單例。當然,本文也會給出怎麼進行防守的方法。
一個簡單的單例
來一個簡單的單例模式例子:
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private String name; public String getName() { return this.name;
} private Singleton() { this.name = "Neo";
} public static Singleton getInstance() { return INSTANCE;
}
}
上面是一個比較簡單的餓漢寫法的單例模式,我們看看客戶端調用:
public class APP { // 由於構造方法上加了 private 修飾,所以我們已經不能通過 ‘new’ 來產生實例了
// Singleton intance = new Singleton();
Singleton instance = Singleton.getInstance();
System.out.println(instance.getName());
}
通過反射破壞單例
原理很簡單,通過反射獲取其構造方法,然後重新生成一個實例。
class APP { public static void main(String[] args) throws Exception {
Singleton instance1 = Singleton.getInstance(); // 下面我們通過反射得到其構造方法,並且修改其構造方法的訪問權限,並用這個構造方法構造一個對象
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance2 = (Singleton) constructor.newInstance(); // 是不是產生了兩個實例了?
System.out.println(instance1 == instance2); // false
}
}
顯然,說好的單例已經不單一了,上面的程序運行結果肯定是:false
防止反射方式破壞
如果要避免單例被反射破壞,Java 提供了枚舉,舉個例子:
public enum Singleton {
INSTANCE;// 這裡只有一項
private String name;
Singleton() { this.name = "Neo";
} public static Singleton getInstance() { return INSTANCE;
} public String getName() { return this.name;
}
}
這個時候,如果我們再想通過反射獲取類的構造方法:
Constructor constructor = Singleton.class.getDeclaredConstructor();
會拋出 NoSuchMethodException 異常:
Exception in thread "main" java.lang.NoSuchMethodException: com.javadoop.Singleton.
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.javadoop.singleton.APP.main(APP.java:11)
對於枚舉,JVM 會自動進行實例的創建,其構造方法由 JVM 在創建實例的時候進行調用。
我們在代碼中是獲取不到 enum 類的構造方法的。
通過序列化破壞
下面,我們再說說另一種破解方法:序列化、反序列化。
我們知道,序列化是將 java 對象轉換為字節流,反序列化是從字節流轉換為 java 對象。
class APP { public static void main(String[] args) throws ... {
Singleton instance1 = Singleton.getInstance();
Constructor constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton instance2 = (Singleton) constructor.newInstance(); // instance3 將從 instance1 序列化後,反序列化而來
Singleton instance3 = null;
ByteArrayOutputStream bout = null;
ObjectOutputStream out = null; try {
bout = new ByteArrayOutputStream();
out = new ObjectOutputStream(bout);
out.writeObject(instance1);
ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
ObjectInputStream in = new ObjectInputStream(bin);
instance3 = (Singleton) in.readObject();
} catch (Exception e) {
} finally { // close bout&out
} // 顯然,instance3 和 instance1 不是同一個對象了
System.out.println(instance1 == instance3); // false
}
}
毫無疑問,instance1 == instance3 也會返回 false。
防止序列化破壞
在序列化之前,我們要在類上面加上 implements Serializable。
我們需要做的是,在類中加上 readResolve() 這個方法,返回實例。
public class Singleton implements Serializable { private static final Singleton INSTANCE = new Singleton(); private String name; public String getName() { return this.name;
} private Singleton() { this.name = "Neo";
} public static Singleton getInstance() { return INSTANCE;
} // 看這裡
public Object readResolve() throws ObjectStreamException { return INSTANCE;
}
}
你再試一下,會發現變成 true 了。
因為在反序列化的時候,JVM 會自動調用 readResolve() 這個方法,我們可以在這個方法中替換掉從流中反序列化回來的對象。
這個方法完整的描述是這樣的:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
總結
文中沒有示例說反序列化在 enum 類中的表現,我直接說結論吧。enum 類自帶這種特殊光環,不用寫 readResolve() 方法就可以自動防止反序列化方式對單例的破壞。
閱讀更多 JavaSpring高級進階 的文章