單例模式相信你們都不陌生,咱們不討論單例模式的幾種寫法及其優劣。今天咱們單獨拎出單例的幾種實現來看看如何有效的抵禦反射及序列化的攻擊。若是不瞭解反射和序列化的能夠看這兩篇文章。
反射
序列化html
單例模式最根本的在於類只能有一個實例,若是經過反射來構建這個類的實例,單例模式就會被破壞,下面咱們經過例子來看下:java
/**
* 靜態內部類式單例模式
*/
class Singleton implements Serializable{
private static class SingletonClassInstance {
private static final Singleton instance = new Singleton();
}
//方法沒有同步,調用效率高
public static Singleton getInstance() {
return SingletonClassInstance.instance;
}
private Singleton() {}
}
複製代碼
相信你們對於這個單例的這種實現方式確定不陌生,下面咱們來看看經過反射來建立類實例會不會破壞單例模式。main函數代碼以下:安全
Singleton sc1 = Singleton.getInstance();
Singleton sc2 = Singleton.getInstance();
System.out.println(sc1); // sc1,sc2是同一個對象
System.out.println(sc2);
/*經過反射的方式直接調用私有構造器(經過在構造器裏拋出異常能夠解決此漏洞)*/
Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.learn.example.Singleton");
Constructor<Singleton> c = clazz.getDeclaredConstructor(null);
c.setAccessible(true); // 跳過權限檢查
Singleton sc3 = c.newInstance();
Singleton sc4 = c.newInstance();
System.out.println("經過反射的方式獲取的對象sc3:" + sc3); // sc3,sc4不是同一個對象
System.out.println("經過反射的方式獲取的對象sc4:" + sc4);
複製代碼
下面咱們來看輸出:bash
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
經過反射的方式獲取的對象sc3:com.learn.example.Singleton@25154f
經過反射的方式獲取的對象sc4:com.learn.example.Singleton@10dea4e
複製代碼
咱們看到正常的調用getInstance是符合咱們預期的,若是經過反射(繞過檢查,經過反射能夠調用私有的),那麼單例模式實際上是失效了,咱們建立了兩個徹底不一樣的對象sc3和sc4。咱們如何來修復這個問題呢?反射須要調用構造函數,那咱們能夠在構造函數裏面進行判斷。修復代碼以下:多線程
class Singleton implements Serializable{
private static class SingletonClassInstance {
private static final Singleton instance = new Singleton();
}
//方法沒有同步,調用效率高
public static Singleton getInstance() {
return SingletonClassInstance.instance;
}
//防止反射獲取多個對象的漏洞
private Singleton() {
if (null != SingletonClassInstance.instance)
throw new RuntimeException();
}
}
複製代碼
咱們看到惟一的改進在於,構造函數裏面添加了判斷,若是當前已有實例,經過拋出異常來阻止反射建立對象。咱們來看下輸出:函數
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
at java.lang.reflect.Constructor.newInstance(Unknown Source)
at com.learn.example.RunMain.main(RunMain.java:45)
Caused by: java.lang.RuntimeException
at com.learn.example.Singleton.<init>(RunMain.java:28)
... 5 more
複製代碼
咱們看到,咱們經過反射建立對象的時候會拋出異常了。post
除了反射之外,反序列化過程也會破壞單例模式,咱們來看下現階段反序列化輸出的結果:ui
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
對象定義了readResolve()方法,經過反序列化獲得的對象:com.learn.example.Singleton@16ec8df
複製代碼
咱們看到反序列化後的對象和原對象sc1已經不是同一個對象了。咱們須要對反序列化過程進行處理,處理代碼以下:spa
//防止反序列化獲取多個對象的漏洞。
//不管是實現Serializable接口,或是Externalizable接口,當從I/O流中讀取對象時,readResolve()方法都會被調用到。
//實際上就是用readResolve()中返回的對象直接替換在反序列化過程當中建立的對象
private Object readResolve() throws ObjectStreamException {
return SingletonClassInstance.instance;
}
複製代碼
咱們從註釋裏面也能夠看出來,readResolve方法會將原來反序列化出來的對象進行覆蓋。咱們丟棄原來反序列化出來的對象,使用已經建立的好的單例對象進行覆蓋。咱們來看如今的輸出:線程
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
對象定義了readResolve()方法,經過反序列化獲得的對象:com.learn.example.Singleton@52e922
複製代碼
關於readResolve這個方法的詳細解釋能夠看這篇文章:
序列化的相關方法介紹
Effective Java中推薦使用枚舉來實現單例,由於枚舉實現單例能夠阻止反射及序列化的漏洞,下面咱們經過例子來看下:
class Resource{}
/**
* 使用枚舉實現單例
*/
enum SingletonEnum{
INSTANCE;
private Resource instance;
SingletonEnum() {
instance = new Resource();
}
public Resource getInstance() {
return instance;
}
}
複製代碼
咱們在main方法中調用代碼:
Resource resource1 = SingletonEnum.INSTANCE.getInstance();
Resource resource2 = SingletonEnum.INSTANCE.getInstance();
System.out.println(resource1);
System.out.println(resource2);
複製代碼
輸出以下:
com.learn.example.Resource@52e922
com.learn.example.Resource@52e922
複製代碼
咱們看到,經過枚舉咱們實現了單例,那麼枚舉是如何保證單例的(如何知足多線程及序列化的標準的)?其實枚舉是一個普通的類,它繼承自java.lang.Enum類。咱們將上面的class文件反編譯後,會獲得以下代碼:
public final class SingletonEnum extends Enum<SingletonEnum> {
public static final SingletonEnum INSTANCE;
public static SingletonEnum[] values();
public static SingletonEnum valueOf(String s);
static {};
}
複製代碼
由反編譯後的代碼可知,INSTANCE 被聲明爲static 的,在類加載過程,能夠知道虛擬機會保證一個類的() 方法在多線程環境中被正確的加鎖、同步。因此,枚舉實現是在實例化時是線程安全。
Java規範中規定,每個枚舉類型極其定義的枚舉變量在JVM中都是惟一的,所以在枚舉類型的序列化和反序列化上,Java作了特殊的規定。
在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是經過 java.lang.Enum 的 valueOf() 方法來根據名字查找枚舉對象。
也就是說,如下面枚舉爲例,序列化的時候只將 INSTANCE 這個名稱輸出,反序列化的時候再經過這個名稱,查找對於的枚舉類型,所以反序列化後的實例也會和以前被序列化的對象實例相同。
Effective Java中單元素的枚舉類型被做者認爲是實現Singleton的最佳方法。