设计模式——单例模式的破坏

概述:

  • 之前学习了单例模式的几种实现,解决了多线程情况下,单例的线程安全问题,保证了单例的实现。但是单例模式在下面两种情况下也会被破坏:反射序列化

反射:

  • 通过反射是可以破坏单例的,例如使用内部类实现的单例。通过反射获取其默认的构造函数,然后使默认构造函数可访问,就可以创建新的对象了。如下面代码:
public class MainClass {

    public static void main(String[] args) {

        PersonLazyInnerClass person1 = PersonLazyInnerClass.getPersonLazyInnerClass();
        PersonLazyInnerClass person2 = null;
        try {
            Class<PersonLazyInnerClass> cla = PersonLazyInnerClass.class;
            //获得默认构造函数
            Constructor<PersonLazyInnerClass> cons = cla.getDeclaredConstructor();
            //使默认构造函数可访问
            cons.setAccessible(true);
            //创建对象
            person2 = cons.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(person1.hashCode());
        System.out.println(person2.hashCode());
    }
}
  • 测试结果


    image.png
  • 所以想要阻止破坏单例,思路是阻止外部能调用类的构造函数一次以上。所以可以增加一个标志位,用于判断构造函数是否被调用过。如下代码:

public class PersonLazyInnerClassSafe {

    //申明一个标志位,用于标志构造函数是否被调用过
    private static Boolean alreadyNew = false;

    private PersonLazyInnerClassSafe(){
        //加锁防止并发
        synchronized (PersonLazyInnerClassSafe.class){
            //第一次被调用,仅修改标志位;后续被调用抛异常
            if(alreadyNew == false){
                alreadyNew = true;
            }else {
                throw new RuntimeException("单例模式被破坏!");
            }
        }
    }

    private static class Holder{
        private static PersonLazyInnerClassSafe personLazyInnerClassSafe = new PersonLazyInnerClassSafe();
    }

    public static PersonLazyInnerClassSafe getInstance(){
        return Holder.personLazyInnerClassSafe;
    }
}
  • 测试结果


    image.png
  • 增加标志位的确能阻止单例的破坏,但是这个代码有一个BUG,那就是如果单例是先用的反射创建的,那如果你再用正常的方法getInstance()获取单例,就会报错。因为此时标志位已经标志构造函数被调用过了。这种写法除非你能保证getInstance先于反射执行。
  • 先试正常执行代码,此时是可以正确返回单例。这里简单解释一下,person2执行的时候不会去调用PersonLazyInnerClassSafe类的构造函数。因为Holder内部类里面personLazyInnerClassSafe属性是静态的,静态属性在类加载的时候就初始化一次,在生命周期内就不会再被初始化了。
public class MainClass {

    public static void main(String[] args) {
        //正常执行代码
        PersonLazyInnerClassSafe person1 = PersonLazyInnerClassSafe.getInstance();
        PersonLazyInnerClassSafe person2 = PersonLazyInnerClassSafe.getInstance();
        System.out.println(person1.hashCode());
        System.out.println(person2.hashCode());
    }
}
  • 先反射后正常调用,此时会发现正常调用没法用了。
public class MainClass {

    public static void main(String[] args) {
        //先反射获取单例
        PersonLazyInnerClassSafe person2 = null;
        try {
            Class<PersonLazyInnerClassSafe> cla = PersonLazyInnerClassSafe.class;
            //获得默认构造函数
            Constructor<PersonLazyInnerClassSafe> cons = cla.getDeclaredConstructor();
            //使默认构造函数可访问
            cons.setAccessible(true);
            //调用默认构造函数,创建对象
            person2 = cons.newInstance();
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println("反射对象: " + person2.hashCode());
        //在用正常的方法获取单例,此时会报错
        PersonLazyInnerClassSafe person1 = PersonLazyInnerClassSafe.getInstance();
        System.out.println("正常获取" + person1.hashCode());
    }
}
  • 测试结果


反序列化:

1. 破坏单例
  • 反序列化也是一种会破坏单例的方法。简单来讲,反序列化也是通过反射调用newInstance()实例化对象,下面我一步步解释。例如下面代码可以通过反序列化破坏单例:
public class MainClass {

    public static void main(String[] args) {
        //序列化
        PersonLazyInnerClassSafe person3 = PersonLazyInnerClassSafe.getInstance();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempfile"));
        out.writeObject(PersonLazyInnerClassSafe.getInstance());

        File file  = new File("tempfile");
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        //调用readObject()反序列化
        PersonLazyInnerClassSafe person4 = (PersonLazyInnerClassSafe)in.readObject();
        System.out.println("正常构造:" + person3.hashCode());
        System.out.println("反序列化Person:" + person4.hashCode());
    }
}
  • 测试结果
    可以看到,反序列化后,生成了一个新的实例。


    image.png
  • 原理解释
    反序列化为什么能生成新的实例,必须从源码看起。这里分析readObject()里面的调用源码。会发现readObject()方法后进入了readObject0(false)方法。
    public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);  //通过debug会发现进入此方法
            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方法,会发现进入了readOrdinaryObject()方法。

    private Object readObject0(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++;
        try {
            switch (tc) {
                ...省略部分源码

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));   //会进入该逻辑

                case TC_EXCEPTION:
                    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()中有两处关键代码,其中关键代码1中的关键语句为:

obj = desc.isInstantiable() ? desc.newInstance() : null;

  • 此处代码是通过描述对象desc,先判断类是否可以实例化,如果可以实例化,则执行desc.newInstance()通过反射实例化类,否则返回null。
   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  //读取Class
                || cl == ObjectStreamClass.class) {
            throw new InvalidClassException("invalid class descriptor");
        }
        
        /**===============关键代码1====================== **/
        Object obj;
        try {
            obj = desc.isInstantiable() ? desc.newInstance() : null;  //通过类描述信息,初始化对象obj。
        } catch (Exception ex) {
            throw (IOException) new InvalidClassException(
                desc.forClass().getName(),
                "unable to create instance").initCause(ex);
        }
         /**===============关键代码1====================== **/
        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);
         /**===============关键代码2====================== **/
        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) {
                handles.setObject(passHandle, obj = rep);
            }
        }
         /**===============关键代码2====================== **/
        return obj;
    }
  • 此处通过debug可以发现,类是可实例化的,所以执行desc.newInstance() 去实例化对象,此处就是重点,让我疑问的地方了。我们知道newInstance必定会调用默认的无参构造函数。但是我们在上面PersonLazyInnerClassSafe单例类已经禁止了反射创建新实例,但是反序列化还是能创建出新实例,那么此处的反序列化中的反射具体是如何执行的呢?继续分析newInstance()源码。
  • newInstance()的源码如下所示,可以发现,程序会自动加载合适的构造函数cons,然后再去根据这个构造函数去cons.newInstance()创建对象。通过debug发现,此处的cons对象是Object,真像大白了。所以反序列化是通过Object的构造函数去反射,生成了新的实例
/** serialization-appropriate constructor, or null if none */
private Constructor<?> cons;
...省略部分源码
Object newInstance()
        throws InstantiationException, InvocationTargetException,
               UnsupportedOperationException
    {
        requireInitialized();
        if (cons != null) {
            try {
                return cons.newInstance();
            } catch (IllegalAccessException ex) {
                // should not occur, as access checks have been suppressed
                throw new InternalError(ex);
            }
        } else {
            throw new UnsupportedOperationException();
        }
    }
  • 此处可以给大家看下debug 的截图


    image.png
  • 然后我改写了上面的测试序列化破坏单例的代码,验证一下反序列化是通过Object的构造函数去反射,生成了新的实例,具体代码如下
public class MainClass {

    public static void main(String[] args) {
        //验证一下反序列化是通过Object的构造函数去反射,生成了新的实例
        PersonLazyInnerClassSafe person3 = PersonLazyInnerClassSafe.getInstance();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tempfile"));
        out.writeObject(PersonLazyInnerClassSafe.getInstance());

        File file  = new File("tempfile");
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
        //反序列化生成的Object对象
        Object ob = in.readObject();
        //将Object强转为PersonLazyInnerClassSafe
        PersonLazyInnerClassSafe person4 = (PersonLazyInnerClassSafe)ob;
        System.out.println("正常构造:" + person3.hashCode());
        System.out.println("反序列化Object:" + ob.hashCode());
        System.out.println("反序列化Person:" + person4.hashCode());
    }
}
  • 其中会发现反序列化出来的Object对象强制转换为PersonLazyInnerClassSafe时,只是把ob 对象的引用指向了person4 ,并没有调用PersonLazyInnerClassSafe 的构造函数。

//反序列化生成的Object对象
Object ob = in.readObject();
//将Object强转为PersonLazyInnerClassSafe
PersonLazyInnerClassSafe person4 = (PersonLazyInnerClassSafe)ob;

  • 运行结果如下,可以看到ob和person4 是相同的对象:


    image.png
2. 阻止破坏单例
  • 上面解释了那么多序列化是如何通过反射破坏的单例,那么如何阻止单例被序列化破坏呢?关键点在上面readOrdinaryObject()方法源码的关键代码2中,下面单独截取该段代码:
        if (obj != null &&
            handles.lookupException(passHandle) == null &&
            desc.hasReadResolveMethod())   //判断对象是否实现readResolve方法  
        {
            Object rep = desc.invokeReadResolve(obj); //反射调用对象的readResolve方法  
            if (unshared && rep.getClass().isArray()) {
                rep = cloneArray(rep);
            }
            if (rep != obj) {  //如果对象readResolve返回的对象与 默认序列化对象不等,返回readResolve方法返回的对象  
                handles.setObject(passHandle, obj = rep);
            }
        }
  • 通过阅读上面代码的注释,我们即可知道,只需要实现我们自己的readResolve()方法,在这个方法中返回我们想要的单例,就可以解决序列化破坏单例。具体改写如下:
public class PersonLazyInnerClassSafe implements Serializable{

    //申明一个标志位,用于标志构造函数是否被调用过
    public static Boolean alreadyNew = false;

    private PersonLazyInnerClassSafe(){
        System.out.println("调用构造函数!!!");
        //加锁防止并发
        synchronized (PersonLazyInnerClassSafe.class){
            //第一次被调用,仅修改标志位;后续被调用抛异常
            if(alreadyNew == false){
                alreadyNew = true;
            }else {
                throw new RuntimeException("单例模式被破坏!");
            }
        }
    }

    private static class Holder{
        private static PersonLazyInnerClassSafe personLazyInnerClassSafe = new PersonLazyInnerClassSafe();
    }

    public static PersonLazyInnerClassSafe getInstance(){
        return Holder.personLazyInnerClassSafe;
    }

    //阻止序列化破坏单例
    private Object readResolve(){
        return Holder.personLazyInnerClassSafe;
    }
}
  • 测试如下,成功解决单例被破坏问题


    image.png

总结:

  1. 破坏单例有两种方式 反射、反序列化
  2. 反射破坏的原理是:通过反射获取其默认的构造函数,并且改变其构造函数的访问域,从而实现调用构造函数创建新实例。解决方案是:在构造函数中增加一个标志位,用于判断构造函数是否被调用过,阻止外部能调用类的构造函数一次以上。
  3. 反序列化破坏构造函数的原理:通过Object的构造函数,反射出单例类对象,从而创建了新的实例。解决方案是:在单例类中写一个readResolve()方法,在这个方法中返回我们想要的单例,就可以解决序列化破坏单例。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 单例模式(SingletonPattern)一般被认为是最简单、最易理解的设计模式,也因为它的简洁易懂,是项目中最...
    成热了阅读 4,299评论 4 34
  • 前言 本文主要参考 那些年,我们一起写过的“单例模式”。 何为单例模式? 顾名思义,单例模式就是保证一个类仅有一个...
    tandeneck阅读 2,541评论 1 8
  • 在Java中,单例模式分为很多种,本人所了解的单例模式有以下几种,如有不全还请大家留言指点: 饿汉式 懒汉式/Do...
    Duang了个Duang阅读 1,453评论 0 2
  • 隧道里暗风涌动,我闻到了她身上的味道。 汗汗的夹杂着香味,我默默的猜测她的工作。 是去镇上刚卖完特产吗? 特产是从...
    由緒阅读 170评论 0 2
  • 海南——一个我曾计划环岛骑行毕业游,却未能成行之处。 上天却也很给面子,老师安排了我过来这边出差,反而能够“公费游...
    前行的始者007阅读 182评论 0 1