Java 序列化是如何破坏单例模式的?

先看代码:

csharp 复制代码
public class Singleton {
    private Singleton(){}

    private static class SingletonInstance{
        private static final Singleton instance = new Singleton();
    }

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

我想应该没有不知道这行个类是干嘛的小伙伴了吧,这是单例模式的一种写法。

单例模式是每一个 Java boy 必须要掌握的设计模式,它所描述的是在某个进程内,某个类有且仅有一个实例。我们知道要破坏单例模式就必须让它创建多个对象。创建对象的方式无非就几种:

  1. new
  2. clone
  3. 反射
  4. 反序列化

首先单例模式的构造器一定是 private 的,所以 new 这种方式是无法破坏单例模式的。 而 clone 需要实现 Cloneable 接口,单例模式谁如果实现了这个接口,请打死它。所以就剩下反射和反序列化了。本篇文章只讨论反序列化。

反序列化破坏单例模式

与 clone 方式一样,反序列化需要实现 Serializable 接口,但是有小伙伴可能会说,谁会在单例模式中实现 Serializable 接口咯,除非他疯了,确实是这种情况,但是在实际情况中它并不是一定会避免的,有些类它就是一定要序列化。比如单例对象在不同环境或应用实例之间的共享、持久化或状态恢复,当然这些场景都属于比较特殊的场景。

继续用上面例子:

java 复制代码
public class SerializableSingleton implements Serializable {
    // 省略部分代码
}

然后在对该类进行序列化和反序列化

java 复制代码
public class Test {
    public static void main(String[] args) throws Exception {
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.txt"));
        SerializableSingleton singleton = SerializableSingleton.getInstance();
        oos.writeObject(singleton);

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("Singleton.txt"));
        SerializableSingleton singleton1 = (SerializableSingleton) ois.readObject();

        System.out.println("singleton = singleton1:" + (singleton == singleton1));
    }
}

运行结果

ini 复制代码
singleton = singleton1:false

通过对 Singleton 进行反序列化得到了一个全新的对象,这就破坏了 Singleton 的单例性了。我们看 readObject() 源码就知道了。

java 复制代码
    public final Object readObject() throws IOException, ClassNotFoundException {
        //...
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);
            //...
            return obj;
        } finally {
            //...
        }
    }

调用 readObject0()

java 复制代码
    private Object readObject0(boolean unshared) throws IOException {
        // ...
        try {
            switch (tc) {
                // ...

                case TC_OBJECT:
                    // readObject0()
                    return checkResolve(readOrdinaryObject(unshared));
                // ...
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

readObject0() 是根据反序列化对象的不同执行不同的方法来反序列化一个实例对象。我们这里是 Object,所以进一步看 readOrdinaryObject()

scss 复制代码
    private Object readOrdinaryObject(boolean unshared) throws IOException {
        // ...

        Object obj;
        try {
            // 核心代码
            // 反射创建一个新对象
            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()) {
            Object rep = desc.invokeReadResolve(obj);
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {
                // Filter the replacement object
                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;
    }

这段代码最核心的地方就是:

ini 复制代码
obj = desc.isInstantiable() ? desc.newInstance() : null;

底层依然是利用反射的方式来创建一个新对象。

那么对于这种方式有什么保护措施没?在 readOrdinaryObject() 最后面一段就已经告知了:

csharp 复制代码
if (obj != null && handles.lookupException(passHandle) == null &&
      desc.hasReadResolveMethod()) {
     //....
 }        

判断反序列化的类是否已实现了 readResolve() ,如果有则会调用该方法,我们只需要在该方法里面返回原对象就可以了。验证下。

typescript 复制代码
public class SerializableSingleton implements Serializable {
    // ...
    private Object readResolve() {
        return SingletonInstance.instance;
    }
}

执行结果:

ini 复制代码
singleton = singleton1:true

执行结果为 true,就说明序列化和反序列化出来的是同一个对象。

所以,要想防止单例被反序列化破坏,就让单例实现 readResolve()方法,返回同一个对象即可。

相关推荐
雷神乐乐3 分钟前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。6 分钟前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野13 分钟前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航15 分钟前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
confiself31 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
Wlq041536 分钟前
J2EE平台
java·java-ee
XiaoLeisj43 分钟前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
杜杜的man1 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*1 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu1 小时前
Go语言结构体、方法与接口
开发语言·后端·golang