单例模式相信大家都不陌生,我们不讨论单例模式的几种写法及其优劣。今天我们单独拎出单例的几种实现来看看如何有效的抵御反射及序列化的攻击。如果不了解反射和序列化的可以看这两篇文章。
反射
序列化单例模式最根本的在于类只能有一个实例,如果通过反射来构建这个类的实例,单例模式就会被破坏,下面我们通过例子来看下:
/** * 静态内部类式单例模式 */ class Singleton implements Serializable{ private static class SingletonClassInstance { private static final Singleton instance = new Singleton(); } //方法没有同步,调用效率高 public static Singleton getInstance() { return SingletonClassInstance.instance; } private Singleton() {} } 复制代码
相信大家对于这个单例的这种实现方式肯定不陌生,下面我们来看看通过反射来创建类实例会不会破坏单例模式。main函数代码如下:
Singleton sc1 = Singleton.getInstance(); Singleton sc2 = Singleton.getInstance(); System.out.println(sc1); // sc1,sc2是同一个对象 System.out.println(sc2); /*通过反射的方式直接调用私有构造器(通过在构造器里抛出异常可以解决此漏洞)*/ Class<Singleton> clazz = (Class<Singleton>) Class.forName("com.learn.example.Singleton"); Constructor<Singleton> c = clazz.getDeclaredConstructor(null); c.setAccessible(true); // 跳过权限检查 Singleton sc3 = c.newInstance(); Singleton sc4 = c.newInstance(); System.out.println("通过反射的方式获取的对象sc3:" + sc3); // sc3,sc4不是同一个对象 System.out.println("通过反射的方式获取的对象sc4:" + sc4); 复制代码
下面我们来看输出:
com.learn.example.Singleton@52e922 com.learn.example.Singleton@52e922 通过反射的方式获取的对象sc3:com.learn.example.Singleton@25154f 通过反射的方式获取的对象sc4:com.learn.example.Singleton@10dea4e 复制代码
我们看到正常的调用getInstance是符合我们预期的,如果通过反射(绕过检查,通过反射可以调用私有的),那么单例模式其实是失效了,我们创建了两个完全不同的对象sc3和sc4。我们如何来修复这个问题呢?反射需要调用构造函数,那我们可以在构造函数里面进行判断。修复代码如下:
class Singleton implements Serializable{ private static class SingletonClassInstance { private static final Singleton instance = new Singleton(); } //方法没有同步,调用效率高 public static Singleton getInstance() { return SingletonClassInstance.instance; } //防止反射获取多个对象的漏洞 private Singleton() { if (null != SingletonClassInstance.instance) throw new RuntimeException(); } } 复制代码
我们看到唯一的改进在于,构造函数里面添加了判断,如果当前已有实例,通过抛出异常来阻止反射创建对象。我们来看下输出:
com.learn.example.Singleton@52e922 com.learn.example.Singleton@52e922 Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source) at java.lang.reflect.Constructor.newInstance(Unknown Source) at com.learn.example.RunMain.main(RunMain.java:45) Caused by: java.lang.RuntimeException at com.learn.example.Singleton.<init>(RunMain.java:28) ... 5 more 复制代码
我们看到,我们通过反射创建对象的时候会抛出异常了。
除了反射以外,反序列化过程也会破坏单例模式,我们来看下现阶段反序列化输出的结果:
com.learn.example.Singleton@52e922 com.learn.example.Singleton@52e922 对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@16ec8df 复制代码
我们看到反序列化后的对象和原对象sc1已经不是同一个对象了。我们需要对反序列化过程进行处理,处理代码如下:
//防止反序列化获取多个对象的漏洞。 //无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。 //实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象 private Object readResolve() throws ObjectStreamException { return SingletonClassInstance.instance; } 复制代码
我们从注释里面也可以看出来,readResolve方法会将原来反序列化出来的对象进行覆盖。我们丢弃原来反序列化出来的对象,使用已经创建的好的单例对象进行覆盖。我们来看现在的输出:
com.learn.example.Singleton@52e922 com.learn.example.Singleton@52e922 对象定义了readResolve()方法,通过反序列化得到的对象:com.learn.example.Singleton@52e922 复制代码
关于readResolve这个方法的详细解释可以看这篇文章:
序列化的相关方法介绍Effective Java中推荐使用枚举来实现单例,因为枚举实现单例可以阻止反射及序列化的漏洞,下面我们通过例子来看下:
class Resource{} /** * 使用枚举实现单例 */ enum SingletonEnum{ INSTANCE; private Resource instance; SingletonEnum() { instance = new Resource(); } public Resource getInstance() { return instance; } } 复制代码
我们在main方法中调用代码:
Resource resource1 = SingletonEnum.INSTANCE.getInstance(); Resource resource2 = SingletonEnum.INSTANCE.getInstance(); System.out.println(resource1); System.out.println(resource2); 复制代码
输出如下:
com.learn.example.Resource@52e922 com.learn.example.Resource@52e922 复制代码
我们看到,通过枚举我们实现了单例,那么枚举是如何保证单例的(如何满足多线程及序列化的标准的)?其实枚举是一个普通的类,它继承自java.lang.Enum类。我们将上面的class文件反编译后,会得到如下代码:
public final class SingletonEnum extends Enum<SingletonEnum> { public static final SingletonEnum INSTANCE; public static SingletonEnum[] values(); public static SingletonEnum valueOf(String s); static {}; } 复制代码
由反编译后的代码可知,INSTANCE 被声明为static 的,在类加载过程,可以知道虚拟机会保证一个类的() 方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。