「源码分析」— 为什么枚举是单例模式的最佳方法
1. 引言
枚举类型(enum type)是在 Java 1.5 中引入的一种新的引用类型,是由 Java 提供的一种语法糖,其本质是 int 值。关于其用法之一,便是单例模式,并且在《Effective Java》中有被提到:
单元素的枚举类型已经成为实现 Singleton 的最佳方法
本文便是探究 “为什么枚举是单例模式的最佳方法?”。
答案先写在前面,两个字:“简单”。
public enum EnumSingleton {
INSTANCE;
}
Java 在我们使用它的同时,解决了危害单例模式安全性的两个问题:“反射攻击” * 和 *“反序列化攻击”。
本文的内容概要如下:
回顾常见的单例模式方法;
探索 Java 中的枚举是如何防止两种攻击;
若不使用枚举,又如何防止两种攻击。
2. 常见单例模式方法
本小节将回顾下常见的单例模式方法,熟悉的同学可以直接跳过这节。
(1)懒汉式
特点:懒加载,线程不安全
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
(2)饿汉式
特点:提前加载,线程安全
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
(3)双重校验锁
特点:懒加载,线程安全
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
(4)静态内部类
特点:懒加载,线程安全
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
3. 防止反射攻击
从第 2 节中列举的常用单例模式方法,可看出这些方法具有共同点之一是私有的构造函数。这是为了防止在该类的外部直接调用构建函数创建对象了。但是该做法却无法防御反射攻击:
public class ReflectAttack {
public static void main(String[] args) throws Exception {
Singleton instance = Singleton.getInstance();
// 获取无参的构造函数
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
// 使用构造函数创建对象
constructor.setAccessible(true);
Singleton reflectInstance = constructor.newInstance();
System.out.println(instance == reflectInstance);
}
}
// output:
// false
下面我们反射攻击枚举类型:
public class ReflectAttack {
public static void main(String[] args) throws Exception {
EnumSingleton instance = EnumSingleton.INSTANCE;
// 获取无参的构造函数
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
// 使用构造函数创建对象
constructor.setAccessible(true);
EnumSingleton reflectInstance = constructor.newInstance();
System.out.println(instance == reflectInstance);
}
}
// output:
// Exception in thread "main" java.lang.NoSuchMethodException: com.chaycao.java.EnumSingleton.<init>()
// at java.lang.Class.getConstructor0(Class.java:3082)
// at java.lang.Class.getDeclaredConstructor(Class.java:2178)
// at com.chaycao.java.ReflectAttack.main(ReflectAttack.java:14)
报了 NoSuchMethodException
异常,是由于 EnumSingleton
中没有无参构造器,那枚举类中的构造函数是怎么样的?
Java 生成的枚举类都会继承 Enum
抽象类,其只有一个构造函数:
public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {
// name: 常量的名称
// ordinal: 常量的序号(枚举声明中的位置,从0开始递增)
// 若以 EnumSingleton 的 INSTANCE 常量为例:
// name = “INSTANCE”;ordinal = 0
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
}
那我们修改下 getDeclaredConstructor
方法的参数,重新获取构造函数试下:
public class ReflectAttack {
public static void main(String[] args) throws Exception {
EnumSingleton instance = EnumSingleton.INSTANCE;
Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
EnumSingleton reflectInstance = constructor.newInstance("REFLECT_INSTANCE", 1);
System.out.println(instance == reflectInstance);
}
}
// output:
// Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
// at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
// at com.chaycao.java.ReflectAttack.main(ReflectAttack.java:16)
这次虽然成功获取到了构造函数,但是仍然报错,并提示我们不能反射创建枚举对象。
错误位于 Constructor
的 newInstance
方法,第 417 行,代码如下:
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
如果该类是 ENUM
类型,则会抛出 IllegalArgumentException
异常,便也阻止了反射攻击。
4. 防止反序列化攻击
下面是对于常用方法的反序列化攻击:
public class DeserializeAttack {
public static void main(String[] args) throws Exception {
Singleton instance = Singleton.getInstance();
byte[] bytes = serialize(instance);
Object deserializeInstance = deserialize(bytes);
System.out.println(instance == deserializeInstance);
}
private static byte[] serialize(Object object) throws Exception {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(object);
byte[] bytes = baos.toByteArray();
return bytes;
}
private static Object deserialize(byte[] bytes) throws Exception {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais);
return ois.readObject();
}
}
// output:
// false
无法阻止反序列攻击,可以成功创建出两个对象。我们改成枚举类型试下:
public class DeserializeAttack {
public static void main(String[] args) throws Exception {
EnumSingleton instance = EnumSingleton.INSTANCE;
byte[] bytes = serialize(instance);
Object deserializeInstance = deserialize(bytes);
System.out.println(instance == deserializeInstance);
}
//....
}
// true
反序列得到的仍是同样的对象,这是为什么,下面深入 ObjectOutputStream
的序列化方法看下 Enum
类型的序列化内容,顺着 writeobject
方法找到 writeObject0
方法。
// ObjectOutputStream > writeobject0()
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
}
对于 Enum
类型将执行专门的 writeEnum
方法进行序列化,该方法内容如下:
private void writeEnum(Enum<?> en,
ObjectStreamClass desc,
boolean unshared) throws IOException
{
// 1. ENUM类型标志(常量):“126”
bout.writeByte(TC_ENUM);
ObjectStreamClass sdesc = desc.getSuperDesc();
// 2. 完整类名:“com.chaycao.java.EnumSingleton: static final long serialVersionUID = 0L;”
writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);
handles.assign(unshared ? null : en);
// 3. Enum对象的名称:“INSTANCE”
writeString(en.name(), false);
}
从上述代码已经可以看出 EnumSingleton.INSTANCE
的反序列化内容。
接着我们再来观察 Enum
类型的反序列化, ObjectInputStream
与 ObjectOutputStream
类似,对于 Enum
类型也使用专用的 readEnum
方法:
private Enum<?> readEnum(boolean unshared) throws IOException {
// 1. 检查标志位
if (bin.readByte() != TC_ENUM) {
throw new InternalError();
}
// 2. 检查类名是否是Enum类型
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;
// 3. 加载类,并使用类的valueOf方法获取Enum对象
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;
}
其过程对应了之前的序列化过程,而其中最重要的便是 Enum.valueOf
方法:
public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {
// name = "INSTANCE"
// 根据名称查找
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);
}
根据名称查找对象,再返回,所以仍会返回 EnumSingleton
中的 INSTANCE
,不会存在反序列化的危险。
综上所述,可知枚举类型在 Java 中天生就不惧怕反射和反序列化的攻击,这是由 Java 自身提供的逻辑保证。那第 2 节中所提及的单例模式方法,是否也有办法能防止反射和反序列攻击?
5.非枚举的防守方法
本节以懒汉式为例,其他单例模式方法同样适用。
(1)防止反射
增加一个标志变量,在构造函数中检查是否已被调用过,若已被调用过,将抛出异常,保证构造函数只被调用一次:
public class Singleton {
private static Singleton instance;
private static boolean isInstance = false;
private Singleton() {
synchronized (Singleton.class) {
if (!isInstance) {
isInstance = true;
} else {
throw new RuntimeException("单例模式受到反射攻击!已成功阻止!");
}
}
}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
(2)防止序列化
增加一个 readResolve
方法并返回 instance
对象。当 ObjectInputStream
类反序列化时,如果对象存在 readResolve
方法,则会调用该方法返回对象。
public class Singleton implements Serializable {
private static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Object readResolve() {
return instance;
}
}
6. 小结
由于 Java 的特殊处理,为枚举防止了反射、序列化攻击,我们可以直接使用枚举,不用担心单例模式的安全性,十分便利。但同时我们也需要记住反射攻击和序列化攻击的存在。
大家可以长按二维码,关注下~
你的订阅,是我写作路上最大的支持!