破解單例模式:反射、序列化與克隆攻擊的防御之道
可能有人看了我上一篇文章里幾種方式對比的表格,覺得枚舉有缺點(diǎn),為什么Joshua Bloch還推薦使用枚舉?
這就要提到單例的破解了。普通的單例模式是可以通過反射和序列化/反序列化來破解的,而Enum由于自身的特性問題,是無法破解的。當(dāng)然,由于這種情況基本不會出現(xiàn),因此我們在使用單例模式的時候也比較少考慮這個問題。
枚舉類是實(shí)現(xiàn)單例模式最好的方式
在單例模式的實(shí)現(xiàn)中,除去枚舉方法實(shí)現(xiàn)的單例模式,其它的實(shí)現(xiàn)都可以利用反射構(gòu)造新的對象,從而破壞單例模式,但是枚舉就不行,下面說說原因:
破壞單例的方式有 3 種,反射、克隆以及序列化,下面詳細(xì)介紹:
反射
常見的單例模式實(shí)現(xiàn)中,往往有一個私有的構(gòu)造函數(shù),防止外部程序的調(diào)用,但是通過反射可以輕而易舉的破壞這個限制:
public class DobleCheckSingleton {
private DobleCheckSingleton(){}
private static volatile DobleCheckSingleton dobleCheckSingleton;
public static DobleCheckSingleton getSingleton(){
if (dobleCheckSingleton == null){
synchronized (DobleCheckSingleton.class){
if (dobleCheckSingleton == null){
dobleCheckSingleton = new DobleCheckSingleton();
}
}
}
return dobleCheckSingleton;
}
public static void main(String[] args) {
try {
DobleCheckSingleton dobleCheckSingleton = DobleCheckSingleton.getSingleton();
Constructor<DobleCheckSingleton> constructor = DobleCheckSingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
DobleCheckSingleton reflectInstance = constructor.newInstance();
System.out.println(dobleCheckSingleton == reflectInstance);
} catch (Exception e e.printStackTrace();
}
}
}
輸出:false,單例被破壞
顯然,通過反射可以破壞所有含有無參構(gòu)造器的單例類,如可以破壞懶漢式、餓漢式、靜態(tài)內(nèi)部類的單例模式。
但是反射無法破壞通過枚舉實(shí)現(xiàn)的單例模式,利用反射構(gòu)造新的對象,由于 enum 沒有無參構(gòu)造器,結(jié)果會拋出 NoSuchMethodException 異常。
public enum EnumSingleton {
INSTANCE;
public static void main(String[] args) {
try {
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
// 獲取無參的構(gòu)造函數(shù)
Constructor<EnumSingleton> constructor = null;
constructor = EnumSingleton.class.getDeclaredConstructor();
// 使用構(gòu)造函數(shù)創(chuàng)建對象
constructor.setAccessible(true);
EnumSingleton reflectInstance = constructor.newInstance();
System.out.println(enumSingleton == reflectInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
輸出:java.lang.NoSuchMethodException: singleton.EnumSingleton.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at singleton.EnumSingleton.main(EnumSingleton.java:19)
枚舉安全的原因解釋:
對 EnumSingleton 文件進(jìn)行反編譯,可以發(fā)現(xiàn) EnumSingleton 繼承于 Enum,而 Enum 類確實(shí)沒有無參的構(gòu)造器,所以拋出 NoSuchMethodException。
枚舉類 EnumSingleton 反編譯結(jié)果
public final class singleton.EnumSingleton extends java.lang.Enum<singleton.EnumSingleton>
Enum 類的構(gòu)造方法
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
進(jìn)一步,通過調(diào)用父類有參構(gòu)造器構(gòu)造枚舉實(shí)例對象,樣例程序又拋出 IllegalArgumentException 異常。
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
public static void main(String[] args) {
try {
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
// 獲取無參的構(gòu)造函數(shù)
Constructor<EnumSingleton> constructor = null;
constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
// 使用構(gòu)造函數(shù)創(chuàng)建對象
constructor.setAccessible(true);
EnumSingleton reflectInstance = constructor.newInstance("test",1);
System.out.println(enumSingleton == reflectInstance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
輸出:
java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at singleton.EnumSingleton.main(EnumSingleton.java:31)
因?yàn)?Constructor 的 newInstance 方法限定了 clazz 的類型不能是 enum,否則拋出異常。
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
...
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
...
}
所以枚舉類不能通過反射構(gòu)建構(gòu)造函數(shù)的方式構(gòu)建新的實(shí)例。
序列化
先看看通過序列化破壞單例的例子,其中 Singleton 實(shí)現(xiàn)了 Serializable 接口,才有可能通過序列化破壞單例。
public class DobleCheckSingleton implements Serializable {
private DobleCheckSingleton() {
}
private static volatile DobleCheckSingleton dobleCheckSingleton;
public static DobleCheckSingleton getSingleton() {
if (dobleCheckSingleton == null) {
synchronized (DobleCheckSingleton.class) {
if (dobleCheckSingleton == null) {
dobleCheckSingleton = new DobleCheckSingleton();
}
}
}
return dobleCheckSingleton;
}
public static void main(String[] args) {
try {
DobleCheckSingleton dobleCheckSingleton = DobleCheckSingleton.getSingleton();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
oos.writeObject(dobleCheckSingleton);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
DobleCheckSingleton s1 = (DobleCheckSingleton) ois.readObject();
ois.close();
System.out.println(dobleCheckSingleton == s1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
輸出:false,單例被破壞
枚舉類實(shí)現(xiàn),枚舉類不實(shí)現(xiàn) Serializable 接口,都可以進(jìn)行序列化,并且返回原來的單例。
public enum EnumSingleton {
INSTANCE;
public static void main(String[] args) {
try {
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
oos.writeObject(enumSingleton);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
EnumSingleton s1 = (EnumSingleton) ois.readObject();
ois.close();
System.out.println(enumSingleton == s1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
輸出:true
原因: 枚舉類的 writeObject 方法僅僅是將 Enum.name 寫到文件中,反序列化時,根據(jù) readObject 方法的源碼定位到 Enum 的 valueOf 方法,他會根據(jù)名稱返回原來的對象。
克隆
實(shí)現(xiàn) Cloneable 接口重寫 clone 方法,但是 Enum 類中 clone 的方法是 final 類型,無法重寫,也就不能通過克隆破壞單例。
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
}
不用枚舉如何防止單例模式破壞
若實(shí)現(xiàn)了序列化接口,重寫 readResolve 方法即可,反序列化時將調(diào)用該方法返回對象實(shí)例。
public Object readResolve() throws ObjectStreamException {
return dobleCheckSingleton;
}
通過反射破壞單例的場景,可以在構(gòu)造方法中判斷實(shí)例是否已經(jīng)創(chuàng)建,若已創(chuàng)建則拋出異常。
private Singleton(){
if (instance !=null){
throw new RuntimeException("實(shí)例已經(jīng)存在,請通過 getInstance()方法獲取");
}
}
通過clone破壞單例的場景,可以重寫clone方法,返回已有單例對象。