vlambda博客
学习文章列表

「源码分析」— 为什么枚举是单例模式的最佳方法

1. 引言

枚举类型(enum type)是在 Java 1.5 中引入的一种新的引用类型,是由 Java 提供的一种语法糖,其本质是 int 值。关于其用法之一,便是单例模式,并且在《Effective Java》中有被提到:

单元素的枚举类型已经成为实现 Singleton 的最佳方法

本文便是探究 “为什么枚举是单例模式的最佳方法?”。

答案先写在前面,两个字:“简单”。

 
   
   
 
  1. public enum EnumSingleton {

  2. INSTANCE;

  3. }

Java 在我们使用它的同时,解决了危害单例模式安全性的两个问题:“反射攻击” * 和 *“反序列化攻击”。

本文的内容概要如下:

  1. 回顾常见的单例模式方法;

  2. 探索 Java 中的枚举是如何防止两种攻击;

  3. 若不使用枚举,又如何防止两种攻击。

2. 常见单例模式方法

本小节将回顾下常见的单例模式方法,熟悉的同学可以直接跳过这节。

(1)懒汉式

特点:懒加载,线程不安全

 
   
   
 
  1. public class Singleton {

  2. private static Singleton instance;


  3. private Singleton() {}


  4. public static Singleton getInstance() {

  5. if (instance == null) {

  6. instance = new Singleton();

  7. }

  8. return instance;

  9. }

  10. }

(2)饿汉式

特点:提前加载,线程安全

 
   
   
 
  1. public class Singleton {

  2. private static Singleton instance = new Singleton();


  3. private Singleton() {}


  4. public static Singleton getInstance() {

  5. return instance;

  6. }

  7. }

(3)双重校验锁

特点:懒加载,线程安全

 
   
   
 
  1. public class Singleton {

  2. private volatile static Singleton instance;


  3. private Singleton() {}


  4. public static Singleton getInstance() {

  5. if (instance == null) {

  6. synchronized (Singleton.class) {

  7. if (instance == null) {

  8. instance = new Singleton();

  9. }

  10. }

  11. }

  12. return instance;

  13. }

  14. }

(4)静态内部类

特点:懒加载,线程安全

 
   
   
 
  1. public class Singleton {

  2. private static class SingletonHolder {

  3. private static final Singleton INSTANCE = new Singleton();

  4. }


  5. private Singleton() {}


  6. public static final Singleton getInstance() {

  7. return SingletonHolder.INSTANCE;

  8. }

  9. }

3. 防止反射攻击

从第 2 节中列举的常用单例模式方法,可看出这些方法具有共同点之一是私有的构造函数。这是为了防止在该类的外部直接调用构建函数创建对象了。但是该做法却无法防御反射攻击:

 
   
   
 
  1. public class ReflectAttack {

  2. public static void main(String[] args) throws Exception {

  3. Singleton instance = Singleton.getInstance();

  4. // 获取无参的构造函数

  5. Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();

  6. // 使用构造函数创建对象

  7. constructor.setAccessible(true);

  8. Singleton reflectInstance = constructor.newInstance();

  9. System.out.println(instance == reflectInstance);

  10. }

  11. }

  12. // output:

  13. // false

下面我们反射攻击枚举类型:

 
   
   
 
  1. public class ReflectAttack {

  2. public static void main(String[] args) throws Exception {

  3. EnumSingleton instance = EnumSingleton.INSTANCE;

  4. // 获取无参的构造函数

  5. Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();

  6. // 使用构造函数创建对象

  7. constructor.setAccessible(true);

  8. EnumSingleton reflectInstance = constructor.newInstance();

  9. System.out.println(instance == reflectInstance);

  10. }

  11. }

  12. // output:

  13. // Exception in thread "main" java.lang.NoSuchMethodException: com.chaycao.java.EnumSingleton.<init>()

  14. // at java.lang.Class.getConstructor0(Class.java:3082)

  15. // at java.lang.Class.getDeclaredConstructor(Class.java:2178)

  16. // at com.chaycao.java.ReflectAttack.main(ReflectAttack.java:14)

报了 NoSuchMethodException 异常,是由于 EnumSingleton 中没有无参构造器,那枚举类中的构造函数是怎么样的?

Java 生成的枚举类都会继承 Enum 抽象类,其只有一个构造函数:

 
   
   
 
  1. public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable {

  2. // name: 常量的名称

  3. // ordinal: 常量的序号(枚举声明中的位置,从0开始递增)

  4. // 若以 EnumSingleton 的 INSTANCE 常量为例:

  5. // name = “INSTANCE”;ordinal = 0

  6. protected Enum(String name, int ordinal) {

  7. this.name = name;

  8. this.ordinal = ordinal;

  9. }

  10. }

那我们修改下 getDeclaredConstructor方法的参数,重新获取构造函数试下:

 
   
   
 
  1. public class ReflectAttack {

  2. public static void main(String[] args) throws Exception {

  3. EnumSingleton instance = EnumSingleton.INSTANCE;

  4. Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class, int.class);

  5. constructor.setAccessible(true);

  6. EnumSingleton reflectInstance = constructor.newInstance("REFLECT_INSTANCE", 1);

  7. System.out.println(instance == reflectInstance);

  8. }

  9. }

  10. // output:

  11. // Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects

  12. // at java.lang.reflect.Constructor.newInstance(Constructor.java:417)

  13. // at com.chaycao.java.ReflectAttack.main(ReflectAttack.java:16)

这次虽然成功获取到了构造函数,但是仍然报错,并提示我们不能反射创建枚举对象。

错误位于 ConstructornewInstance方法,第 417 行,代码如下:

 
   
   
 
  1. if ((clazz.getModifiers() & Modifier.ENUM) != 0)

  2. throw new IllegalArgumentException("Cannot reflectively create enum objects");

如果该类是 ENUM 类型,则会抛出 IllegalArgumentException异常,便也阻止了反射攻击。

4. 防止反序列化攻击

下面是对于常用方法的反序列化攻击:

 
   
   
 
  1. public class DeserializeAttack {

  2. public static void main(String[] args) throws Exception {

  3. Singleton instance = Singleton.getInstance();

  4. byte[] bytes = serialize(instance);

  5. Object deserializeInstance = deserialize(bytes);

  6. System.out.println(instance == deserializeInstance);

  7. }


  8. private static byte[] serialize(Object object) throws Exception {

  9. ByteArrayOutputStream baos = new ByteArrayOutputStream();

  10. ObjectOutputStream oos = new ObjectOutputStream(baos);

  11. oos.writeObject(object);

  12. byte[] bytes = baos.toByteArray();

  13. return bytes;

  14. }


  15. private static Object deserialize(byte[] bytes) throws Exception {

  16. ByteArrayInputStream bais = new ByteArrayInputStream(bytes);

  17. ObjectInputStream ois = new ObjectInputStream(bais);

  18. return ois.readObject();

  19. }

  20. }

  21. // output:

  22. // false

无法阻止反序列攻击,可以成功创建出两个对象。我们改成枚举类型试下:

 
   
   
 
  1. public class DeserializeAttack {

  2. public static void main(String[] args) throws Exception {

  3. EnumSingleton instance = EnumSingleton.INSTANCE;

  4. byte[] bytes = serialize(instance);

  5. Object deserializeInstance = deserialize(bytes);

  6. System.out.println(instance == deserializeInstance);

  7. }

  8. //....

  9. }

  10. // true

反序列得到的仍是同样的对象,这是为什么,下面深入 ObjectOutputStream 的序列化方法看下 Enum 类型的序列化内容,顺着 writeobject方法找到 writeObject0方法。

 
   
   
 
  1. // ObjectOutputStream > writeobject0()

  2. if (obj instanceof String) {

  3. writeString((String) obj, unshared);

  4. } else if (cl.isArray()) {

  5. writeArray(obj, desc, unshared);

  6. } else if (obj instanceof Enum) {

  7. writeEnum((Enum<?>) obj, desc, unshared);

  8. }

对于 Enum 类型将执行专门的 writeEnum方法进行序列化,该方法内容如下:

 
   
   
 
  1. private void writeEnum(Enum<?> en,

  2. ObjectStreamClass desc,

  3. boolean unshared) throws IOException

  4. {

  5. // 1. ENUM类型标志(常量):“126”

  6. bout.writeByte(TC_ENUM);

  7. ObjectStreamClass sdesc = desc.getSuperDesc();

  8. // 2. 完整类名:“com.chaycao.java.EnumSingleton: static final long serialVersionUID = 0L;”

  9. writeClassDesc((sdesc.forClass() == Enum.class) ? desc : sdesc, false);

  10. handles.assign(unshared ? null : en);

  11. // 3. Enum对象的名称:“INSTANCE”

  12. writeString(en.name(), false);

  13. }

从上述代码已经可以看出 EnumSingleton.INSTANCE 的反序列化内容。

接着我们再来观察 Enum 类型的反序列化, ObjectInputStreamObjectOutputStream 类似,对于 Enum 类型也使用专用的 readEnum 方法:

 
   
   
 
  1. private Enum<?> readEnum(boolean unshared) throws IOException {

  2. // 1. 检查标志位

  3. if (bin.readByte() != TC_ENUM) {

  4. throw new InternalError();

  5. }


  6. // 2. 检查类名是否是Enum类型

  7. ObjectStreamClass desc = readClassDesc(false);

  8. if (!desc.isEnum()) {

  9. throw new InvalidClassException("non-enum class: " + desc);

  10. }


  11. int enumHandle = handles.assign(unshared ? unsharedMarker : null);

  12. ClassNotFoundException resolveEx = desc.getResolveException();

  13. if (resolveEx != null) {

  14. handles.markException(enumHandle, resolveEx);

  15. }


  16. String name = readString(false);

  17. Enum<?> result = null;

  18. // 3. 加载类,并使用类的valueOf方法获取Enum对象

  19. Class<?> cl = desc.forClass();

  20. if (cl != null) {

  21. try {

  22. @SuppressWarnings("unchecked")

  23. Enum<?> en = Enum.valueOf((Class)cl, name);

  24. result = en;

  25. } catch (IllegalArgumentException ex) {

  26. throw (IOException) new InvalidObjectException(

  27. "enum constant " + name + " does not exist in " +

  28. cl).initCause(ex);

  29. }

  30. if (!unshared) {

  31. handles.setObject(enumHandle, result);

  32. }

  33. }


  34. handles.finish(enumHandle);

  35. passHandle = enumHandle;

  36. return result;

  37. }

其过程对应了之前的序列化过程,而其中最重要的便是 Enum.valueOf方法:

 
   
   
 
  1. public static <T extends Enum<T>> T valueOf(Class<T> enumType, String name) {

  2. // name = "INSTANCE"

  3. // 根据名称查找

  4. T result = enumType.enumConstantDirectory().get(name);

  5. if (result != null)

  6. return result;

  7. if (name == null)

  8. throw new NullPointerException("Name is null");

  9. // 没有找到,抛出异常

  10. throw new IllegalArgumentException(

  11. "No enum constant " + enumType.getCanonicalName() + "." + name);

  12. }

根据名称查找对象,再返回,所以仍会返回 EnumSingleton中的 INSTANCE,不会存在反序列化的危险。

综上所述,可知枚举类型在 Java 中天生就不惧怕反射和反序列化的攻击,这是由 Java 自身提供的逻辑保证。那第 2 节中所提及的单例模式方法,是否也有办法能防止反射和反序列攻击?

5.非枚举的防守方法

本节以懒汉式为例,其他单例模式方法同样适用。

(1)防止反射

增加一个标志变量,在构造函数中检查是否已被调用过,若已被调用过,将抛出异常,保证构造函数只被调用一次:

 
   
   
 
  1. public class Singleton {

  2. private static Singleton instance;

  3. private static boolean isInstance = false;


  4. private Singleton() {

  5. synchronized (Singleton.class) {

  6. if (!isInstance) {

  7. isInstance = true;

  8. } else {

  9. throw new RuntimeException("单例模式受到反射攻击!已成功阻止!");

  10. }

  11. }

  12. }


  13. public static Singleton getInstance() {

  14. if (instance == null) {

  15. instance = new Singleton();

  16. }

  17. return instance;

  18. }

  19. }

(2)防止序列化

增加一个 readResolve 方法并返回 instance 对象。当 ObjectInputStream类反序列化时,如果对象存在 readResolve 方法,则会调用该方法返回对象。

 
   
   
 
  1. public class Singleton implements Serializable {

  2. private static Singleton instance;


  3. private Singleton() {}


  4. public static Singleton getInstance() {

  5. if (instance == null) {

  6. instance = new Singleton();

  7. }

  8. return instance;

  9. }


  10. private Object readResolve() {

  11. return instance;

  12. }

  13. }

6. 小结

由于 Java 的特殊处理,为枚举防止了反射、序列化攻击,我们可以直接使用枚举,不用担心单例模式的安全性,十分便利。但同时我们也需要记住反射攻击和序列化攻击的存在。


大家可以长按二维码,关注下~

你的订阅,是我写作路上最大的支持!