反射攻击和反序列化问题是两种最常见的破坏单例模式的手段,前者利用反射机制修改构造函数的可见性然后强行创建一个新的实例,后者是将原先的对象序列化后再反序列化从而生成一个新的实例
反射攻击
产生原因
单例模式实现的一个关键点就是构造函数的私有性,反射攻击利用反射机制获取并修改构造函数的可见性,从而可以创建出新的实例,破坏单例:
public class Reflection {
public static void normalReflection() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
\\利用反射获取构造函数
Constructor c = StaticInnerClassSingleton.class.getDeclaredConstructor();
\\改变构造函数的可见性
c.setAccessible(true);
\\调用newInstance创建新的实例
StaticInnerClassSingleton o1 = (StaticInnerClassSingleton) c.newInstance();
\\通过getInstance方法获取单例
StaticInnerClassSingleton o2 = StaticInnerClassSingleton.getInstance();
System.out.println(o1);
System.out.println(o2);
}
运行结果
design.Singleton.StaticInnerClassSingleton@1d44bcfa
design.Singleton.StaticInnerClassSingleton@266474c2
Process finished with exit code 0
通过运行结果可以发现,两个实例不是同一个,单例模式被破坏。
枚举防止反射攻击
JDK不允许通过反射创建新的枚举实例,所以枚举单例不会被反射攻击破坏:
public static void enumReflection() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Constructor c = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
c.setAccessible(true);
EnumSingleton o1 = (EnumSingleton) c.newInstance();
EnumSingleton o2 = EnumSingleton.getInstance();
System.out.println(o1);
System.out.println(o2);
}
运行结果:
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at design.Singleton.Reflection.enumReflection(Reflection.java:22)
at design.Singleton.Reflection.main(Reflection.java:30)
Process finished with exit code 1
反序列化攻击
产生原因
对象序列化再反序列化时,会生成一个新的对象,从而破坏单例:
public static void normalSingletonDeserializeProblem() throws IOException, ClassNotFoundException {
StaticInnerclassSingleton o1 = StaticInnerclassSingleton.getInstance();
//通过writeObject将对象序列化到文件
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("tempFile"));
oos.writeObject(o1);
//读取文件,反序列化
File file = new File("tempFile");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
StaticInnerclassSingleton newInstance = (StaticInnerclassSingleton) ois.readObject();
System.out.println(o1);
System.out.println(newInstance);
System.out.println(StaticInnerclassSingleton.getInstance());
}
运行结果:
design.Singleton.StaticInnerclassSingleton@63947c6b
design.Singleton.StaticInnerclassSingleton@34a245ab
design.Singleton.StaticInnerclassSingleton@63947c6b
Process finished with exit code 0
反序列化之后,生成了一个新的对象,单例被破坏
解决方法
通过实现一个readResolve方法,可以避免反序列化问题
class NoProblemSingleton implements Serializable {
private NoProblemSingleton() {}
public static NoProblemSingleton getInstance() {
return InnerHolder.INSTANCE;
}
private static class InnerHolder {
private static final NoProblemSingleton INSTANCE = new NoProblemSingleton();
}
//实现readResolve方法可以解决反序列化攻击,反序列化时会检查有没有这个方法,如果有可以调用这个方法返回对象
private Object readResolve() {
return InnerHolder.INSTANCE;
}
}
运行结果:
design.Singleton.NoProblemSingleton@63947c6b
design.Singleton.NoProblemSingleton@63947c6b
design.Singleton.NoProblemSingleton@63947c6b
true
Process finished with exit code 0
实现readResolve方法后,反序列化的对象和newInstance返回的对象是同一个
原理
反序列化调用readObject方法时,会判断类中是否有readResolve方法。如果有,会调用readResolve方法并最终用readResolve方法返回的对象替换反序列化出来的对象。不过替换是发生在反序列化之后的,也就是说反序列化的对象还是生成了,只不过无法被访问只能等待GC回收。
枚举类型防止反序列化攻击
枚举单例不会有反序列化问题,因为readObject如果判断反序列化的对象是枚举类型,会直接调用Enum.valueOf方法返回枚举实例
总结
- 反射攻击和反序列化是单例模式的两个最常见的问题
- 在单例类中实现readResolve方法可以避免反序列化问题
- 枚举既不存在反射问题,也不存在反序列化问题