反射破坏单例的核心是先用getdeclaredconstructor()获取私有构造器,再调setaccessible(true)绕过访问检查,最后newinstance()创建新实例;枚举单例因jvm硬编码保护可免疫,其他需在构造器内校验instance是否已存在且声明为static final。

直接调用 Constructor.newInstance() 本身并不“暴力”,真正起作用的是配合 setAccessible(true) 关闭访问检查,再强行触发私有构造器——这才是突破单例私有防线的核心动作。
拿到私有构造器并解除访问限制
必须用 getDeclaredConstructor()(不是 getConstructor()),它能获取包括 private 在内的所有构造器。接着调用 setAccessible(true),绕过 JVM 的语言级访问控制。这一步不可省略,否则调用 newInstance() 会立刻抛出 IllegalAccessException。
- 若目标类无参,传
null或空数组:getDeclaredConstructor() - 若有参,类型必须严格匹配,比如
String.class, int.class,不能靠自动装箱“蒙混过关” - 模块化环境(JDK 9+)下,还需确保目标包对
java.base模块开放,否则setAccessible可能静默失败
让构造器执行时“不设防”
很多单例只在 getInstance() 方法里做校验,但构造器本身没加防护。一旦反射调用成功,就等于新建一个对象,完全绕开原有逻辑。
- 饿汉式看似安全,但若构造器里没判断已存在实例,反射仍可 new 出第二个
- 懒汉式或双重检查锁,只要
instance字段不是final,就可能被反射设为null后再次触发初始化 - 常见错误是只在静态块或方法里判空,却忘了构造器也该是“守门人”
为什么有时看起来“没破坏成功”
不是反射失效,而是单例实现本身做了防御。比如在私有构造器开头加了 if (instance != null) throw new RuntimeException() —— 这种写法依赖外部静态字段状态,但字段若非 final,仍可能被反射篡改后再调用。
- 真正可靠的防御是把
instance声明为private static final,这样反射set会失败或抛异常 - 枚举单例根本无法通过反射创建实例,因为
Enum类型的构造器在 JVM 层被硬编码拦截,getDeclaredConstructor()都拿不到 - JDK 9+ 虽标记
newInstance()过时,但实际仍在用;替代方案如工厂或 DI 容器,本质是避开反射,而非修复反射漏洞
关键不在“能不能调”,而在“调完能不能活”
反射能调私有构造器,是 Java 语言特性;但调完能否得到一个可用、独立、绕过单例约束的对象,取决于构造器内部是否做了状态自检,以及 instance 字段是否具备不可变性。
- 如果构造器只是简单初始化字段,没做任何已有实例校验,那一次
newInstance()就等于一次新实例 - 如果构造器抛异常、或依赖不可篡改的
final字段,反射就只能走到一半,创建失败 - 所谓“暴力”,其实是利用了设计上对构造器入口缺乏防御的疏忽,不是技术本身有多强











