10

前言

之前在学习单例模式的时候从没考虑过安全的问题,一直以为单例是无懈可击的,今天我来教你如何破坏单例模式以及应对方法。

首先来看看下面这个常见的单例写法

public class Singleton(
    
    private Singleton(){}
    
    private static class SingletonInstance{
        private static final Singleton instance=new Singleton();
    }
    
    public static Singleton getInstance(){
        return SingletonInstance.instance;
    }
)

看起来无懈可击,那么真的是这样吗?

我们知道要破坏单例,则必须创建对象,那么我们顺着这个思路走,创建对象的方式无非就是new,clone,反序列化,以及反射

单例模式的首要条件就是构造方法私有化,所以new这种方式去破坏单例的可能性是不存在的
要调用clone方法,那么必须实现Cloneable接口,但是单例模式是不能实现这个接口的,因此排除这种可能性。
因此我们本篇来讨论一下反序列化反射如何对单例模式进行破坏。

使用反序列化破坏单例模式

序列化是破坏单例模式的一大利器。其与克隆性质有些相似,需要类实现序列化接口,相比于克隆,实现序列化在实际操作中更加不可避免,有些类,它就是一定要序列化。

下面我们来做个测试,在上面的单例模式中实现序列化接口,如下

public class Singleton implements Serializable {

然后我们对拿到的对象进行序列化和反序列进行测试

public class Test {  
    public static void main(String[] args) throws Exception {  
        //序列化  
        Singleton instance1 = Singleton.getInstance();  
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("tempFile"));  
        objectOutputStream.writeObject(instance1);  
        //反序列化  
        File file = new File("tempFile");  
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(file));  
        Singleton instance2 = (Singleton) objectInputStream.readObject();  
        System.out.println(instance1 == instance2);  
    }  
}

执行结果为

image.png

通过对Singleton的序列化与反序列化得到的对象是一个新的对象,这就破坏了Singleton的单例性。

image.png

接下来我们来试着打个断点debug一下

image.png

可以看到进入了readObject0这个方法里,我们进去看看

image.png

继续下一步会走到readOrdinaryObject方法中,可以看到其实反序列化底层也是使用反射帮我们创建了一个新的对象

image.png

那是不是我们就不能阻止单例被破坏了呢?并不是!

现在我们在Singleton类中加上了一个readResolve方法,该方法返回了INSTANCE实例,然后重新执行一下:

public class Singleton implements Serializable {  
  
    private Singleton() {  
    }  
  
    private static class SingletonInstance {  
        private static final Singleton INSTANCE = new Singleton();  
   }   
    public static Singleton getInstance() {  
        return SingletonInstance.INSTANCE;  
   }  
    private Object readResolve() {  
        return SingletonInstance.INSTANCE;  
   }  
 
}

image.png

结果竟然为true,也就是说序列化和反序列出来的是同一个对象!

image.png

那这到底是什么原理,我们来看看刚才的readOrdinaryObject方法:

image.png

看到上面应该很清楚了,在条件判断中 desc.hasReadResolveMethod()会判断是否有readResolve()方法,如果有的话会通过desc.invokeReadResolve(obj)去反射调用该方法,返回的就是同一个对象。

看到这里小伙伴们应该明白了,总结一句话就是:如果想要防止单例被反序列化破坏。就让单例类实现readResolve()方法。

使用反射破坏单例模式

说完反序列化破坏单例,那现在我们来看看反射如何破坏单例模式:

public class Test {  
    public static void main(String[] args) throws Exception {  
  
      Singleton instance1 = Singleton.getInstance();  
      //通过反射创建对象  
      Class<Singleton> singletonClass = Singleton.class;  
      Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(); 
      //暴力破解私有构造器
      constructor.setAccessible(true);  
      Singleton instance2 = constructor.newInstance();  
     
      System.out.println(instance1 == instance2);  
  }  
}

image.png

执行结果为 false,也就是说通过反射也能够破坏单例模式

我们如何应对呢?
即便是通过反射来创建实例,也是调用类中的构造器来实现的,所以我们可以在构造器中做文章。
改造Singleton类中的私有构造器如下:

public class Singleton implements Serializable {  
  
    private Singleton() {  
        if (SingletonInstance.INSTANCE != null) {  
            throw new RuntimeException("不允许反射调用构造器");  
      }  
    }    
    private static class SingletonInstance {  
        private static final Singleton INSTANCE = new Singleton();  
    }    
    public static Singleton getInstance() {  
        return SingletonInstance.INSTANCE;  
    }    
    private Object readResolve() {  
        return SingletonInstance.INSTANCE;  
    }  
}

执行结果:
image.png

很显然报异常了,这样便防止了这种方法实现的单例模式被反射破坏。

image.png

饿汉式实现的单例模式都可以这样来防止单例模式被反射破坏。
懒汉式实现的单例模式是不可以防止被反射破坏的。
现在我们用双重检查锁式实现的单例模式来进行测试:

public class Singleton {
    private static volatile Singleton instance ;

    private Singleton(){
        if(instance != null){
            throw new RuntimeException("不允许反射调用构造器");
        }
    }

    public static Singleton getInstance(){
        if(instance == null){
            synchronized (Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

image.png

重新执行一下,结果还是一样,那这样就没问题了吗?不!

现在我们修改一下测试类:

public class Test {
    public static void main(String[] args) throws Exception {
        //通过反射创建单例对象
        Class<Singleton> singletonClass = Singleton.class;
        Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();
        //获取单例对象
        Singleton instance1 = Singleton.getInstance();
        
        System.out.println(instance1 == instance2);
    }
}

调整一下顺序,现在我们先使用反射创建对象,再调用单例的getInstance()方法,结果如下:

image.png

我们把通过反射创建实例和调用静态方法getInstance()获得实例的位置互换了,所以一开始通过反射创建实例调用构造器,此时构造器中的判断instance != null是无用的,所以这种方法是不适用懒汉式实现的单例模式来防止被反射破坏的。

总结:如果今后需要自己手动实现一个单例的话,可以选择 构造器判断 + 实现 readResolve() 方法的方式
来防止单例被破坏。

image.png

那么有没有更简单的方法呢?答案是有的!

如果不想在构造器内部加判断,也不想写readResolve()方法,那你可以选择使用枚举来实现单例模式

使用枚举实现单例

在StakcOverflow中,有一个关于 What is an efficient way to implement a singleton pattern in Java? 的讨论:

image.png

如上图,得票率最高的回答是:使用枚举。

回答者引用了Joshua Bloch大神在《Effective Java》中明确表达过的观点:

使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。

接下来我们来看看如何使用枚举实现单例:

public enum EnumSingleton {
    INSTANCE; 
    public EnumSingleton getInstance(){ 
         return INSTANCE;
    }
}

可以看到相比双重检查等单例模式,使用枚举实现的单例模式更加优雅,那么上面这个代码是安全的吗,还是用原来的测试代码来测试一下:

public class Test {
    public static void main(String[] args) throws Exception {

        Singleton instance1 = Singleton.getInstance();

        //通过反射创建对象
        Class<Singleton> singletonClass = Singleton.class;
        Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();
        
        System.out.println(instance1 == instance2);
    }
}

image.png

结果会报 Exception in thread "main" java.lang.NoSuchMethodException

image.png

简单来说就是因为SingletonClass.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,因为一旦一个类声明为枚举,实际上就是继承了Enum,来看看Enum类源码:

public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
        private final String name;
        public final String name() {
            return name;
        }
        private final int ordinal;
        public final int ordinal() {
            return ordinal;
        }
        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
        //余下省略

看下Enum源码就明白,这两个参数是nameordial两个属性,因为继承了父类构造器,所以在刚才的测试中才会找不到无参构造器,那么是不是我们去调用父类的构造器就可以了呢?我们来测试一下:

public class Test {
    public static void main(String[] args) throws Exception {

        Singleton instance1 = Singleton.getInstance();

        //通过反射创建对象
        Class<Singleton> singletonClass = Singleton.class;
        //调用父类构造器
        Constructor<Singleton> constructor = singletonClass.getDeclaredConstructor(String.class, int.class);
        constructor.setAccessible(true);
        Singleton instance2 = constructor.newInstance();

        System.out.println(instance1 == instance2);
    }
}

注意:我们在上面通过singletonClass.getDeclaredConstructor(String.class, int.class)来调用父类构造器,来看下执行结果:

image.png

来看看在哪里抛出的异常:

image.png

总结来说就是反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。所以枚举是不怕发射攻击的。

枚举和反序列化

那枚举又是如何避免被反序列化来创建新对象的呢?

枚举对象的序列化、反序列化有自己的一套机制。序列化时,仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。

下面分析一下valueOf源码:

image.png

再来看看enumConstantDirectory()源码:

image.png

继续看getEnumConstantsShared()源码:

image.png

getEnumConstantsShared()方法获取枚举类的values()方法,然后得到枚举类所创建的所有枚举对象。

每个枚举对象都有一个唯一的name属性。序列化只是将name属性序列化,在反序列化的时候,通过创建一个Map(key,value),搭建起name和与之对应的对象之间的联系,然后通过索引key来获得枚举对象

总的来说就是枚举在反序列化的过程中并没有创建新的对象,而通过name属性拿到原有的对象,因此保证了枚举类型实现单例模式的序列化安全。

总结

如果今后要自己手动实现一个单例模式首先推荐使用枚举来实现,在面试被问到单例模式的时候也可以和面试官吹吹牛逼了,今天就暂时学习到这里,如果有什么不对的地方请多多指教。
image.png


超大只乌龟
882 声望1.4k 粉丝

区区码农