设计模式6,单例模式(中)

目录

序列化和反序列化破坏单例模式

反射破坏单例模式

原型模式破坏单例模式

枚举单例模式


接下来说一下破坏单例模式的办法和解决途径。

序列化和反序列化破坏单例模式

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

单例类需要实现Serializable接口

运行结果:

 可以看出,这就不是一个结果了。想要解决这个问题,只需要在单例类中加一个方法就好

 运行结果:

那为什么加上这个方法就可以呢?他也并不是接口中的实现方法,这就要从输入输出流说起,首先看下ObjectInputStream#readObject源码:

通过方法返回值可以看出,返回的是obj,那么看一下readObject0这个方法

通过debug可以看到进入了这个方法

返回是obj,所以还是看一下obj的创建的地方,进入这个方法

 通过注释可以看出,如果这个类是可序列化的,那么就返回true,所以从这可以看出,这个obj是通过反射创建出的新对象。这也就解释了为什么通过序列化和反序列化把单例模式破坏了。但还没有找到原因,再往下看

看下这个判断

通过注释可以看出,如果这个类是可序列化的,并且定义了一个readResolve方法,就返回true。所以这里返回true,接下来看invokeReadResolve

看注释很清楚的看出,这个方法就是运行resolveMethod的,然后再赋值回obj。所以结果也就相同了。

那回过头再看这种方式,虽然结果是一样的,但是在处理过程中实际上还是new了一个新的对象,所以对于序列化和反序列化对单例的破坏还是要注意的。


反射破坏单例模式

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = HungrySingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor();
        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);
    }
}

运行结果:

这就是反射攻击,利用反射获取到单例类的构造器,再把构造器权限打开,再用构造器就可以new一个新的对象了。

如何防御这种攻击?既然是通过跳过java程序检查权限的方式调用构造器,那么我们在构造器里做判断就可以了。

这种方式适用于在类加载的时候就创建对象的情况,同样也适用于静态内部类的方式 。但这种方式不适用于懒汉式单例,举个例子:

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = LazySingleton.class;
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        LazySingleton newInstance = (LazySingleton) constructor.newInstance();

        LazySingleton instance = LazySingleton.getInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

返回结果是不同的,这是跟创建顺序有关,懒汉式并不是在一开始就new的,而是在调用getInstance方法才会new,所以上面代码先用反射创建了一个对象,再调用instance,所以就出现了两个。所以反射的能力相当大。


原型模式破坏单例模式

也就是用克隆的方式来破坏单例模式。如果一个单例实现了Cloneable接口

public class HungrySingleton implements Serializable,Cloneable {
    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;
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

利用反射中的机制调用克隆方法 

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        HungrySingleton instance = HungrySingleton.getInstance();
        Method method = instance.getClass().getDeclaredMethod("clone");
        method.setAccessible(true);
        HungrySingleton cloneInstance = (HungrySingleton) method.invoke(instance);
        System.out.println(instance);
        System.out.println(cloneInstance);
    }
}

 结果就不一样了,如何防御?1.不实现Cloneable接口。2.重写clone方法

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return getInstance();
    }

枚举单例模式

上面说了关于单例模式的攻击,也知道反射的强大。而枚举类的单例,可以抵御序列化与反序列化,也可以抵御反射攻击。

对于序列化攻击

public enum EnumInstance {
    INSTANCE;
    private Object data;

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }

    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}
public class Test {
    public static void main(String[] args) throws 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));
        EnumInstance newInstance = (EnumInstance) ois.readObject();

        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}

结果:

看下源码:

同样是ObjectInputStream#readObject--readObject0

这里的选择就是readEnum了,进去看看

 通过readString获取枚举对象的名称,然后根据名称获取到对应的枚举常量。因为枚举中名称都是唯一的,所以拿到的枚举常量也是唯一的,也就维持了枚举的单例属性。

对于反射攻击

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = EnumInstance.class;
        Constructor constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumInstance newInstance = (EnumInstance) constructor.newInstance();

        EnumInstance instance = EnumInstance.getInstance();
        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}

报错

枚举并不含有无参构造器,那么调整一下代码

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Class clazz = EnumInstance.class;
        Constructor constructor = clazz.getDeclaredConstructor(String.class , int.class);
        constructor.setAccessible(true);
        EnumInstance newInstance = (EnumInstance) constructor.newInstance("耗子" , 666);

        EnumInstance instance = EnumInstance.getInstance();
        System.out.println(instance.getData());
        System.out.println(newInstance.getData());
        System.out.println(instance.getData() == newInstance.getData());
    }
}

还是报错

不能通过反射创建枚举类型,进报错位置源码看看

 所以说,单例模式用枚举还是比较方便的,也是官方比较推荐的方式。

顺便提一嘴,枚举类型的运用

public enum EnumInstance {
    INSTANCE{
        protected void printTest(){
            System.out.println("耗子打印测试");
        }
    };
    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;
    }
}

调用:

EnumInstance instance = EnumInstance.getInstance();
instance.printTest();

非常方便。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值