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

1. 引言

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

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

本文即是探究 “为何枚举是单例模式的最佳方法?”。微信

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

  
  
  
   
   
            
   
   
  1. flex

  2. this

  3. spa

public enum EnumSingleton { INSTANCE;}

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

本文的内容概要以下:线程

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

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

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

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)

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

错误位于 ConstructornewInstance方法,第 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 类型的反序列化, ObjectInputStreamObjectOutputStream 相似,对于 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 的特殊处理,为枚举防止了反射、序列化攻击,咱们能够直接使用枚举,不用担忧单例模式的安全性,十分便利。但同时咱们也须要记住反射攻击和序列化攻击的存在。


你们能够长按二维码,关注下~

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


本文分享自微信公众号 - java宝典(java_bible)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索