添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

1. 引言

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

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

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

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

public enum EnumSingleton {
    INSTANCE;

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

本文的内容概要如下:

  • 回顾常见的单例模式方法;
  • 探索 Java 中的枚举是如何防止两种攻击;
  • 若不使用枚举,又如何防止两种攻击。
  • 2. 常见单例模式方法

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

    public class Singleton {
        private static Singleton instance;
        private Singleton() {}
        public static Singleton getInstance() {
            if (instance == null) {
                instance = new Singleton();
            return instance;
        

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

    public class Singleton {
        private static Singleton instance = new Singleton();
        private Singleton() {}
        public static Singleton getInstance() {
            return instance;
        

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

    双重校验锁

    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;
        

    特点:懒加载,线程安全

    静态内部类

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