Bootstrap

如何避免反射和序列化破坏单例模式

单例模式的研究重点有以下几个:

  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方法,而不是其他。

;