从jdk1.5开始,可通过编写一个包含单个元素的枚举类型来实现单例:
public enum Singleton {
uniqueInstance;
public void SingletonOperation() {
......
}
}
//然后就可以通过Singleton.uniqueInstace.SingletonOperaion来调用
这种方法在功能上与共有域方法相近,但它更加简洁,无偿地提供了序列化的机制,绝对防止多次实例化,即使在面对复杂的序列化或者反射攻击时也能防止。
单元素的枚举类型是实现Singleton的最佳方法。
首先来了解常用的单例模式为何能被攻击破坏。
破坏单例模式
破坏单例模式主要有三种方式:克隆、反射和序列化。简单了解一下这三种方式如何破坏单例。
- 克隆
这种攻击方式只针对实现了Cloneable接口的单例类。clone方法是不会调用构造函数的,它是直接从内存中copy内存区域的。
而clone方法是Object类的protected方法,默认情况下一个对象是不能直接调用clone方法的。
对于有实现Cloneable接口需求的单例类,应重写clone方法,直接返回其内部的instance,而不能直接返回super.clone()。
public class testSingleton {
public static void main(String[] args) throws Exception{
Singleton singleton = Singleton.getInstance();
Singleton clone = (Singleton) singleton.clone();
System.out.println(singleton == clone);
}
public static class Singleton implements Cloneable{
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
}
- 反射
反射可以获取类的构造函数,同时再通过setAccessible(true)就可调用私有构造函数来创建对象。
要防止反射攻击,只能在单例构造方法中检测instance是否为null,或使用first执行标志等,只要是第二次调用了构造方法就抛异常。
public class testSingleton {
public static void main(String[] args) throws Exception{
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton reflect = (Singleton)constructor.newInstance();
System.out.println(singleton == reflect);
}
public static class Singleton{
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
}
- 序列化
这种攻击也只对实现了Serializable接口的单例有效,而有些单例恰巧是必须序列化的。
序列化的攻击方式如下所示,主要问题就在inputStream.readObject中。
public class testSingleton {
public static void main(String[] args) throws Exception{
Singleton singleton = Singleton.getInstance();
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("serfile"));
outputStream.writeObject(Singleton.getInstance());
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("serfile"));
Singleton seriable = (Singleton) inputStream.readObject();
System.out.println(singleton == seriable);
}
public static class Singleton implements Serializable{
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
}
在ObjectInputStream.readObject方法执行时,其内部方法readOrdinaryObject中执行了该语句:
obj = desc.isInstantiable() ? desc.newInstance() : null;
其中desc是类描述符,也就是说,如果一个实现了Serializable/Externalizable接口的类可以在运行时实例化,那么就调用newInstance()方法,使用其默认构造方法反射创建新的对象实例,自然也就破坏了单例性。
要防御序列化攻击,就得将instance声明为transient,且在单例中加入:
private Object readResolve() {
return instance;
}
因为在readOrdinaryObject方法中,会通过desc.hasReadResolveMethod()检查类中是否存在readResolve方法,若存在,则执行desc.invokeReadResolve(obj)来调用该方法,readResolve方法会用自定义的反序列化逻辑覆盖默认实现,因此强制它返回instance本身,从而防止产生新的实例。
枚举单例的防御
- 针对反射
java的枚举类型实际上都继承自Enum抽象类,而该类只有一个带有两个参数的构造方法:
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
我们通过反射获取这个构造方法:
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
测试发现抛出如下异常:

image.png
从它抛出的异常的注释:Cannot reflectively create enum objects可看出,从JDK反射机制的内部实现就已经排除了用反射创建枚举实例的可能。
- 针对序列化
尝试用同样的序列化攻击方式来攻击枚举实现的单例,发现,最终获得的实际是同一个实例。
在ObjectInputStream类的readObjectO方法中,枚举类型获取到的为TC_ENUM,专门针对枚举类型做处理,调用readEnum方法,该方法会通过调用Enum.valueof方法,传入获取到的枚举类型以及具体的枚举值,从而获得最终的单例。
JDK内部对枚举类型的专门处理同样也绕开了序列化对枚举实现单例的攻击。