bigywater.tech
Java 内部原理 · 64

枚举单例真实验证:反射和反序列化对枚举真实无效

"用枚举实现单例最安全"是《Effective Java》里的经典建议,这篇真实用反射攻击和序列化两条常见的破坏单例的路径,分别打一遍普通单例和枚举单例,验证枚举到底安全在哪。

普通单例真实被攻破
真实验证:反射拿到私有构造器并 setAccessible(true) 后,真实创建出了第二个实例,fake == getInstance() 为 false
枚举反射真实攻击失败
真实验证:对枚举做同样的反射操作,在拿构造器这一步就真实抛出 NoSuchMethodException

上一篇:静态嵌套类 vs 非静态内部类真实验证

真实验证:普通单例真实被反射攻破

私有构造器 + 静态字段实现的经典单例,反射能不能绕过 private 修饰符真实创建第二个实例。

EnumSingletonDemo.java class RegularSingleton { private static final RegularSingleton INSTANCE = new RegularSingleton(); private RegularSingleton() {} public static RegularSingleton getInstance() { return INSTANCE; } } Constructor<RegularSingleton> ctor = RegularSingleton.class.getDeclaredConstructor(); ctor.setAccessible(true); RegularSingleton fake = ctor.newInstance();
真实实测
普通单例反射攻击: fake == RegularSingleton.getInstance() : false
普通单例被真实攻破,产生了第二个实例

真实结果证明 private 修饰符对反射来说只是一层"默认拒绝",setAccessible(true) 真实能绕过这层访问检查,直接调用私有构造器创建出第二个实例——单例的"唯一性"被真实打破了。

真实验证:同样的攻击手法,对枚举真实失效

用同样的反射手法(getDeclaredConstructor + setAccessible + newInstance)去攻击一个枚举单例。

EnumSingletonDemo.java enum Singleton { INSTANCE; int state = 0; } Constructor<Singleton> enumCtor = Singleton.class.getDeclaredConstructor();
真实实测
枚举反射攻击真实失败: NoSuchMethodException - EnumSingletonDemo$Singleton.<init>()
Singleton 枚举真实的构造器个数: 1
  真实构造器签名: [class java.lang.String, int]
真实失败的原因,在字节码层面就已经注定。反射攻击这次真实在第一步——getDeclaredConstructor()(无参版本)——就失败了,原因是枚举的构造器真实不是无参的:编译器给每个枚举类型的构造器真实强制加上了两个隐藏参数,分别是枚举常量的名字(String)和序号(int),这是 JVM 规范对枚举类型的硬性要求,不是库层面的约定,普通开发者的代码没有办法绕过或重载出一个无参构造器。即使真的手动拿到这个双参数构造器并强行调用,JVM 在类加载和枚举实例化阶段也会真实做额外校验,官方文档明确说明枚举类型的反射实例化在规范层面就是被禁止的。

真实验证:枚举反序列化真实保持单例

另一条常见的破坏单例路径是序列化再反序列化——真实验证枚举在这条路径上的表现。

真实实测
枚举反序列化: deserialized == Singleton.INSTANCE : true
枚举反序列化后 state 保持: 100

真实结果证明枚举的反序列化机制和普通类完全不同:普通类反序列化默认会通过反射创建一个新对象(除非手动实现 readResolve() 来"纠正"这一点,这也是很多单例实现容易遗漏的一步),而枚举的反序列化真实是由 JVM 按名字查找已存在的枚举常量,直接返回这个真实唯一的实例,不会创建新对象——这也是《Effective Java》推荐枚举单例的核心原因:不需要手写任何额外代码去防御反射和序列化,JVM 在语言规范层面就把这两条路都堵死了。

分步演示:攻击是在哪一步真实失手的

每一行都是真实运行这段代码得到的结果或异常。

同样的反射攻击手法,普通单例真实被攻破,枚举在第一步就真实失败
这一步新验证的调用

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,反射攻击结果和反序列化结果重复运行完全一致。
    • 和enum values() 隐藏克隆那篇一起看,能更完整理解枚举类型在字节码层面被编译器和 JVM 特殊对待的各种细节。
    • 没有涉及:普通单例如何通过实现 readResolve() 方法来防御反序列化攻击(可以做到,但需要手写)、双重检查锁定(DCL)单例在多线程下的真实正确性——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号