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

相关推荐
hongtianzai2 分钟前
Laravel8.x核心特性全解析
java·c语言·开发语言·golang·php
AntBlack3 分钟前
Ant-Browser : 发布一个开源免费的指纹浏览器 ,欢迎体验
后端·架构·go
逸Y 仙X5 分钟前
文章十一:ElasticSearch Dynamic Template详解
java·大数据·数据库·elasticsearch·搜索引擎·全文检索
隔壁小邓7 分钟前
IDEA 中同时启动多个微服务
java·微服务·intellij-idea
:1217 分钟前
idea17创建tomcat项目(计网底层核心理解!)
java·ide·intellij-idea
楼田莉子8 分钟前
C++高性能并发内存池:三种Cache的设计及其内存申请释放
c++·后端·链表·哈希算法·visual studio
Brookty11 分钟前
网络通信核心:四元组、socket与IO机制详解
java·网络通信·网络入门
短剑重铸之日16 分钟前
《ShardingSphere解读》12 解析引擎:SQL 解析流程应该包括哪些核心阶段?(下)
数据库·后端·sql·架构·shardingsphere·分库分表
佩奇大王21 分钟前
P159 摆动序列
java·开发语言·算法
计算机学姐23 分钟前
基于SpringBoot的网吧管理系统
java·spring boot·后端·spring·tomcat·intellij-idea·mybatis