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

时间:2022-07-22
本文章向大家介绍「源码分析」— 为什么枚举是单例模式的最佳方法,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

1. 引言

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

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

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

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

public enum EnumSingleton {
    INSTANCE;
}

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

本文的内容概要如下:

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