• java设计模式——单例模式(二)


     破坏单例模式

    上一章节,介绍了单例模式的几种方式,这次来学习一波我们创建的单例模式是否安全,能不能破坏。换句话说,也就是在程序运行中,不止有一个实例。

    一. 序列化,反序列化破坏

    以饿汉式的单例模式为例,先看下面的代码:

    /**
     * @program: designModel
     * @description: 饿汉式,与懒汉式最大的区别,就是延时加载,但是饿汉式如果不用该实例,会占用资源
     * @author: YuKai Fan
     * @create: 2018-12-04 16:57
     **/
    public class HungrySingleton implements Serializable {
        private final static HungrySingleton hungrySingleton;
    
        static {
            hungrySingleton = new HungrySingleton();
        }
        private HungrySingleton() {
    
            }
    
        }
        public static HungrySingleton getInstance() {
            return hungrySingleton;
        }
    
    }
    /**
     * @program: designModel
     * @description:
     * @author: YuKai Fan
     * @create: 2018-12-04 14:07
     **/
    public class Test {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
    
            HungrySingleton instance = HungrySingleton.getInstance();
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
            oos.writeObject(instance);
    
            File file = new File("singleton_file");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
    
            HungrySingleton newInstance = (HungrySingleton)ois.readObject();
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
        }
    }

    上面这段代码的输出结果是

    可以看出,产生了两种不同的实例,并输出false。

     那么为什么会这样,在输出流,和输入流的过程中,会将类进行序列化,但是输出的实例与输入的实例确实不一样的,这样单例模式就别破坏了。

    现在改动一下上面的饿汉式单例模式代码,添加一个Object类型的readResolve方法,然后返回这个实例:

    /**
     * @program: designModel
     * @description: 饿汉式,与懒汉式最大的区别,就是延时加载,但是饿汉式如果不用该实例,会占用资源
     * @author: YuKai Fan
     * @create: 2018-12-04 16:57
     **/
    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;
        }
    
        private Object readResolve() {
            return hungrySingleton;
        }
    }

    在运行一下,得到的结果为:

    可以看到,结果是true,只存在一个实例。

    这其中的原理需要来解读ObjectInputStream的readObject()  源码才能知晓。

    //ObjectInputStream中的readObject方法,会根据实例类中的是否存在readResolve方法,来返回最终的实例是原来的,还是创建新的
            
            方法readObject:
                try {
                    Object obj = readObject0(false);
                    handles.markDependency(outerHandle, passHandle);
                }
    
            方法readObject0:
                case TC_OBJECT:
                        return checkResolve(readOrdinaryObject(unshared));
    
            方法readOrdinaryObject:
                Object obj;
                try {
                    //判断实例类是不是序列化的
                    obj = desc.isInstantiable() ? desc.newInstance() : null;
                }
                //如果是序列化的,在判断是否有readResolve方法
                if (obj != null &&
                    handles.lookupException(passHandle) == null &&
                    desc.hasReadResolveMethod())
                {
                    Object rep = desc.invokeReadResolve(obj);}
    
            方法hasReadResolveMethod:
                /**
                 * Returns true if represented class is serializable or externalizable and
                 * defines a conformant readResolve method.  Otherwise, returns false.
                 * 这个注释就是表达了,是否存在这个readResolve方法
                 */
                boolean hasReadResolveMethod() {
                    return (readResolveMethod != null);
                }
    
            方法invokeReadResolve:
                if (readResolveMethod != null) {
                try {
                    //利用反射的invoke,来调用实例中的readResolve方法,返回实例
                    return readResolveMethod.invoke(obj, (Object[]) null);
                }        

    上面的源码了解到,为什么只在代码中添加了一个readResolve()方法,就解决了序列化攻击。

    在readObject中的一层层封装的方法中,readOrdinaryObject()会判断类是否序列化,如果是,则调用hasReadResolveMethod()判断是否有readResolve()方法,如果存在readResolve()方法就调用invokeReadResolve()方法,利用反射来获取类中readResolve方法返回的实例。

    二. 反射攻击破坏

    之前学过单例模式,知道了。在单例模式中,通过创建一个私有的无参构造器在阻止类在其他地方被创建,从而保证只有一个实例。但是,通过反射的方式,可以改变构造器的类型,即改为public。看下面代码:

    /**
     * @program: designModel
     * @description: 通过反射,来获取新的实例,破坏单例模式
     * @author: YuKai Fan
     * @create: 2018-12-05 14:55
     **/
    public class Test3 {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
    
            //饿汉式
            Class objectClass = HungrySingleton.class;
            Constructor constructor = objectClass.getDeclaredConstructor();
            //通过反射,将类的构造器权限改为了public,这样就这样new出新的实例
            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);
        }   
    }

    得到的结果是:false

    从上面代码可以看到,利用反射的getDeclaredConstructor()方法将构造器的权限修改为public,这样就可以创建不同的实例了

    那么怎么样来阻止呢?

    看下修改后的,饿汉式单例模式的代码:直接在私有构造器中加上一个判断即可

    /**
     * @program: designModel
     * @description: 饿汉式,与懒汉式最大的区别,就是延时加载,但是饿汉式如果不用该实例,会占用资源
     * @author: YuKai Fan
     * @create: 2018-12-04 16:57
     **/
    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;
        }
    
        private Object readResolve() {
            return hungrySingleton;
        }
    }

    上面的原理,是用饿汉式的特点,在类加载的时候就创建了实例,这样即使改变了构造器的权限也无法判断成功,因为此时实例已经创建了,无法在调用构造器方法。

    但是这仅仅只适用于饿汉式,和静态内部类的方式。如果单例模式是延时加载,那就跟代码的执行顺序有关了。看下面这段代码:

    懒汉式单例模式:

    /**
     * @program: designModel
     * @description: 懒汉单例,懒汉式注重的就是延迟加载,当在使用到这个实例的时候才会初始化
     * @author: YuKai Fan
     * @create: 2018-12-04 14:04
     **/
    public class LazySingleton {
        private static LazySingleton lazySingleton = null;
    //    private static boolean flag = true;
        private LazySingleton() {
            /*if (flag) {
                flag = false;
            } else {
                throw new RuntimeException("单例构造器禁止反射调用");
            }*/
            if (lazySingleton != null) {
                throw new RuntimeException("单例构造器禁止反射调用");
            }
    
        }
    
        //在代码块上加锁,让这个方法每次只能有一个线程访问,这样只会产生一个实例
        //这种方式,锁的是class类,存在加锁和解锁的开销,对性能有一定影响
        public static LazySingleton getInstance() {
            synchronized(LazySingleton.class) {
                if (lazySingleton == null) {
                    lazySingleton = new LazySingleton();
                }
            }
            return lazySingleton;
        }
    }
    /**
     * @program: designModel
     * @description: 通过反射,来获取新的实例,破坏单例模式
     * @author: YuKai Fan
     * @create: 2018-12-05 14:55
     **/
    public class Test3 {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
    
       Class objectClass = LazySingleton.class;
            Constructor constructor = objectClass.getDeclaredConstructor();
            //通过反射,将类的构造器权限改为了public,这样就这样new出新的实例
            constructor.setAccessible(true);
            //如果按照上面两种方式在类加载的时候判断,依旧会就会产生不同的实例
            *//*
                可以看出,这跟创建实例的顺序是有关的,
                如果先执行LazySingleton.getInstance()方法,由于getInstance是同步的,就会先拿到实例,后面反射在获取实例时,此时单例对象已经存在,就会抛出异常
                在多线程环境下,如果获取单例一个线程后执行,反射单例一个线程先执行,那就会产生两个不同的实例
             *//*
            LazySingleton newInstance = (LazySingleton) constructor.newInstance();
            LazySingleton instance = LazySingleton.getInstance();
    
    
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
       }   
    }

    根据上面代码的注释看得出,在延时加载的情况下单例模式的反射攻击并没有有效的防止措施。也不好在私有的构造器中添加判断。那么下面介绍一种推荐的单例模式,利用枚举类的特性来,实现单例。

    枚举类:

    /**
     * @program: designModel
     * @description: 使用枚举实现单例
     * @author: YuKai Fan
     * @create: 2018-12-05 15:55
     **/
    public enum EnumInstance {
        INSTANCE{
            protected void printTest() {
                System.out.println("Print Test");
            }
        };
        protected abstract void printTest();
        private Object data;
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
    
        public static EnumInstance getInstance() {
            return INSTANCE;
        }
    }

    Test:

    package com.javaDesign.designModel.creational.Singleton;
    
    import java.io.*;
    import java.lang.reflect.InvocationTargetException;
    
    /**
     * @program: designModel
     * @description: 通过反射,来获取新的实例,破坏单例模式
     * @author: YuKai Fan
     * @create: 2018-12-05 14:55
     **/
    public class Test3 {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
    
            /**
             * 枚举类型的单例模式下的,反射与序列化攻击
             */
            EnumInstance instance = EnumInstance.getInstance();
            instance.setData(new Object());
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
            oos.writeObject(instance);
    
            File file = new File("singleton_file");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
              /*
                ObjectInputStream中有一个readEnum()方法,这是读取枚举类的方法,它会获取到枚举对象的名称name,
                String name = readString(false);
                Enum<?> result = null;
                Class<?> cl = desc.forClass();
                if (cl != null) {
                    try {
                        @SuppressWarnings("unchecked")
                        //根据name获取到枚举常量,由于name是唯一的,并且对应一个枚举常量,所以对于枚举类,实例也只会产生一个,所以枚举类对于序列化的破坏是不受影响的
                        Enum<?> en = Enum.valueOf((Class)cl, name);
                        result = en;
                    }
               */
            EnumInstance newInstance = (EnumInstance)ois.readObject();
            System.out.println(instance.getData());
            System.out.println(newInstance.getData());
            System.out.println(instance.getData() == newInstance.getData());
    
        }
    }

    阅读源码可知,由于ObjectInputStream中的readEnum()方法,根据枚举类类名来得到唯一的枚举常量,从而只会产生一个实例,所以枚举类对于序列化的破坏是不受影响的

    下面模拟反射攻击:

    package com.javaDesign.designModel.creational.Singleton;
    
    import java.io.*;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.InvocationTargetException;
    
    /**
     * @program: designModel
     * @description: 通过反射,来获取新的实例,破坏单例模式
     * @author: YuKai Fan
     * @create: 2018-12-05 14:55
     **/
    public class Test3 {
        public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException, ClassNotFoundException {
            /**
             * 枚举类型的单例模式下的,反射与序列化攻击
             */
            /*EnumInstance instance = EnumInstance.getInstance();
            instance.setData(new Object());
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
            oos.writeObject(instance);
    
            File file = new File("singleton_file");
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
              *//*
                ObjectInputStream中有一个readEnum()方法,这是读取枚举类的方法,它会获取到枚举对象的名称name,
                String name = readString(false);
                Enum<?> result = null;
                Class<?> cl = desc.forClass();
                if (cl != null) {
                    try {
                        @SuppressWarnings("unchecked")
                        //根据name获取到枚举常量,由于name是唯一的,并且对应一个枚举常量,所以对于枚举类,实例也只会产生一个,所以枚举类对于序列化的破坏是不受影响的
                        Enum<?> en = Enum.valueOf((Class)cl, name);
                        result = en;
                    }
               *//*
            EnumInstance newInstance = (EnumInstance)ois.readObject();
            System.out.println(instance.getData());
            System.out.println(newInstance.getData());
            System.out.println(instance.getData() == newInstance.getData());*/
            Class objectClass = EnumInstance.class;
            /*
                因为枚举类中是没有无参构造器的,所以必须要传两个参数
             */
            Constructor constructor = objectClass.getDeclaredConstructor(String.class, int.class);
            //通过反射,将类的构造器权限改为了public,这样就这样new出新的实例
            constructor.setAccessible(true);
            EnumInstance instance = EnumInstance.getInstance();
            EnumInstance newInstance = (EnumInstance) constructor.newInstance("测试",666);
            /*
                通过反射来获取实例时,会有一个判断,看是否是枚举类,如果是的话就会抛出异常,这样反射攻击也会失败
                newInstance():
                    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
                     throw new IllegalArgumentException("Cannot reflectively create enum objects");
             */
            System.out.println(instance);
            System.out.println(newInstance);
            System.out.println(instance == newInstance);
    
        }
    }

    输出结果为:

    同样阅读newInstance()方法源码,通过反射获取实例,会判断是否是枚举类,这样反射攻击也会无效。

  • 相关阅读:
    类的加载顺序
    自定义形状类
    java的参数传递
    复数相加+equels、hashcode、clone<二>
    复数相加+equels、hashcode、clone<一>
    命令行程序
    计算阶乘
    控制程序的流程
    java运算符
    强制类型转换细节解析
  • 原文地址:https://www.cnblogs.com/FanJava/p/10072419.html
Copyright © 2020-2023  润新知