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

概述:

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

反射:

  • 通过反射是可以破坏单例的,例如使用内部类实现的单例。通过反射获取其默认的构造函数,然后使默认构造函数可访问,就可以创建新的对象了。如下面代码:
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()方法,在这个方法中返回我们想要的单例,就可以解决序列化破坏单例。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容

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