通过一系列演进,现在我们可以确定改造后的单例模式在系统运行过程中只会产生一个单例对象。但是这是不是坚不可摧的呢?实际上有两种方式会破坏这种单例模式。
序列号和反序列化
现在以饿汉模式为例,写序列号和反序列化的测试代码。
执行的结果如下,从结果中可以看出,序列化和反序列化后拿到的对象与原先不是同一个对象,这就违背了单例模式的初衷。
为了解决这个问题,则只需要在这个类中加入以下方法。添加readResolve()方法,直接返回实例。
再次执行测试方法,得到以下结果。说明加了readResolve方法之后,序列号与反序列化拿到的对象和原先的是同一个对象了。
readResolve()方法并不是Object声明的,而且为什么一定要取这个名字呢?我们可以从ObjectInputStream的源码中找到原因。首先看到,反序列化后获取对象的方法ois.readObject(),查看readObject()的源码。
readObject()方法返回的obj是通过readObject0()方法得来的。再看readObject0()进行了什么操作。
这个方法中有一段是switch/case匹配,我们需要返回的是对象,所以现在只需要关注这边的checkResolve()方法。查看该方法,其中如下段落:
从这段代码可以看出,这边会判断实例所属类是否有readResolve()方法。如果有,则执行,返回的是原实例,最后返回该实例。所以最终,在单例类中加上readResolve()方法返回实例,就可以防止序列化与反序列化破坏单例模式。具体的可以自己查看源码,查看反序列化重新获取实例的过程。
反射攻击
现在再写一个通过反射拿到新实例对象的测试方法。
该方法中显示在27行拿到该类的构造器。由于该类构造器私有化,则在28行获取构造器权限。通过构造器拿到一个实例和通过getInstance()方法拿到的实例进行对比,结果是通过反射会创建新的实例。
解决方法也比较简单,只需要改造私有化的构造器,如下。调用构造器的时候进行判断,抛出异常。
验证一下,如预期抛出了异常。
这样就能解决反射调用私有构造器带来的单例模式破坏了。