vlambda博客
学习文章列表

单例模式-序列化问题

在前面文章 ,提到了对象序列化会破坏单例模式,同时也做了试验,今天我们来借着这个问题了解一下序列化的过程。

序列化的作用起源这里就不详细赘述了,本文主要解决序列化破坏单例的问题。如果想详细了解序列化,反序列化过程,推荐一篇文章,写得清清楚楚,包括底层实现流程。

https://cloud.tencent.com/developer/article/1125165)

问题原因

回顾之前序列化的测试代码,重点在于反序列化,我们需要探究为什么它返回了一个新对象。

        // 将对象从文件当中反序列化
        try{
            fis = new FileInputStream("Singleton.obj");
            ois = new ObjectInputStream(fis);
            s1 = (Singleton) ois.readObject();
        }finally{
            ois.close();
            fis.close();
        }

反序列化调用的是 ObjectInputStream 类的 readObject() 方法,继续往下定位问题所在。

源码的过程有些曲折,这边省略了中间过程。

最终走到了readOrdinaryObject() 这个方法,返回值就是反序列化的结果,发现它还是通过反射创建了一个新的对象。。。。。

private Object readOrdinaryObject(boolean unshared)
    throws IOException
{
    if (bin.readByte() != TC_OBJECT) {
        throw new InternalError();
    }

    // 获取类的描述信息
    ObjectStreamClass desc = readClassDesc(false);
    desc.checkDeserialize();
    // 通过描述信息获取 Class 对象
    Class<?> cl = desc.forClass();
    if (cl == String.class || cl == Class.class
            || cl == ObjectStreamClass.class) {
        throw new InvalidClassException("invalid class descriptor");
    }

    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);
    }

解决方案

当然官方提供了一个『小后门』,单例类可以通过实现 readResolve() 这个方法,解决序列化破坏单例的问题,代码如下,可以看到 readResolve() 方法中我们返回了之前创建的单例对象。

class Singleton implements Serializable {

    private static final long serialVersionUID = 1L;
    String name;

    private Singleton() {
    }

    private static Singleton instance = new Singleton();

    public static Singleton getInstance() {
        return instance;
    }

    private Object readResolve() {
        return instance;
    }
}

再次运行之前的测试代码:

    public static void main(String[] args) throws Exception {
        Singleton s1 = null;
        Singleton s = Singleton.getInstance();

        FileOutputStream fos = null;
        ObjectOutputStream oos = null;

        FileInputStream fis = null;
        ObjectInputStream ois = null;
        try {
            fos = new FileOutputStream("Singleton.obj");
            oos = new ObjectOutputStream(fos);
            oos.writeObject(s);
        } finally {
            oos.flush();
            oos.close();
            fos.close();
        }

        try{
            fis = new FileInputStream("Singleton.obj");
            ois = new ObjectInputStream(fis);
            s1 = (Singleton) ois.readObject();
        }finally{
            ois.close();
            fis.close();
        }

        System.out.println(s.hashCode());
        System.out.println(s1.hashCode());

        System.out.println(s == s1);
    }

结果如下:

回到上面提到的反序列化方法 readOrdinaryObject() ,看它的后半部分代码,可以发现这个『小后门』。

        // 判断类中是否定义了 readResolved() 方法
        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);
            }
        }

当然也可以通过枚举来实现单例模式,就不存在被序列化破坏单例的问题,这是因为在反序列化的过程中对枚举类型进行了特殊处理。

我们看到 readEnum() 这个方法,用来对枚举类型进行反序列化。

    private Enum<?> readEnum(boolean unshared) throws IOException {
        if (bin.readByte() != TC_ENUM) {
            throw new InternalError();
        }

        ObjectStreamClass desc = readClassDesc(false);
        if (!desc.isEnum()) {
            throw new InvalidClassException("non-enum class: " + desc);
        }

        int enumHandle = handles.assign(unshared ? unsharedMarker : null);
        ClassNotFoundException resolveEx = desc.getResolveException();
        if (resolveEx != null) {
            handles.markException(enumHandle, resolveEx);
        }

        String name = readString(false);
        Enum<?> result = null;
        Class<?> cl = desc.forClass();
        if (cl != null) {
            try {
                @SuppressWarnings("unchecked")
                Enum<?> en = Enum.valueOf((Class)cl, name);
                result = en;
            } catch (IllegalArgumentException ex) {
                throw (IOException) new InvalidObjectException(
                    "enum constant " + name + " does not exist in " +
                    cl).initCause(ex);
            }
            if (!unshared) {
                handles.setObject(enumHandle, result);
            }
        }

        handles.finish(enumHandle);
        passHandle = enumHandle;
        return result;
    }

重点在这,最终是直接调用枚举类提供的 Api 来获取对象,参数是枚举类的 Class 对象,以及枚举变量名称,返回值就是之前定义的枚举对象。

Enum<?> en = Enum.valueOf((Class)cl, name);

总结

通过这篇文章,我们了解了:

  • 反序列化破坏单例模式的原因

  • 通过在单例类中实现 readResolve() 解决问题

  • 通过枚举实现单例,可以避免序列化的问题

如果觉得文章对你有帮助,欢迎留言点赞。




大家的关注是我持续输出的动力

长按扫码可关注