目录
接下来说一下破坏单例模式的办法和解决途径。
序列化和反序列化破坏单例模式
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();
非常方便。