《Effective Java》已经告诉我们,在单例类中提供一个readResolve方法就可以完成单例特性。这里大家可以自己去测试。
接下来,我们去看看Java提供的反序列化是如何创建对象的!
ObjectInputStream
对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream的readObject 方法执行情况到底是怎样的。
为了节省篇幅,这里给出ObjectInputStream的readObject的调用栈:
大家顺着此图的关系,去看readObject方法的实现。
首先进入readObject0方法里,关键代码如下:
switch (tc) { //省略部分代码 case TC_STRING: case TC_LONGSTRING: return checkResolve(readString(unshared)); case TC_ARRAY: return checkResolve(readArray(unshared)); case TC_ENUM: return checkResolve(readEnum(unshared)); case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared)); case TC_EXCEPTION: IOException ex = readFatalException(); throw new WriteAbortedException("writing aborted", ex); case TC_BLOCKDATA: case TC_BLOCKDATALONG: if (oldMode) { bin.setBlockDataMode(true); bin.peek(); // force header read throw new OptionalDataException( bin.currentBlockRemaining()); } else { throw new StreamCorruptedException( "unexpected block data"); } //省略部分代码
这里就是判断目标对象的类型,不同类型执行不同的动作。我们的是个普通的Object对象,自然就是进入case TC_OBJECT的代码块中。然后进入readOrdinaryObject方法中。
readOrdinaryObject方法的代码片段:
private Object readOrdinaryObject(boolean unshared) throws IOException { //此处省略部分代码 Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); } //此处省略部分代码 if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { handles.setObject(passHandle, obj = rep); } } return obj; }
重点看代码块:
Object obj; try { obj = desc.isInstantiable() ? desc.newInstance() : null; } catch (Exception ex) { throw (IOException) new InvalidClassException( desc.forClass().getName(), "unable to create instance").initCause(ex); }
这里创建的这个obj对象,就是本方法要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。
isInstantiable:如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。针对serializable和externalizable我会在其他文章中介绍。
desc.newInstance:该方法通过反射的方式调用无参构造方法新建一个对象。
所以。到目前为止,也就可以解释,为什么序列化可以破坏单例了?即序列化会通过反射调用无参数的构造方法创建一个新的对象。
接下来再看,为什么在单例类中定义readResolve就可以解决该问题呢?还是在readOrdinaryObjec方法里继续往下看。
if (obj != null && handles.lookupException(passHandle) == null && desc.hasReadResolveMethod()) { Object rep = desc.invokeReadResolve(obj); if (unshared && rep.getClass().isArray()) { rep = cloneArray(rep); } if (rep != obj) { handles.setObject(passHandle, obj = rep); } }
这段代码也很清楚地给出答案了!
如果目标类有readResolve方法,那就通过反射的方式调用要被反序列化的类的readResolve方法,返回一个对象,然后把这个新的对象复制给之前创建的obj(即最终返回的对象)。那readResolve 方法里是什么?就是直接返回我们的单例对象。
public class Elvis implements Serializable { public static final Elvis INSTANCE = new Elvis(); private Elvis() { System.err.println("Elvis Constructor is invoked!"); } private Object readResolve() { return INSTANCE; } }
所以,原理也就清楚了,主要在Singleton中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏。
单元素枚举类型
第三种实现单例的方式是,声明一个单元素的枚举类:
// Enum singleton - the preferred approach public enum Elvis { INSTANCE; public void leaveTheBuilding() { ... } }
这个方法跟提供公有的字段方法很类似,但它更简洁,提供天然的可序列化机制和能够强有力地保证不会出现多次实例化的情况 ,甚至面对复杂的序列化和反射的攻击下。这种方法可能看起来不太自然,但是拥有单元素的枚举类型可能是实现单例模式的最佳实践。注意,如果单例必须要继承一个父类而非枚举的情况下是无法使用该方式的(不过可以声明一个实现了接口的枚举)。
我们分析一下,枚举类型是如何阻止反射来创建实例的?直接源码:
看Constructor类的newInstance方法。
public T newInstance(Object ... initargs) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, null, modifiers); } } if ((clazz.getModifiers() & Modifier.ENUM) != 0) throw new IllegalArgumentException("Cannot reflectively create enum objects"); ConstructorAccessor ca = constructorAccessor; // read volatile if (ca == null) { ca = acquireConstructorAccessor(); } @SuppressWarnings("unchecked") T inst = (T) ca.newInstance(initargs); return inst; }
这行代码(clazz.getModifiers() & Modifier.ENUM) != 0 就是用来判断目标类是不是枚举类型,如果是抛出异常IllegalArgumentException(“Cannot reflectively create enum objects”),无法通过反射创建枚举对象!很显然,反射无效了。
接下来,再看一下反序列化是如何预防的。依然按照上面说的顺序去找到枚举类型对应的readEnum方法,如下:
private Enum<?> readEnum(boolean unshared) throws IOException { if (bin.readByte() != TC_ENUM) { throw new InternalError(); } ObjectStreamClass desc = readClassDesc(false); if (!desc.isEnum()) { throw new InvalidClassException("non-enum class: " + desc); } int enumHandle = handles.assign(unshared ? unsharedMarker : null); ClassNotFoundException resolveEx = desc.getResolveException(); if (resolveEx != null) { handles.markException(enumHandle, resolveEx); } String name = readString(false); Enum<?> result = null; Class<?> cl = desc.forClass(); if (cl != null) { try { @SuppressWarnings("unchecked") Enum<?> en = Enum.valueOf((Class)cl, name); result = en; } catch (IllegalArgumentException ex) { throw (IOException) new InvalidObjectException( "enum constant " + name + " does not exist in " + cl).initCause(ex); } if (!unshared) { handles.setObject(enumHandle, result); } } handles.finish(enumHandle); passHandle = enumHandle; return result; }
readString(false):首先获取到枚举对象的名称name。
Enum<?> en = Enum.valueOf((Class)cl, name):再指定名称的指定枚举类型获得枚举常量,由于枚举中的name是唯一,切对应一个枚举常量。所以我们获取到了唯一的常量对象。这样就没有创建新的对象,维护了单例属性。
看看Enum.valueOf 的JavaDoc文档:
- 返回具有指定名称的指定枚举类型的枚举常量。 该名称必须与用于声明此类型中的枚举常量的标识符完全匹配。 (不允许使用无关的空白字符。)
具体实现:
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) { T result = enumType.enumConstantDirectory().get(name); if (result != null) return result; if (name == null) throw new NullPointerException("Name is null"); throw new IllegalArgumentException( "No enum constant " + enumType.getCanonicalName() + "." + name); }
enumConstantDirectory():返回一个Map,维护着名称到枚举常量的映射。我们就是从这个Map里获取已经声明的枚举常量,通过这个缓存池一样的组件,让我们可以重用这个枚举常量!
总结
- 常见的单例写法有他的弊端,存在安全性问题,如:反射,序列化的影响。
- 《Effective Java》作者Josh Bloch 提倡使用单元素枚举类型的方式来实现单例,首先创建一个枚举很简单,其次枚举常量是线程安全的,最后有天然的可序列化机制和防反射的机制。
参考
- 《单例模式的七种写法》
- 《单例与序列化的那些事儿》
- 《Effective Java》
原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/14089.html