以前在学习单例模式的时候从没考虑过安全的问题,一直觉得单例是无懈可击的,今天我来教你如何破坏单例模式以及应对方法。java
首先来看看下面这个常见的单例写法面试
public class Singleton( private Singleton(){} private static class SingletonInstance{ private static final Singleton instance=new Singleton(); } public static Singleton getInstance(){ return SingletonInstance.instance; } )
看起来无懈可击,那么真的是这样吗?安全
咱们知道要破坏单例,则必须建立对象,那么咱们顺着这个思路走,建立对象的方式无非就是new,clone,反序列化,以及反射。学习
单例模式的首要条件就是构造方法私有化,因此new
这种方式去破坏单例的可能性是不存在的
要调用clone方法,那么必须实现Cloneable
接口,可是单例模式是不能实现这个接口的,所以排除这种可能性。
所以咱们本篇来讨论一下反序列化和反射如何对单例模式进行破坏。测试
序列化是破坏单例模式的一大利器。其与克隆性质有些类似,须要类实现序列化接口,相比于克隆,实现序列化在实际操做中更加不可避免,有些类,它就是必定要序列化。this
下面咱们来作个测试,在上面的单例模式中实现序列化接口,以下spa
public class Singleton implements Serializable {
而后咱们对拿到的对象进行序列化和反序列进行测试debug
public class Test { public static void main(String[] args) throws Exception { //序列化 Singleton instance1 = Singleton.getInstance(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile")); objectOutputStream.writeObject(instance1); //反序列化 File file = new File("tempFile"); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file)); Singleton instance2 = (Singleton) objectInputStream.readObject(); System.out.println(instance1 == instance2); } }
执行结果为 3d
经过对Singleton的序列化与反序列化获得的对象是一个新的对象,这就破坏了Singleton的单例性。code
接下来咱们来试着打个断点debug一下
能够看到进入了readObject0
这个方法里,咱们进去看看
继续下一步会走到readOrdinaryObject
方法中,能够看到其实反序列化底层也是使用反射帮咱们建立了一个新的对象。
那是否是咱们就不能阻止单例被破坏了呢?并非!
如今咱们在Singleton
类中加上了一个readResolve
方法,该方法返回了INSTANCE
实例,而后从新执行一下:
public class Singleton implements Serializable { private Singleton() { } private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } private Object readResolve() { return SingletonInstance.INSTANCE; } }
结果居然为true,也就是说序列化和反序列出来的是同一个对象!
那这究竟是什么原理,咱们来看看刚才的readOrdinaryObject
方法:
看到上面应该很清楚了,在条件判断中 desc.hasReadResolveMethod()
会判断是否有readResolve()
方法,若是有的话会经过desc.invokeReadResolve(obj)
去反射调用该方法,返回的就是同一个对象。
看到这里小伙伴们应该明白了,总结一句话就是:若是想要防止单例被反序列化破坏。就让单例类实现readResolve()
方法。
说完反序列化破坏单例,那如今咱们来看看反射如何破坏单例模式:
public class Test { public static void main(String[] args) throws Exception { Singleton instance1 = Singleton.getInstance(); //经过反射建立对象 Class<Singleton> singletonClass = Singleton.class; Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(); //暴力破解私有构造器 constructor.setAccessible(true); Singleton instance2 = constructor.newInstance(); System.out.println(instance1 == instance2); } }
执行结果为 false,也就是说经过反射也可以破坏单例模式。
咱们如何应对呢?
即使是经过反射来建立实例,也是调用类中的构造器来实现的,因此咱们能够在构造器中作文章。
改造Singleton
类中的私有构造器以下:
public class Singleton implements Serializable { private Singleton() { if (SingletonInstance.INSTANCE != null) { throw new RuntimeException("不容许反射调用构造器"); } } private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } private Object readResolve() { return SingletonInstance.INSTANCE; } }
执行结果:
很显然报异常了,这样便防止了这种方法实现的单例模式被反射破坏。
饿汉式
实现的单例模式均可以这样来防止单例模式被反射破坏。 懒汉式实现的单例模式是不能够防止被反射破坏的。
如今咱们用双重检查锁式
实现的单例模式来进行测试:
public class Singleton { private static volatile Singleton instance ; private Singleton(){ if(instance != null){ throw new RuntimeException("不容许反射调用构造器"); } } public static Singleton getInstance(){ if(instance == null){ synchronized (Singleton.class){ if(instance == null){ instance = new Singleton(); } } } return instance; } }
从新执行一下,结果仍是同样,那这样就没问题了吗?不!
如今咱们修改一下测试类:
public class Test { public static void main(String[] args) throws Exception { //经过反射建立单例对象 Class<Singleton> singletonClass = Singleton.class; Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(); constructor.setAccessible(true); Singleton instance2 = constructor.newInstance(); //获取单例对象 Singleton instance1 = Singleton.getInstance(); System.out.println(instance1 == instance2); } }
调整一下顺序,如今咱们先使用反射建立对象,再调用单例的getInstance()
方法,结果以下:
咱们把经过反射建立实例
和调用静态方法getInstance()
得到实例的位置互换了,因此一开始经过反射建立实例
调用构造器,此时构造器中的判断instance != null
是无用的,因此这种方法是不适用懒汉式
实现的单例模式来防止被反射破坏的。
总结:若是从此须要本身手动实现一个单例的话,能够选择 构造器判断
+ 实现 readResolve()
方法的方式
来防止单例被破坏。
那么有没有更简单的方法呢?答案是有的!
若是不想在构造器内部加判断,也不想写readResolve()
方法,那你能够选择使用枚举来实现单例模式。
在StakcOverflow中,有一个关于 What is an efficient way to implement a singleton pattern in Java? 的讨论:
如上图,得票率最高的回答是:使用枚举。
回答者引用了Joshua Bloch大神在《Effective Java》中明确表达过的观点:
使用枚举实现单例的方法虽然尚未普遍采用,可是单元素的枚举类型已经成为实现Singleton的最佳方法。接下来咱们来看看如何使用枚举实现单例:
public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; } }
能够看到相比双重检查等单例模式,使用枚举实现的单例模式更加优雅,那么上面这个代码是安全的吗,仍是用原来的测试代码来测试一下:
public class Test { public static void main(String[] args) throws Exception { Singleton instance1 = Singleton.getInstance(); //经过反射建立对象 Class<Singleton> singletonClass = Singleton.class; Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(); constructor.setAccessible(true); Singleton instance2 = constructor.newInstance(); System.out.println(instance1 == instance2); } }
结果会报 Exception in thread "main" java.lang.NoSuchMethodException
简单来讲就是由于SingletonClass.getDeclaredConstructors()
获取全部构造器,会发现并无咱们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,由于一旦一个类声明为枚举,实际上就是继承了Enum,来看看Enum类源码:
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable { private final String name; public final String name() { return name; } private final int ordinal; public final int ordinal() { return ordinal; } protected Enum(String name, int ordinal) { this.name = name; this.ordinal = ordinal; } //余下省略
看下Enum源码就明白,这两个参数是name和ordial两个属性,由于继承了父类构造器,因此在刚才的测试中才会找不到无参构造器,那么是否是咱们去调用父类的构造器就能够了呢?咱们来测试一下:
public class Test { public static void main(String[] args) throws Exception { Singleton instance1 = Singleton.getInstance(); //经过反射建立对象 Class<Singleton> singletonClass = Singleton.class; //调用父类构造器 Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(String.class, int.class); constructor.setAccessible(true); Singleton instance2 = constructor.newInstance(); System.out.println(instance1 == instance2); } }
注意:咱们在上面经过singletonClass.getDeclaredConstructor(String.class, int.class)
来调用父类构造器,来看下执行结果:
来看看在哪里抛出的异常:
总结来讲就是反射在经过newInstance建立对象时,会检查该类是否ENUM修饰,若是是则抛出异常,反射失败。因此枚举是不怕发射攻击的。
那枚举又是如何避免被反序列化来建立新对象的呢?
枚举对象的序列化、反序列化有本身的一套机制。序列化时,仅仅是将枚举对象的name
属性输出到结果中,反序列化的时候则是经过java.lang.Enum的valueOf()
方法来根据名字查找枚举对象。
下面分析一下valueOf
源码:
再来看看enumConstantDirectory()
源码:
继续看getEnumConstantsShared()
源码:
getEnumConstantsShared()
方法获取枚举类的values()
方法,而后获得枚举类所建立的全部枚举对象。
每一个枚举对象都有一个惟一的name
属性。序列化只是将name
属性序列化,在反序列化的时候,经过建立一个Map(key,value)
,搭建起name
和与之对应的对象之间的联系,而后经过索引key
来得到枚举对象。
总的来讲就是枚举在反序列化的过程当中并无建立新的对象,而经过name
属性拿到原有的对象,所以保证了枚举类型实现单例模式的序列化安全。
若是从此要本身手动实现一个单例模式首先推荐使用枚举来实现,在面试被问到单例模式的时候也能够和面试官吹吹牛逼了,今天就暂时学习到这里,若是有什么不对的地方请多多指教。