前言
单例模式有很多种形式:饿汉式、懒汉式、DCL(双重校验)、静态内部类、容器单例、ThreadLocal单例,具体代码请查看单例模式的7种形式。本文着重记录下序列化、反射攻击对单例的破坏以及相应的解决方案,最后简单介绍下枚举单例在这两个方面的优势以及其实际应用。
序列化破坏单例
一个栗子来看序列化对单例的破坏:
// 序列化对单例的破坏,以饿汉为例
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(hungrySingleton);
File file = new File("singleton");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); //c1
System.out.println(hungrySingleton);
System.out.println(newSingleton);
输出:HungrySingleton@776ec8df
HungrySingleton@26a1ab54
可以看到hungrySingleton和newSingleton指向的是两个不同的对象,也就是这个单例模式创建了两个对象实例。
以上是c1行执行的readObject方法调用链,执行readOrdinaryObject方法,通过反射获取到对象的Class,然后做出一个判断,如果是序列化的类就new一个instance。这就是造成生成了第二个对象的原因。
private Object readOrdinaryObject(boolean unshared) throws IOException {
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}
// 反射
ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {
// isInstantiable 如果desc是序列化的类就new一个instance返回
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
...
}
/**
* Returns true if represented class is serializable/externalizable and can
* be instantiated by the serialization runtime--i.e., if it is
* externalizable and defines a public no-arg constructor, or if it is
* non-externalizable and its first non-serializable superclass defines an
* accessible no-arg constructor. Otherwise, returns false.
*/
boolean isInstantiable() {
requireInitialized();
return (cons != null);
}
防止单例破坏的解决办法
如何防止序列化对单例的破坏,继续看readOrdinaryObject函数,new出这个instance之后,会继续执行一个判断,如果类是一个有readResolve方法的可序列化类,则会执行一个代码块。代码块里面的内容是通过动态代理的方式执行类的readResolve方法,并且用返回的对象将new出来的这个obj覆盖掉,并且返回覆盖之后的对象。
private Object readOrdinaryObject(boolean unshared) throws IOException {
...
Object obj;
try {
// isInstantiable 如果desc是序列化的类就new一个instance返回
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
...
}
...
if (obj != null &&
handles.lookupException(passHandle) == null &&
// 如果desc是一个有readResolve方法的可序列化类返回true
desc.hasReadResolveMethod())
{
//返回true会执行invokeReadResolve,找到readResolve方法并执行
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
// readResolve返回的对象覆盖掉原先创建的那个对象
handles.setObject(passHandle, obj = rep);
}
...
}
// 返回
return obj;
}
/**
* Returns true if represented class is serializable or externalizable and
* defines a conformant readResolve method. Otherwise, returns false.
*/
boolean hasReadResolveMethod() {
requireInitialized();
return (readResolveMethod != null);
}
/** class-defined readResolve method, or null if none */
private Method readResolveMethod;
Object invokeReadResolve(Object obj)
throws IOException, UnsupportedOperationException
{
requireInitialized();
if (readResolveMethod != null) {
try {
// 执行方法
return readResolveMethod.invoke(obj, (Object[]) null);
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ObjectStreamException) {
throw (ObjectStreamException) th;
} else {
throwMiscException(th);
throw new InternalError(th); // never reached
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
那么这个readResolveMethod是什么时候赋值为readResolve方法的呢?通过全局查找,发现ObjectStreamClass对象在构造的时候将readResolve放了进去。
private ObjectStreamClass(final Class<?> cl) {
...
writeReplaceMethod = getInheritableMethod(
cl, "writeReplace", null, Object.class);
readResolveMethod = getInheritableMethod(
cl, "readResolve", null, Object.class);
...
}
查看调用栈发现,在一开始我们执行oos.writeObject(hungrySingleton);就执行了lookup函数对传进来的对象进行扫描,把它的私有域、非静态非抽象方法,另外如果类实现的是Externalizable接口(Serializable的子接口,可以自定义指定序列化哪些属性),会获取非静态私有方法。
那么方法就显而易见了,在单例类中添加readResolve函数,并让它返回创建好的单例就行
public class HungrySingleton implements Serializable {
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
// 注意不能是static的readResolve,否则获取不到
private Object readResolve() {
return instance;
}
}
再次执行测试函数,得到结果:HungrySingleton@776ec8df
HungrySingleton@776ec8df
对单例的反射攻击
还是以饿汉为例,反射可以获取单例模式私有的构造器,并且改变访问权限,所以private在反射下''变成了''public。
// 对单例的反射攻击
Class objClass = HungrySingleton.class;
HungrySingleton instance = HungrySingleton.getInstance(); // c2
Constructor constructor = objClass.getDeclaredConstructor();
constructor.setAccessible(true); // 设置访问权限
HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();// c3
System.out.println(instance);
System.out.println(newInstance);
输出结果:HungrySingleton@eed1f14
HungrySingleton@1b28cdfa
反射攻击解决办法
一个比较简单的解决方法是在私有构造器中做一层判断,判断当前单例对象是否已经存在,存在则抛出异常。
private HungrySingleton() {
// 简单防止反射攻击,适用于饿汉,静态内部类
if (null != instance) {
throw new RuntimeException("单例模式禁止反射调用");
}
}
但是,这种方法仅适用于饿汉、静态内部类,因为这两个是在类加载的时候便创建单例对象,所以反射攻击必然在单例对象创建之后。而对于懒汉式,仍然在私有构造器中添加上述代码,并且将c2处的代码放到c3下面先利用反射获取一个对象,然后再创建单例,因为这个反射获取的对象引用并不指向单例里面的instance,所以创建了两个,而在多线程环境下更容易出现上述情况。
// 对单例的反射攻击
Class objClass = LazySingleton.class;
Constructor constructor = objClass.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton newInstance = (LazySingleton) constructor.newInstance(); // c3
LazySingleton instance = LazySingleton.getInstance(); // c2
System.out.println(instance);
System.out.println(newInstance);
输出结果:LazySingleton@7229724f
LazySingleton@4c873330
那么创建计数或者使用信号量,并且在构造器中加以判断?别忘了反射也能访问成员变量等。
private static boolean flag = true;
private LazySingleton(){
if (flag) {
flag = false;
} else {
throw new RuntimeException("单例模式禁止反射调用");
}
}
public static void main(String[] args) throws Exception {
Class objClass = LazySingleton.class;
Constructor constructor = objClass.getDeclaredConstructor();
constructor.setAccessible(true);
LazySingleton newInstance = (LazySingleton) constructor.newInstance(); // c3
// 在用getInstance获取单例之前,先用反射把false掷回true
Field flag = objClass.getDeclaredField("flag");
flag.setAccessible(true);
flag.set(newInstance, true);
LazySingleton instance = LazySingleton.getInstance(); // c2
}
输出结果还是:LazySingleton@7229724f
LazySingleton@4c873330
枚举式单例
在Effective Java中推荐的枚举式单例模式,既能防止反射攻击又能解决序列化的问题
public enum EnumSingleton {
INSTANCE;
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
枚举式单例的序列化
测试方法
public static void main(String[] args) throws Exception {
// 枚举类单例的序列化
EnumSingleton enumSingleton = EnumSingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("file"));
oos.writeObject(enumSingleton);
File file = new File("file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumSingleton newEnumSingleton = (EnumSingleton) ois.readObject();
System.out.println(enumSingleton);
System.out.println(newEnumSingleton);
System.out.println(enumSingleton == newEnumSingleton);
输出结果:INSTANCE
INSTANCE
true
在readObject0这个函数中,对于类型是ENUM的调用了readEnum函数
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
通过readString获取枚举对象的name,反射获取枚举类,在进行赋值并返回,因为枚举类型的name是唯一的,对应一个枚举常量,所以拿到的en肯定是唯一的。
private Enum<?> readEnum(boolean unshared) throws IOException {
...
// 获取枚举对象的名称
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);
}
}
...
return result;
}
枚举式单例的反射
// 枚举类单例的反射
Class enumSingletonClass = EnumSingleton.class;
// 枚举类型没有无参构造
Constructor constructor = enumSingletonClass.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingleton instance = (EnumSingleton) constructor.newInstance("hello", 1);
EnumSingleton newInstance = EnumSingleton.getInstance();
System.out.println(instance);
System.out.println(newInstance);
抛出异常: java.lang.IllegalArgumentException: Cannot reflectively create enum objects,不能用反射创建枚举对象。
jdk源码(1.8)中newInstance函数,c4处可以看出枚举类型使用newInstance方法会抛出异常。
@CallerSensitive
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) // c4
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;
}
枚举单例实现
那单例的实例方法可以写在INSTANCE里面,当然可以以实现接口的形式,然后也可以定义属性,比如下面代码中的data。
public interface MySingleton {
void doSomething();
}
public enum EnumSingleton implements MySingleton{
INSTANCE {
@Override
public void doSomething() { // 实例方法
System.out.println("hello world");
}
};
private String data; // 属性
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
public static EnumSingleton getInstance() {
return INSTANCE;
}
}
public static void main(String[] args) {
MySingleton instance = EnumSingleton.getInstance(); // 接口
instance.doSomething();
}