单例模式相信你们都不陌生,咱们不讨论单例模式的几种写法及其优劣。今天咱们单独拎出单例的几种实现来看看如何有效的抵御反射及序列化的攻击。若是不了解反射和序列化的能够看这两篇文章。
反射
序列化html
单例模式最根本的在于类只能有一个实例,若是经过反射来构建这个类的实例,单例模式就会被破坏,下面咱们经过例子来看下:java
/**
* 静态内部类式单例模式
*/
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);
复制代码
下面咱们来看输出:bash
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
复制代码
咱们看到,咱们经过反射建立对象的时候会抛出异常了。post
除了反射之外,反序列化过程也会破坏单例模式,咱们来看下现阶段反序列化输出的结果:ui
com.learn.example.Singleton@52e922
com.learn.example.Singleton@52e922
对象定义了readResolve()方法,经过反序列化获得的对象:com.learn.example.Singleton@16ec8df
复制代码
咱们看到反序列化后的对象和原对象sc1已经不是同一个对象了。咱们须要对反序列化过程进行处理,处理代码以下:spa
//防止反序列化获取多个对象的漏洞。
//不管是实现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 的,在类加载过程,能够知道虚拟机会保证一个类的() 方法在多线程环境中被正确的加锁、同步。因此,枚举实现是在实例化时是线程安全。
Java规范中规定,每个枚举类型极其定义的枚举变量在JVM中都是惟一的,所以在枚举类型的序列化和反序列化上,Java作了特殊的规定。
在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是经过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
也就是说,如下面枚举为例,序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再经过这个名称,查找对于的枚举类型,所以反序列化后的实例也会和以前被序列化的对象实例相同。
Effective Java中单元素的枚举类型被做者认为是实现Singleton的最佳方法。