使用枚舉來實現(xiàn)單例

從jdk1.5開始,可通過編寫一個包含單個元素的枚舉類型來實現(xiàn)單例:

public enum Singleton {
    uniqueInstance;
    public void SingletonOperation() { 
        ......
    }
}
//然后就可以通過Singleton.uniqueInstace.SingletonOperaion來調用

這種方法在功能上與共有域方法相近,但它更加簡潔,無償?shù)靥峁┝诵蛄谢臋C制,絕對防止多次實例化,即使在面對復雜的序列化或者反射攻擊時也能防止。
單元素的枚舉類型是實現(xiàn)Singleton的最佳方法。

首先來了解常用的單例模式為何能被攻擊破壞。

破壞單例模式

破壞單例模式主要有三種方式:克隆、反射和序列化。簡單了解一下這三種方式如何破壞單例。

  • 克隆
    這種攻擊方式只針對實現(xiàn)了Cloneable接口的單例類。clone方法是不會調用構造函數(shù)的,它是直接從內存中copy內存區(qū)域的。
    而clone方法是Object類的protected方法,默認情況下一個對象是不能直接調用clone方法的。
    對于有實現(xiàn)Cloneable接口需求的單例類,應重寫clone方法,直接返回其內部的instance,而不能直接返回super.clone()。
public class testSingleton {

    public static void main(String[] args) throws Exception{
        Singleton singleton = Singleton.getInstance();

        Singleton clone = (Singleton) singleton.clone();
        System.out.println(singleton == clone);
    }

    public static class Singleton implements Cloneable{

        private static Singleton instance = new Singleton();

        private Singleton() {}

        public static Singleton getInstance() {
            return instance;
        }

        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
}
  • 反射
    反射可以獲取類的構造函數(shù),同時再通過setAccessible(true)就可調用私有構造函數(shù)來創(chuàng)建對象。
    要防止反射攻擊,只能在單例構造方法中檢測instance是否為null,或使用first執(zhí)行標志等,只要是第二次調用了構造方法就拋異常。
public class testSingleton {

    public static void main(String[] args) throws Exception{
        Singleton singleton = Singleton.getInstance();

        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton reflect = (Singleton)constructor.newInstance();
        System.out.println(singleton == reflect);
    }

    public static class Singleton{

        private static Singleton instance = new Singleton();

        private Singleton() {}

        public static Singleton getInstance() {
            return instance;
        }
    }
}
  • 序列化
    這種攻擊也只對實現(xiàn)了Serializable接口的單例有效,而有些單例恰巧是必須序列化的。
    序列化的攻擊方式如下所示,主要問題就在inputStream.readObject中。
public class testSingleton {

    public static void main(String[] args) throws Exception{
        Singleton singleton = Singleton.getInstance();

        ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serfile"));
        outputStream.writeObject(Singleton.getInstance());

        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("serfile"));
        Singleton seriable = (Singleton) inputStream.readObject();
        System.out.println(singleton == seriable);

    }

    public static class Singleton implements Serializable{

        private static Singleton instance = new Singleton();

        private Singleton() {}

        public static Singleton getInstance() {
            return instance;
        }
    }
}

在ObjectInputStream.readObject方法執(zhí)行時,其內部方法readOrdinaryObject中執(zhí)行了該語句:

obj = desc.isInstantiable() ? desc.newInstance() : null;

其中desc是類描述符,也就是說,如果一個實現(xiàn)了Serializable/Externalizable接口的類可以在運行時實例化,那么就調用newInstance()方法,使用其默認構造方法反射創(chuàng)建新的對象實例,自然也就破壞了單例性。
要防御序列化攻擊,就得將instance聲明為transient,且在單例中加入:

private Object readResolve() {
    return instance;
}

因為在readOrdinaryObject方法中,會通過desc.hasReadResolveMethod()檢查類中是否存在readResolve方法,若存在,則執(zhí)行desc.invokeReadResolve(obj)來調用該方法,readResolve方法會用自定義的反序列化邏輯覆蓋默認實現(xiàn),因此強制它返回instance本身,從而防止產生新的實例。

枚舉單例的防御

  • 針對反射
    java的枚舉類型實際上都繼承自Enum抽象類,而該類只有一個帶有兩個參數(shù)的構造方法:
    protected Enum(String name, int ordinal) {
        this.name = name;
        this.ordinal = ordinal;
    }

我們通過反射獲取這個構造方法:

Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);

測試發(fā)現(xiàn)拋出如下異常:


image.png

從它拋出的異常的注釋:Cannot reflectively create enum objects可看出,從JDK反射機制的內部實現(xiàn)就已經(jīng)排除了用反射創(chuàng)建枚舉實例的可能。

  • 針對序列化
    嘗試用同樣的序列化攻擊方式來攻擊枚舉實現(xiàn)的單例,發(fā)現(xiàn),最終獲得的實際是同一個實例。
    在ObjectInputStream類的readObjectO方法中,枚舉類型獲取到的為TC_ENUM,專門針對枚舉類型做處理,調用readEnum方法,該方法會通過調用Enum.valueof方法,傳入獲取到的枚舉類型以及具體的枚舉值,從而獲得最終的單例。
    JDK內部對枚舉類型的專門處理同樣也繞開了序列化對枚舉實現(xiàn)單例的攻擊。

參考:http://www.itdecent.cn/p/d9d9dcf23359

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容