美文网首页
设计模式【1.1】-- 你想如何破坏单例模式?

设计模式【1.1】-- 你想如何破坏单例模式?

作者: 秦怀杂货店 | 来源:发表于2021-01-26 09:08 被阅读0次

    [TOC]

    1.单例是什么?

    单例模式:是一种创建型设计模式,目的是保证全局一个类只有一个实例对象,分为懒汉式和饿汉式。所谓懒汉式,类似于懒加载,需要的时候才会触发初始化实例对象。而饿汉式正好相反,项目启动,类加载的时候,就会创建初始化单例对象。

    1.1 优点

    如果只有一个实例,那么就可以少占用系统资源,节省内存,访问也会相对较快。比较灵活。

    1.2 缺点

    不能使用在变化的对象上,特别是不同请求会造成不同属性的对象。由于Spring本身默认实例就是单例的,所以使用的时候需要判断应用场景,要不会造成张冠李戴的现象。而往往操作引用和集合,就更不容易查找到这种诡异的问题。例如:一些配置获取,如果后期使用需要修改其值,要么定义使用单例,后期使用深拷贝,要么不要使用单例。

    既然使用单例模式,那么就得想尽一切办法,保证实例是唯一的,这也是单例模式的使命。但是代码是人写的,再完美的人也可能写出不那么完美的代码,再安全的系统,也有可能存在漏洞。既然你想保证单例,那我偏偏找出方法,创建同一个类多个不同的对象呢?这就是对单例模式的破坏,到底有哪些方式可以破坏单例模式呢?主要但是不限于以下几种:

    • 没有将构造器私有化,可以直接调用。
    • 反射调用构造器
    • 实现了cloneable接口
    • 序列化与反序列化

    2. 破坏单例的几种方法

    2.1 通过构造器创建对象

    一般来说,一个稍微 ✔️ 的单例模式,是不可以通过new来创建对象的,这个严格意义上不属于单例模式的破坏。但是人不是完美的,写出的程序也不可能是完美的,总会有时候疏忽了,忘记了将构造器私有化,那么外部就可以直接调用到构造器,自然就可以破坏单例模式,所以这种写法就是不成功的单例模式。

    /**
     * 下面是使用双重校验锁方式实现单例
     */
    public class Singleton{
        private volatile static Singleton singleton;
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    上面就是使用双重检察锁的方式,实现单例模式,但是忘记了写private的构造器,默认是有一个public的构造器,如果调用会怎么样呢?

        public static void main(String[] args) {
            Singleton singleton = new Singleton();
            Singleton singleton1 = new Singleton();
            System.out.println(singleton.hashCode());
            System.out.println(singleton1.hashCode());
            System.out.println(Singleton.getSingleton().hashCode());
        }
    

    运行的结果如下:

    692404036
    1554874502
    1846274136
    

    三个对象的hashcode都不一样,所以它们不是同一个对象,这样也就证明了,这种单例写法是不成功的。

    2.2 反射调用构造器

    如果单例类已经将构造方法声明成为private,那么暂时无法显式的调用到构造方法了,但是真的没有其他方法可以破坏单例了么?

    答案是有!也就是通过反射调用构造方法,修改权限。

    比如一个看似完美的单例模式:

    import java.io.Serializable;
    
    public class Singleton{
    
        private volatile static Singleton singleton;
        private Singleton(){}
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    测试代码如下:

    import java.lang.reflect.Constructor;
    
    public class SingletonTests {
        public static void main(String[] args) throws Exception {
            Singleton singleton = Singleton.getSingleton();
            Singleton singleton1=Singleton.getSingleton();
            Constructor constructor=Singleton.class.getDeclaredConstructor(null);
            constructor.setAccessible(true);
            Singleton singleton2 =(Singleton) constructor.newInstance(null);
    
            System.out.println(singleton.hashCode());
            System.out.println(singleton1.hashCode());
            System.out.println(singleton2.hashCode());
    
        }
    }
    

    运行结果:

    692404036
    692404036
    1554874502
    

    从结果我们可以看出:放射确实可以调用到已经私有化的构造器,并且构造出不同的对象,从而破坏单例模式。

    那这种情况有没有什么方法可以防止破坏呢?既然要防止破坏,肯定要防止调用私有构造器,也就是调用一次之后,再调用就报错,抛出异常。我们的单例模式可以写成这样:

    import java.io.Serializable;
    
    public class Singleton {
        private static int num = 0;
    
        private volatile static Singleton singleton;
    
        private Singleton() {
            synchronized (Singleton.class) {
                if (num == 0) {
                    num++;
                } else {
                    throw new RuntimeException("Don't use this method");
                }
            }
        }
    
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    测试调用方法不变,测试结果如下,反射调用的时候抛出异常了,说明能够有效阻止反射调用破坏单例的模式:

    Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
        at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
        at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
        at singleton.SingletonTests.main(SingletonTests.java:11)
    Caused by: java.lang.RuntimeException: Don't use this method
        at singleton.Singleton.<init>(Singleton.java:15)
        ... 5 more
    

    2.3 实现了cloneable接口

    如果单例对象已经将构造方法声明成为private,并且重写了构造方法,那么暂时无法调用到构造方法。但是还有一种情况,那就是拷贝,拷贝的时候是不需要经过构造方法的。但是要想拷贝,必须实现Clonable方法,而且需要重写clone方法。

    import java.io.Serializable;
    
    public class Singleton implements Cloneable {
        private static int num = 0;
    
        private volatile static Singleton singleton;
    
        private Singleton() {
            synchronized (Singleton.class) {
                if (num == 0) {
                    num++;
                } else {
                    throw new RuntimeException("Don't use this method");
                }
            }
        }
    
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    }
    

    测试代码如下:

    public class SingletonTests {
        public static void main(String[] args) throws Exception {
            Singleton singleton1=Singleton.getSingleton();
            System.out.println(singleton1.hashCode());
            Singleton singleton2 = (Singleton) singleton1.clone();
            System.out.println(singleton2.hashCode());
        }
    }
    

    运行结果如下,两个对象的hashCode不一致,也就证明了如果继承了Cloneable接口的话,并且重写了clone()方法,则该类的单例就可以被打破,可以创建出不同的对象。但是,这个clone的方式破坏单例,看起来更像是自己主动破坏单例模式,什么意思?

    也就是如果很多时候,我们只想要单例,但是有极少的情况,我们想要多个对象,那么我们就可以使用这种方式,更像是给自己留了一个后门,可以认为是一种良性的破坏单例的方式。

    2.4 序列化破坏单例

    序列化,实际上和clone差不多,但是不一样的地方在于我们很多对象都是必须实现序列化接口的,但是实现了序列化接口之后,对单例的保证有什么风险呢?

    风险就是序列化之后,再反序列化回来,对象的内容是一样的,但是对象却不是同一个对象了。不信?那就试试看:

    单例定义如下:

    import java.io.Serializable;
    
    public class Singleton implements Serializable {
        private static int num = 0;
    
        private volatile static Singleton singleton;
    
        private Singleton() {
            synchronized (Singleton.class) {
                if (num == 0) {
                    num++;
                } else {
                    throw new RuntimeException("Don't use this method");
                }
            }
        }
    
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }
    

    测试代码如下:

    import java.io.*;
    import java.lang.reflect.Constructor;
    
    public class SingletonTests {
        public static void main(String[] args) throws Exception {
    
            Singleton singleton1 = Singleton.getSingleton();
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("file"));
            objectOutputStream.writeObject(singleton1);
            File file = new File("tempFile");
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));
            Singleton singleton2 = (Singleton) objectInputStream.readObject();
            System.out.println(singleton1.hashCode());
            System.out.println(singleton2.hashCode());
        }
    }
    

    上面的代码,先将对象序列化到文件,再从文件反序列化回来,结果如下:

    2055281021
    1198108795
    

    结果证明:两个对象的hashCode不一样,说明这个类的单例被破坏了。

    那么有没有方法在这种情况下,防止单例的破坏呢?答案是:有!!!

    既然调用的是objectInputStream.readObject()来反序列化,那么我们看看里面的源码,里面调用了readObject()方法。

        public final Object readObject()
            throws IOException, ClassNotFoundException {
            return readObject(Object.class);
        }
    

    readObject()方法,里面调用了readObject0()方法:

        private final Object readObject(Class<?> type)
            throws IOException, ClassNotFoundException
        {
            if (enableOverride) {
                return readObjectOverride();
            }
    
            if (! (type == Object.class || type == String.class))
                throw new AssertionError("internal error");
    
            // if nested read, passHandle contains handle of enclosing object
            int outerHandle = passHandle;
            try {
                // 序列化对象
                Object obj = readObject0(type, false);
                handles.markDependency(outerHandle, passHandle);
                ClassNotFoundException ex = handles.lookupException(passHandle);
                if (ex != null) {
                    throw ex;
                }
                if (depth == 0) {
                    vlist.doCallbacks();
                }
                return obj;
            } finally {
                passHandle = outerHandle;
                if (closed && depth == 0) {
                    clear();
                }
            }
        }
    

    readObject0()内部如下,其实是针对不同的类型分别处理:

        private Object readObject0(Class<?> type, boolean unshared) throws IOException {
            boolean oldMode = bin.getBlockDataMode();
            if (oldMode) {
                int remain = bin.currentBlockRemaining();
                if (remain > 0) {
                    throw new OptionalDataException(remain);
                } else if (defaultDataEnd) {
                    /*
                     * Fix for 4360508: stream is currently at the end of a field
                     * value block written via default serialization; since there
                     * is no terminating TC_ENDBLOCKDATA tag, simulate
                     * end-of-custom-data behavior explicitly.
                     */
                    throw new OptionalDataException(true);
                }
                bin.setBlockDataMode(false);
            }
    
            byte tc;
            while ((tc = bin.peekByte()) == TC_RESET) {
                bin.readByte();
                handleReset();
            }
    
            depth++;
            totalObjectRefs++;
            try {
                switch (tc) {
                    // null
                    case TC_NULL:
                        return readNull();
                    // 引用类型
                    case TC_REFERENCE:
                        // check the type of the existing object
                        return type.cast(readHandle(unshared));
                    // 类
                    case TC_CLASS:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast a class to java.lang.String");
                        }
                        return readClass(unshared);
    
                    // 代理
                    case TC_CLASSDESC:
                    case TC_PROXYCLASSDESC:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast a class to java.lang.String");
                        }
                        return readClassDesc(unshared);
    
                    case TC_STRING:
                    case TC_LONGSTRING:
                        return checkResolve(readString(unshared));
    
                    // 数组
                    case TC_ARRAY:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an array to java.lang.String");
                        }
                        return checkResolve(readArray(unshared));
    
                    // 枚举
                    case TC_ENUM:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an enum to java.lang.String");
                        }
                        return checkResolve(readEnum(unshared));
    
                    // 对象
                    case TC_OBJECT:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an object to java.lang.String");
                        }
                        return checkResolve(readOrdinaryObject(unshared));
    
                    // 异常
                    case TC_EXCEPTION:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an exception to java.lang.String");
                        }
                        IOException ex = readFatalException();
                        throw new WriteAbortedException("writing aborted", ex);
    
                    case TC_BLOCKDATA:
                    case TC_BLOCKDATALONG:
                        if (oldMode) {
                            bin.setBlockDataMode(true);
                            bin.peek();             // force header read
                            throw new OptionalDataException(
                                bin.currentBlockRemaining());
                        } else {
                            throw new StreamCorruptedException(
                                "unexpected block data");
                        }
    
                    case TC_ENDBLOCKDATA:
                        if (oldMode) {
                            throw new OptionalDataException(true);
                        } else {
                            throw new StreamCorruptedException(
                                "unexpected end of block data");
                        }
    
                    default:
                        throw new StreamCorruptedException(
                            String.format("invalid type code: %02X", tc));
                }
            } finally {
                depth--;
                bin.setBlockDataMode(oldMode);
            }
        }
    

    可以看到处理对象的时候,调用了readOrdinaryObject()方法,好家伙来了:

        private Object readOrdinaryObject(boolean unshared)
            throws IOException
        {
            if (bin.readByte() != TC_OBJECT) {
                throw new InternalError();
            }
    
            ObjectStreamClass desc = readClassDesc(false);
            desc.checkDeserialize();
    
            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);
            }
    
            passHandle = handles.assign(unshared ? unsharedMarker : obj);
            ClassNotFoundException resolveEx = desc.getResolveException();
            if (resolveEx != null) {
                handles.markException(passHandle, resolveEx);
            }
    
            if (desc.isExternalizable()) {
                readExternalData((Externalizable) obj, desc);
            } else {
                readSerialData(obj, desc);
            }
    
            handles.finish(passHandle);
    
            // 如果实现了hasReadResolveMethod()方法
            if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod())
            {
                // 执行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;
        }
    

    从上面的diamante可以看出,底层是通过反射来实现序列化的,那我们如果不希望它进行反射怎么办?然后可以看到反射之后,其实有一个查找readResolveMethod()方法有关,如果有实现readResolveMethod(),那就直接调用该方法返回结果,而不是返回反射调用之后的结果。这样虽然反射了,但是不起作用。

    那要是我们重写readResolveMethod()方法,就可以直接返回我们的对象,而不是返回反射之后的对象了。

    试试?

    image

    我们将单例模式改造成为这样:

    import java.io.Serializable;
    
    public class Singleton implements Serializable,Cloneable {
        private static int num = 0;
    
        private volatile static Singleton singleton;
    
        private Singleton() {
            synchronized (Singleton.class) {
                if (num == 0) {
                    num++;
                } else {
                    throw new RuntimeException("Don't use this method");
                }
            }
        }
    
        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
        // 阻止反序列反射生成对象
        private Object readResolve() {
            return singleton;
        }
    }
    

    测试代码不变,结果如下,事实证明确实是这样,反序列化不会重新反射对象了,一直是同一个对象,问题完美解决了。

    2055281021
    2055281021
    

    3. 小结

    一个稍微完美的单例,是不会让别人调用构造器的,但是private的构造器,并不能完全阻止对单例的破坏,如果使用反射还是可以非法调用到构造器,因为我们需要一个次数,构造器如果调用次数过多,那么就直接报错。

    但是有时候我们希望留个小后门,所以我们大部分时候不可以破坏单例模式。通过实现cloneable的方式,重写了clone()方法,就可以做到,生成不同的对象。

    序列化和clone(),有点像,都是主动提供破坏的方法,但是很多时候不得已提供序列化接口,却不想被破坏,这个时候可以通过重写readResolve()方法,直接返回对象,不返回反射生成的对象,保护了单例模式不被破坏。

    【作者简介】
    秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使缓慢,驰而不息。写好每一篇文章,期待和你们一起交流。

    开源编程文档:https://damaer.github.io/Coding/#/

    相关文章

      网友评论

          本文标题:设计模式【1.1】-- 你想如何破坏单例模式?

          本文链接:https://www.haomeiwen.com/subject/ukawzktx.html