上篇文章讲到常见的两种单例模式。尽管实现了单例模式在多线程环境下的绝对安全,但是想破坏已经写好单例模式也很简单。
1.反射攻击
上代码:
import java.lang.reflect.Constructor;
/**
* 恶汉模式,加上final不能被继承
*/
public final class HungrySingleton {
//1.私有,静态的变量。类在加载的时候就创建
private static HungrySingleton singleton = new HungrySingleton();
//2.私有构造方法,不允许其他人再来创建对象
private HungrySingleton(){}
/**
* 3.提供一个获取实例的方法
* @return
*/
public static HungrySingleton getInstance(){
return singleton;
}
public static void main(String[] args) throws Exception{
//单例模式获取到的实例
HungrySingleton instance = HungrySingleton.getInstance();
System.out.println("单例模式获取到的实例:" + instance);
/**
* 反射攻破
*/
//获取构造器
Constructor<?>[] constructors = HungrySingleton.class.getDeclaredConstructors();
//破防
constructors[0].setAccessible(true);
//获取新实例
HungrySingleton instance1 = (HungrySingleton)constructors[0].newInstance();
System.out.println("反射后得到的实例:" + instance1);
}
}
看效果:
可以发现两个对象是不一样的,通过反射可以获取额外的对象,那如何解决呢,其实很简单。只需要再构造方法中加入判断逻辑即可:
public final class HungrySingleton {
//1.私有,静态的变量。类在加载的时候就创建
private static HungrySingleton singleton = new HungrySingleton();
private static boolean flag = true;
//2.私有构造方法,不允许其他人再来创建对象
private HungrySingleton(){
if(flag){
flag = false;
}else {
throw new RuntimeException("禁止访问私有构造方法");
}
}
/**
* 3.提供一个获取实例的方法
* @return
*/
public static HungrySingleton getInstance(){
return singleton;
}
看效果:
当反射调用构造方法,判断实例是否已经存在,存在就直接抛异常即可。
2.序列化攻击
我们可以通过一些序列化方式来让java对象以某种格式进行存储或者传输,常见的有java自带的对象流啊,fastjson,hessian等等,但是当把一个单例对象序列化后再反序列化,还是原来的对象吗?上代码:
import java.io.*;
import java.lang.reflect.Constructor;
/**
* 恶汉模式,加上final不能被继承
*/
public final class HungrySingleton1 implements Serializable {
//1.私有,静态的变量。类在加载的时候就创建
private static HungrySingleton1 singleton = new HungrySingleton1();
//2.私有构造方法,不允许其他人再来创建对象
private HungrySingleton1(){
if(singleton != null){
throw new RuntimeException("禁止访问始有构造方法");
}
}
/**
* 3.提供一个获取实例的方法
* @return
*/
public static HungrySingleton1 getInstance(){
return singleton;
}
public static void main(String[] args) throws Exception{
//单例模式获取到的实例
HungrySingleton1 instance = HungrySingleton1.getInstance();
System.out.println("单例模式获取到的实例:" + instance);
/**序列化攻击,这里使用的java自带的对象流
*/
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("obj.txt"));
outputStream.writeObject(instance);
outputStream.flush();
outputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("obj.txt"));
HungrySingleton1 instance1 = (HungrySingleton1) objectInputStream.readObject();
System.out.println("反序列化后得到的对象:" + instance1);
}
}
看结果:
同样的可以得到除了单例模式以外的另一个对象。这到底是什么原因呢,我们来看看
HungrySingleton1 instance1 = (HungrySingleton1) objectInputStream.readObject();
这句代码底层做了啥。进入readObject()源码,看期中一段:
try {
//这里得到了obj对象
Object obj = readObject0(false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
//返回的obj就是反序列化后得到的obj
return obj;
}
这里就需要继续进入 Object obj = readObject0(false);其中有一段枚举
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
再次进入readOrdinaryObject(unshared):最终可以看到最核心的一段代码
Object obj;
try {
//这是最重要的一行
//desc.isInstantiable()判断对象是否可实例化,其实就是判断能否获取到构造法
//desc.newInstance()就是在给我们创建一个新的对象
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
所底层是帮助new了一个对象返回,自然就会产生一个除了单例以外的一个对象。
解决:在readOrdinaryObject(unshared)中有这么一段:
Object rep = desc.invokeReadResolve(obj);
回调对象中的readResolve()方法。此时我们只需要在对象中加入这个方法即可
上代码:
public final class HungrySingleton1 implements Serializable {
//1.私有,静态的变量。类在加载的时候就创建
private static HungrySingleton1 singleton = new HungrySingleton1();
//2.私有构造方法,不允许其他人再来创建对象
private HungrySingleton1(){
if(singleton != null){
throw new RuntimeException("禁止访问始有构造方法");
}
}
//反序列化会调用这个方法,只需要把我们自己的对象返回即可
Object readResolve(){
return singleton;
}
}
看效果
这样就解决啦。
建议单例直接使用枚举方式,枚举是不能反射创建对象的,JDK会抛出异常,同时反序列化后也是同一个。