ObjectInputStream

对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream的readObject 方法执行情况到底是怎样的。

为了节省篇幅,这里给出ObjectInputStream的readObject的调用栈:

大家顺着此图的关系,去看readObject方法的实现。
首先进入readObject0方法里,关键代码如下:

这里就是判断目标对象的类型,不同类型执行不同的动作。我们的是个普通的Object对象,自然就是进入case TC_OBJECT的代码块中。然后进入readOrdinaryObject方法中。
readOrdinaryObject方法的代码片段:

重点看代码块:

这里创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。

isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。 
desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。

所以。到目前为止,也就可以解释,为什么序列化可以破坏单例了?即序列化会通过反射调用无参数的构造方法创建一个新的对象

接下来再看,为什么在单例类中定义readResolve就可以解决该问题呢?还是在readOrdinaryObjec方法里继续往下看。

这段代码也很清楚地给出答案了!
如果目标类有readResolve方法,那就通过反射的方式调用要被反序列化的类的readResolve方法,返回一个对象,然后把这个新的对象复制给之前创建的obj(即最终返回的对象)。那readResolve 方法里是什么?就是直接返回我们的单例对象。

所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。

单元素枚举类型

第三种实现单例的方式是,声明一个单元素的枚举类:

这个方法跟提供公有的字段方法很类似,但它更简洁,提供天然的可序列化机制和能够强有力地保证不会出现多次实例化的情况 ,甚至面对复杂的序列化和反射的攻击下。这种方法可能看起来不太自然,但是拥有单元素的枚举类型可能是实现单例模式的最佳实践。注意,如果单例必须要继承一个父类而非枚举的情况下是无法使用该方式的(不过可以声明一个实现了接口的枚举)。
我们分析一下,枚举类型是如何阻止反射来创建实例的?直接源码:
看Constructor类的newInstance方法。

这行代码(clazz.getModifiers() & Modifier.ENUM) != 0 就是用来判断目标类是不是枚举类型,如果是抛出异常IllegalArgumentException("Cannot reflectively create enum objects"),无法通过反射创建枚举对象!很显然,反射无效了。

接下来,再看一下反序列化是如何预防的。依然按照上面说的顺序去找到枚举类型对应的readEnum方法,如下:

readString(false):首先获取到枚举对象的名称name。
Enum<?> en = Enum.valueOf((Class)cl, name):再指定名称的指定枚举类型获得枚举常量,由于枚举中的name是唯一,切对应一个枚举常量。所以我们获取到了唯一的常量对象。这样就没有创建新的对象,维护了单例属性。

看看Enum.valueOf 的JavaDoc文档:

  • 返回具有指定名称的指定枚举类型的枚举常量。 该名称必须与用于声明此类型中的枚举常量的标识符完全匹配。 (不允许使用无关的空白字符。)

具体实现:

enumConstantDirectory():返回一个Map,维护着名称到枚举常量的映射。我们就是从这个Map里获取已经声明的枚举常量,通过这个缓存池一样的组件,让我们可以重用这个枚举常量!

总结

  1. 常见的单例写法有他的弊端,存在安全性问题,如:反射,序列化的影响。
  2. 《Effective Java》作者Josh Bloch 提倡使用单元素枚举类型的方式来实现单例,首先创建一个枚举很简单,其次枚举常量是线程安全的,最后有天然的可序列化机制和防反射的机制。
01-27 17:07