枚举单例是java中唯一能天然防御反射与反序列化攻击的方案,因jvm在类加载时硬编码创建实例,反射无法获取或调用其构造器,反序列化直接复用已有实例。

能直接破解,只要单例类有无参私有构造器,且没做防御性校验。
核心三步操作
反射绕过 private 限制不靠魔法,而是标准 API 的合法调用:
- 用
getDeclaredConstructor()获取私有构造器对象(注意不是getConstructor()) - 调用
setAccessible(true)关闭 JVM 的访问检查机制 - 执行
newInstance()触发构造逻辑,生成全新对象
为什么这三步能成功
private 是编译期和常规调用的语义约束,并非运行时强制壁垒。JVM 在反射层面提供了关闭访问检查的能力,而单例类若只靠“私有”二字防护,就等于没锁门——只是把钥匙藏起来了,反射就是那把万能备用钥匙。
典型可攻破的单例写法
以下实现默认都挡不住反射,哪怕加了 synchronized 或 volatile:
- 饿汉式:静态字段早初始化,但构造器仍是 private,反射仍可 new 出第二个
- 懒汉式 / DCL:getInstance 没被调用前,反射就能抢先创建实例
- 静态内部类:延迟加载靠类加载机制,但外部类私有构造器仍可被直接调用
真正起作用的防御点在哪
不在类加载、不在同步块、也不在 final 修饰字段——而在构造器内部。
必须在私有构造器第一行加入主动校验:
- 检查已有 instance 是否非 null,是则抛 RuntimeException
- 或用静态 boolean 标志位(如
alreadyInitialized),首次设为 true,二次进入即拒绝 - 注意:instance 字段需为
static且可被构造器读取到(饿汉式、静态内部类方式中需确保其已加载并赋值)
唯一免检方案:枚举单例
Java 枚举的实例由 JVM 在类加载阶段硬编码创建,其构造器无法被反射获取或调用:
-
getDeclaredConstructor()会直接抛NoSuchMethodException - 即使绕过拿到构造器,
newInstance()必抛IllegalArgumentException - 反序列化也复用已有实例,天然防双重攻击











