单例模式:基于反射和反序列化破解单例模式的漏洞及其解决方法

本文转载:https://blog.csdn.net/fly_as_tadpole/article/details/86655360

单例模式:基于反射和反序列化破解单例模式的漏洞及其解决方法

单例模式使得在创建类对象的时候只创建一个对象实例。上一节讲解了五种实现单例模式的方式。

分别为:饿汉模式、懒汉模式、double check、静态内部类、枚举

但是基于反射和反序列化可以破解单例模式的单一实例,在使用反射时可以通过调用setAccesible()直接调用私有构造器,创建新的实例;在反序列化的时候会直接创建新的对象实例。但是以上漏洞只针对前四种方式,枚举由于是基于JVM底层实现机制,是天然的单例模式。


假设我们使用饿汉的实现方式创建了一个单例类:

  1. package com.test.test1.danlimoshi;

  2.  
  3. public class EHanShi {

  4.  
  5. private static EHanShi eHanShi = new EHanShi();

  6. private EHanShi(){}

  7. public static EHanShi getInstance(){

  8. return eHanShi;

  9. }

  10. }

接下来基于反射实现创建两个不同的实例。

 
  1. package com.test.test1.danlimoshi;

  2.  
  3. import java.lang.reflect.Constructor;

  4.  
  5. public class Client2 {

  6. public static void main(String[] args) throws Exception {

  7. EHanShi eHanShi1 = EHanShi.getInstance();

  8. EHanShi eHanShi2 = EHanShi.getInstance();

  9. System.out.println(eHanShi1);

  10. System.out.println(eHanShi2);

  11.  
  12. Class<EHanShi> clazz = (Class<EHanShi>) Class.forName("com.test.test1.danlimoshi.EHanShi");

  13. Constructor<EHanShi> constructor = clazz.getDeclaredConstructor(null);

  14. constructor.setAccessible(true); //跳过检查机制,直接调用私有构造器

  15. EHanShi eHanShi3 = constructor.newInstance();

  16. System.out.println(eHanShi3);

  17.  
  18. }

  19. }

打印结果:显然创新了新的实例。

com.test.test1.danlimoshi.EHanShi@1b6d3586
com.test.test1.danlimoshi.EHanShi@1b6d3586
com.test.test1.danlimoshi.EHanShi@4554617c

如何解决?

在私有构造器通过抛出异常处理。即当创建第二个实例的时候就刨出异常。

 
  1. package com.test.test1.danlimoshi;

  2.  
  3. public class EHanShi {

  4.  
  5. private static EHanShi eHanShi = new EHanShi();

  6. private EHanShi(){

  7. if(eHanShi != null){

  8. throw new RuntimeException();

  9. }

  10. }

  11. public static EHanShi getInstance(){

  12. return eHanShi;

  13. }

  14. }


接下来基于序列化创建新的实例。

 
  1. package com.test.test1.danlimoshi;

  2.  
  3. import java.io.FileInputStream;

  4. import java.io.FileOutputStream;

  5. import java.io.ObjectInputStream;

  6. import java.io.ObjectOutputStream;

  7. import java.lang.reflect.Constructor;

  8.  
  9. public class Client2 {

  10. public static void main(String[] args) throws Exception {

  11. EHanShi eHanShi1 = EHanShi.getInstance();

  12. EHanShi eHanShi2 = EHanShi.getInstance();

  13. System.out.println(eHanShi1);

  14. System.out.println(eHanShi2);

  15.  
  16. // Class<EHanShi> clazz = (Class<EHanShi>) Class.forName("com.test.test1.danlimoshi.EHanShi");

  17. // Constructor<EHanShi> constructor = clazz.getDeclaredConstructor(null);

  18. // constructor.setAccessible(true);

  19. // EHanShi eHanShi3 = constructor.newInstance();

  20. // System.out.println(eHanShi3);

  21.  
  22. try(FileOutputStream fos = new FileOutputStream("D:/a.txt")){

  23. ObjectOutputStream oos = new ObjectOutputStream(fos);

  24. oos.writeObject(eHanShi1);

  25. }catch (Exception e){

  26. e.printStackTrace();

  27. }

  28.  
  29. FileInputStream fis = new FileInputStream("D:/a.txt");

  30. ObjectInputStream ois = new ObjectInputStream(fis);

  31. EHanShi eHanShi3 = (EHanShi)ois.readObject();

  32. System.out.println(eHanShi3);

  33. }

  34. }

打印结果:显然创建了新的实例。

com.test.test1.danlimoshi.EHanShi@1b6d3586
com.test.test1.danlimoshi.EHanShi@1b6d3586
com.test.test1.danlimoshi.EHanShi@6d03e736

如何解决?

在单例类中创建一个方法readResolve(),基于回调机制,在反序列化的时候直接会调用这个方法。返回当前实例。

 
  1. package com.test.test1.danlimoshi;

  2.  
  3. import java.io.Serializable;

  4.  
  5. public class EHanShi implements Serializable {

  6.  
  7. private static EHanShi eHanShi = new EHanShi();

  8. private EHanShi(){

  9. if(eHanShi != null){

  10. throw new RuntimeException();

  11. }

  12. }

  13. public static EHanShi getInstance(){

  14. return eHanShi;

  15. }

  16.  
  17. public Object readResolve(){

  18. return eHanShi;

  19. }

  20. }

那么就不会创建新的实例了。


测试五种实现方式的耗时:

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));
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值