Java編程——單例模式的安全性

單例模式,我想大家再熟悉不過了,不過本文不是介紹單例模式該怎麼寫的。

本文來說說怎麼破壞一個單例,讓你寫的單例變成一個假的單例。當然,本文也會給出怎麼進行防守的方法。

一個簡單的單例

來一個簡單的單例模式例子:

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.getConstructor0(Class.java:3082)

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() 方法就可以自動防止反序列化方式對單例的破壞。


分享到:


相關文章: