本文转载:https://blog.csdn.net/fly_as_tadpole/article/details/86655360
单例模式:基于反射和反序列化破解单例模式的漏洞及其解决方法
单例模式使得在创建类对象的时候只创建一个对象实例。上一节讲解了五种实现单例模式的方式。
分别为:饿汉模式、懒汉模式、double check、静态内部类、枚举
但是基于反射和反序列化可以破解单例模式的单一实例,在使用反射时可以通过调用setAccesible()直接调用私有构造器,创建新的实例;在反序列化的时候会直接创建新的对象实例。但是以上漏洞只针对前四种方式,枚举由于是基于JVM底层实现机制,是天然的单例模式。
假设我们使用饿汉的实现方式创建了一个单例类:
-
package com.test.test1.danlimoshi;
-
public class EHanShi {
-
private static EHanShi eHanShi = new EHanShi();
-
private EHanShi(){}
-
public static EHanShi getInstance(){
-
return eHanShi;
-
}
-
}
接下来基于反射实现创建两个不同的实例。
-
package com.test.test1.danlimoshi;
-
import java.lang.reflect.Constructor;
-
public class Client2 {
-
public static void main(String[] args) throws Exception {
-
EHanShi eHanShi1 = EHanShi.getInstance();
-
EHanShi eHanShi2 = EHanShi.getInstance();
-
System.out.println(eHanShi1);
-
System.out.println(eHanShi2);
-
Class<EHanShi> clazz = (Class<EHanShi>) Class.forName("com.test.test1.danlimoshi.EHanShi");
-
Constructor<EHanShi> constructor = clazz.getDeclaredConstructor(null);
-
constructor.setAccessible(true); //跳过检查机制,直接调用私有构造器
-
EHanShi eHanShi3 = constructor.newInstance();
-
System.out.println(eHanShi3);
-
}
-
}
打印结果:显然创新了新的实例。
com.test.test1.danlimoshi.EHanShi@1b6d3586
com.test.test1.danlimoshi.EHanShi@1b6d3586
com.test.test1.danlimoshi.EHanShi@4554617c
如何解决?
在私有构造器通过抛出异常处理。即当创建第二个实例的时候就刨出异常。
-
package com.test.test1.danlimoshi;
-
public class EHanShi {
-
private static EHanShi eHanShi = new EHanShi();
-
private EHanShi(){
-
if(eHanShi != null){
-
throw new RuntimeException();
-
}
-
}
-
public static EHanShi getInstance(){
-
return eHanShi;
-
}
-
}
接下来基于序列化创建新的实例。
-
package com.test.test1.danlimoshi;
-
import java.io.FileInputStream;
-
import java.io.FileOutputStream;
-
import java.io.ObjectInputStream;
-
import java.io.ObjectOutputStream;
-
import java.lang.reflect.Constructor;
-
public class Client2 {
-
public static void main(String[] args) throws Exception {
-
EHanShi eHanShi1 = EHanShi.getInstance();
-
EHanShi eHanShi2 = EHanShi.getInstance();
-
System.out.println(eHanShi1);
-
System.out.println(eHanShi2);
-
// Class<EHanShi> clazz = (Class<EHanShi>) Class.forName("com.test.test1.danlimoshi.EHanShi");
-
// Constructor<EHanShi> constructor = clazz.getDeclaredConstructor(null);
-
// constructor.setAccessible(true);
-
// EHanShi eHanShi3 = constructor.newInstance();
-
// System.out.println(eHanShi3);
-
try(FileOutputStream fos = new FileOutputStream("D:/a.txt")){
-
ObjectOutputStream oos = new ObjectOutputStream(fos);
-
oos.writeObject(eHanShi1);
-
}catch (Exception e){
-
e.printStackTrace();
-
}
-
FileInputStream fis = new FileInputStream("D:/a.txt");
-
ObjectInputStream ois = new ObjectInputStream(fis);
-
EHanShi eHanShi3 = (EHanShi)ois.readObject();
-
System.out.println(eHanShi3);
-
}
-
}
打印结果:显然创建了新的实例。
com.test.test1.danlimoshi.EHanShi@1b6d3586
com.test.test1.danlimoshi.EHanShi@1b6d3586
com.test.test1.danlimoshi.EHanShi@6d03e736
如何解决?
在单例类中创建一个方法readResolve(),基于回调机制,在反序列化的时候直接会调用这个方法。返回当前实例。
-
package com.test.test1.danlimoshi;
-
import java.io.Serializable;
-
public class EHanShi implements Serializable {
-
private static EHanShi eHanShi = new EHanShi();
-
private EHanShi(){
-
if(eHanShi != null){
-
throw new RuntimeException();
-
}
-
}
-
public static EHanShi getInstance(){
-
return eHanShi;
-
}
-
public Object readResolve(){
-
return eHanShi;
-
}
-
}
那么就不会创建新的实例了。
测试五种实现方式的耗时:
package com.test.test1.danlimoshi;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
//测试多线程环境下实现这几种方式的耗时
public class Client3 {
public static void main(String[] args) throws Exception {
long startTime = System.currentTimeMillis();
int maxCount = 10;
CountDownLatch countDownLatch = new CountDownLatch(maxCount);
for(int i =0;i<maxCount;i++) {
new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 100000; j++) {
//Object o = EHanShi.getInstance();
//Object o =LanHanShi.getInstance();
Object o =JingTaiLeiJiaZai.getInstance();
}
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();//当10个线程执行完成,也即是计数器值为0,main线程继续往下执行
long endTime = System.currentTimeMillis();
System.out.println("总耗时:"+(endTime-startTime));
}
}