【設計模式】如何避免反射和序列化破壞單例模式

單例模式的研究重點有以下幾個:

  1. 構造私有,提供靜態輸出接口
  2. 線程安全,確保全局唯一
  3. 延遲初始化
  4. 防止反射攻擊
  5. 防止序列化破壞單例模式

上一節《單例設計模式實現總結》,我們使用餓漢式、雙重鎖檢查、靜態內部類、枚舉類實踐了前3條。然而光併發安全並不能保證唯一實例,反射和序列化可以破壞單例模式。

public class ReflectSingleton {
    private final static ReflectSingleton instance = new ReflectSingleton();

    private ReflectSingleton() {
    }

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

本文中採用餓漢單例模式作爲最初代碼,演示如何避免反射和序列化破壞單例模式。

防止反射攻擊

使用反射攻擊單例模式

public class Client {
    public static void main(String[] args) throws Exception {
        // 通過全局訪問方法創建實例
        ReflectSingleton instance = ReflectSingleton.getInstance();
        // 通過反射創建實例
        Constructor constructor = ReflectSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        ReflectSingleton newInstance = (ReflectSingleton) constructor.newInstance();
        
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

使用反射時,需要添加構造器權限,否則會拋異常。

Exception in thread "main" java.lang.IllegalAccessException: Class com.lzp.java.concurrent.singleton.destroysingleton.Client can not access a member of class com.lzp.java.concurrent.singleton.destroysingleton.ReflectSingleton with modifiers "private"
	at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:102)
	at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:296)
	at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:288)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:413)
	at com.lzp.java.concurrent.singleton.destroysingleton.Client.main(Client.java:16)

運行結果:

com.lzp.java.concurrent.singleton.destroysingleton.ReflectSingleton@355da254
com.lzp.java.concurrent.singleton.destroysingleton.ReflectSingleton@4dc63996
false

改進措施:反射防禦

抵禦這種攻擊,可以在構造器中添加反射防禦代碼,讓它在被要求創建第二個實例時拋出異常。

private ReflectSingleton() {
    if (instance != null) {
        throw new RuntimeException("禁止反射調用創建多個實例");
    }
}

運行結果:

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.lzp.java.concurrent.singleton.destroysingleton.Client.main(Client.java:18)
Caused by: java.lang.RuntimeException: 禁止反射調用創建多個實例

需要注意的是,在構造器中添加反射防禦代碼,僅適用於基於類初始化加載的單例實現,即餓漢式和靜態內部類實現。對於雙重鎖檢查不會出現反射攻擊的情況。

防止序列化破壞單例模式

反序列化問題

public class Client2 {
    public static void main(String[] args) throws Exception {
        // 使用全局訪問方法創建實例
        SerializeSingleton instance = SerializeSingleton.getInstance();

        // 寫出對象到項目目錄下singleton.txt文件
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton.txt"));
        oos.writeObject(instance);
        // 讀入對象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton.txt"));
        SerializeSingleton newInstance = (SerializeSingleton) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

運行結果:

com.lzp.java.concurrent.singleton.destroysingleton.SerializeSingleton@4b1210ee
com.lzp.java.concurrent.singleton.destroysingleton.SerializeSingleton@27973e9b
false

改進措施:添加readResolve()方法

private Object readResolve() {
    return instance;
}


運行結果:

com.lzp.java.concurrent.singleton.destroysingleton.SerializeSingleton@4b1210ee
com.lzp.java.concurrent.singleton.destroysingleton.SerializeSingleton@4b1210ee
true

爲什麼是readResolve(),而不是其他方法?

此時可以對源碼做單步調試。

// 核心語句
SerializeSingleton newInstance = (SerializeSingleton) ois.readObject();

// ObjectInputStream
public final Object readObject(){
    ...
    try {
    Object obj = readObject0(false);
    .....
    }
}

readObject方法內部調用readObject0方法。

// ObjectInputStream
private Object readObject0(boolean unshared) throws IOException {
    ......
    try {
        switch (tc) {
            ........
            case TC_OBJECT:  // 如果是讀取對象Object
                return checkResolve(readOrdinaryObject(unshared));
            .........
            default:
                throw new StreamCorruptedException(
                    String.format("invalid type code: %02X", tc));
        }
    } finally {
        depth--;
        bin.setBlockDataMode(oldMode);
    }
}

定位到關鍵方法readOrdinaryObject()。

// ObjectInputStream
private Object readOrdinaryObject(boolean unshared)
    throws IOException
{
    .......
    Object obj;
    try {
        // 注:如果爲true,通過反射創建新的實例
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }
    ......
    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        // 內部核心語句:return readResolveMethod.invoke(obj, (Object[]) null);
        // 反射創建原實例
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            // 替換對象
            if (rep != null) {
                if (rep.getClass().isArray()) {
                    filterCheck(rep.getClass(), Array.getLength(rep));
                } else {
                    filterCheck(rep.getClass(), -1);
                }
            }
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

/**
 * 如果類是可序列化的,返回true
 */
boolean isInstantiable() {
    requireInitialized();
    return (cons != null);
}

/**
 * 如果類是可序列化的,並且定義了readResolve()方法,返回true;否則返回false
 */
boolean hasReadResolveMethod() {
    requireInitialized();
    return (readResolveMethod != null);
}

通過調試,我們可以看出,調readObject()方法反序列化的過程中,總會創建一個新的實例。如果SerializeSingleton類中定義了readResolve方法,就通過反射創建原實例,返回時覆蓋之前創建的實例。否則,返回新的實例。

通過底層代碼分析,我們便清楚了爲什麼用的是readResolve方法,而不是其他。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章