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()方法,返回同一个对象即可。

相关推荐
面朝大海,春不暖,花不开16 分钟前
自定义Spring Boot Starter的全面指南
java·spring boot·后端
得过且过的勇者y16 分钟前
Java安全点safepoint
java
钡铼技术ARM工业边缘计算机1 小时前
【成本降40%·性能翻倍】RK3588边缘控制器在安防联动系统的升级路径
后端
夜晚回家1 小时前
「Java基本语法」代码格式与注释规范
java·开发语言
斯普信云原生组1 小时前
Docker构建自定义的镜像
java·spring cloud·docker
wangjinjin1801 小时前
使用 IntelliJ IDEA 安装通义灵码(TONGYI Lingma)插件,进行后端 Java Spring Boot 项目的用户用例生成及常见问题处理
java·spring boot·intellij-idea
wtg44521 小时前
使用 Rest-Assured 和 TestNG 进行购物车功能的 API 自动化测试
java
CryptoPP1 小时前
使用WebSocket实时获取印度股票数据源(无调用次数限制)实战
后端·python·websocket·网络协议·区块链
白宇横流学长1 小时前
基于SpringBoot实现的大创管理系统设计与实现【源码+文档】
java·spring boot·后端
fat house cat_2 小时前
【redis】线程IO模型
java·redis