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

猜你喜欢

转载自www.cnblogs.com/FanJava/p/10072419.html