面试加分项:如何防止序列化和反射破坏单例模式

1,756 阅读5分钟

大家应该都知道反射和序列化会破坏单例模式,但是一部分人可能不知道,如何防止这种破坏,下面文章就记录一下,如何防止单例模式被反射和序列化破坏!

1、单例模式

单例模式:顾名思义就是只有一个实例,并且它自己负责创建自己的对象,这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

实现方式:单例模式的实现方式有很多种,比如懒汉式,饿汉式,双重校验锁,静态内部类,枚举等等,这里就不一一贴出来代码看了,不熟悉或者感兴趣的同学可以点后面链接去自己查看。单例模式

我们这里先写一个饿汉式的,方便下面文章使用。

public class HungrySingleton implements Serializable {

    private final static HungrySingleton hungrySingleton;

    // 初始化
    static {
        hungrySingleton = new HungrySingleton();
    }

    // 构造器私有化
    private HungrySingleton(){

    }

    // 提供唯一访问方式
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

2、序列化破坏单例模式

首先通过getInstance()得到对象,然后序列化到文件中,再读取文件得到一个新的对象,然后判断两个对象是否相同,代码如下:

public class HungrySingletonTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException {

        // 获取对象
        HungrySingleton instance = HungrySingleton.getInstance();
        // 将对象序列化到文件中
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("hungrysingleton.txt"));
        objectOutputStream.writeObject(instance);

        // 读取文件
        File file = new File("hungrysingleton.txt");
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) objectInputStream.readObject();

        // 判断是否为同一个对象
        System.out.println("初始对象:" + instance);
        System.out.println("反序列化后得到的对象:" + newInstance);

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

执行结果 很明显可以看到,通过反序列化拿到了不同的对象,从而说序列化破坏了单例模式,!

原因:因为反序列化是从文件中读取数据,那先来看下ObjectInputStream的readObject()方法,这里主要看下下图打断点的这个方法,也就是readObject0这个方法: image 这个方法里面的代码比较长,我就不截图出来了,这个方法里面有个switch判断,判断读取类型,因为刚才读取的是Object类型的,那就看下TC_OBJECT这一类型里面的readOrdinaryObject方法: image 进入这个方法可以看到最后返回的Object有一个判断,如果这个类实现了序列化接口,那么返回一个newInstance,否则返回null,而刚才写的类已经实现了序列化接口,那么这个方法返回的就是通过反射得到的一个新的实例,所以反序列化拿到了不同的对象! image

3、反射破坏单例模式

首先得到HungrySingleton类的构造器,然后将权限改为true,然后newInstance得到一个新的实例,判断两个实例是否相同,代码如下:

public class HungrySingletonTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        Class hungrySingletonClass = HungrySingleton.class;
        // 得到HungrySingleton类的构造器
        Constructor constructor = hungrySingletonClass.getDeclaredConstructor();
        // 将构造器权限改为true
        constructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

        System.out.println("初始实例:" + instance);
        System.out.println("反射得到的实例:" + newInstance);
        System.out.println(instance == newInstance);
    }
}

结果图 可以看到,得到的实例不同,表示单例模式被反射破坏了!

原因:原因很简单,上面代码也写了注释,就是因为反射将构造器的私有属性改变了,所以类可以通过构造器得到一个新的实例,这样相比于初始化的实例,肯定是不相同的。

4、序列化破坏解决方案

要想解决序列化破坏单例模式其实很简单,只用在单例类内添加一个名为readResolve()方法,代码如下:

public class HungrySingleton implements Serializable {

    private final static HungrySingleton hungrySingleton;

    // 初始化
    static {
        hungrySingleton = new HungrySingleton();
    }

    // 构造器私有化
    private HungrySingleton(){

    }

    // 提供唯一访问方式
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    
    // 返回唯一对象
    private Object readResolve(){
        return hungrySingleton;
    }
}

再执行一下main方法,可以看到如下结果: 结果 可以看到返回了同一个对象,至于原因,继续往下看。

上面将反序列化源码的时候,进入了下图这里: image 由于类实现了序列化接口,那么返回一个新的实例,那么obj必不可能为null,再往下看: image 可以看到它调用了一个hasReadResolveMethod方法,这个方法你进去看注释: image 或者看方法名字也能看清楚,就是判断要反序列化的类是否有readResolve这个方法,而我们刚才的类中有这个方法,那么就会返回true,往下执行,就到了下图这一步,执行invokeReadResolve方法: image

invokeReadResolve(Object obj)
invokeReadResolve(Object obj)

这里通过名字也能看出来,就是通过反射去调用readResolve方法,而我们写的类中,readResolve方法返回的是唯一的HungrySingleton类的实例,由此得到的对象和通过getInstance()得到的对象是一致的!

5、反射破坏解决方案

反射破坏的解决方案,就要针对不同的实现方式来说了,比如上面写的单例模式是饿汉式,就是类在初始化的时候就已经将对象创建好了,针对于这种,我们可以修改一下代码,在构造器内增加判断:

public class HungrySingleton implements Serializable {

    private final static HungrySingleton hungrySingleton;

    // 初始化
    static {
        hungrySingleton = new HungrySingleton();
    }

    // 构造器私有化
    private HungrySingleton(){
        if (hungrySingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用 ");
        }
    }

    // 提供唯一访问方式
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

结果 如果是那些懒汉式或者双重锁的那种的话,可以增加一个静态变量,然后在类初始化的时候将静态变量修改值,然后在构造器内判断静态变量的值来做相应的操作!

6、公众号

如果你觉得我的文章对你有帮助话,欢迎关注我的微信公众号:"一个快乐又痛苦的程序员"(无广告,单纯分享原创文章、已pj的实用工具、各种Java学习资源,期待与你共同进步) 公众号