Java单例序列化破坏的核心是反序列化绕过构造器直接新建对象;加private Object readResolve()方法可在反序列化末尾替换为原实例,确保==为true;枚举单例因JVM强制保障而天然免疫反射与序列化攻击。

Java 中单例模式在序列化时被破坏,核心在于反序列化不调用构造器,而是直接新建对象——哪怕你用了私有构造、静态内部类或双重检查锁,ObjectInputStream 仍会绕过所有初始化逻辑,生成一个新实例。
加 readResolve() 方法是最常用修复手段
它不是阻止反序列化,而是在反序列化完成后“替换结果”:
- 方法签名必须严格为 private Object readResolve(),返回值是真正的单例实例(如
return INSTANCE;) - JVM 在反序列化末尾自动检测并调用该方法,用它的返回值覆盖刚创建的对象
- 适用于实现了
Serializable的普通单例类(饿汉、懒汉、静态内部类等)
用枚举实现单例最彻底防破坏
这是 JVM 层面强制保障的方案,无需任何额外代码:
- 枚举常量(如
INSTANCE)在类加载时就固定,之后所有访问都指向同一引用 - 反序列化时跳过构造器,直接调用
Enum.valueOf()查表复用已有实例 - 反射无法创建新枚举实例,JVM 明确抛出
IllegalArgumentException - 即使字节流被篡改,只要常量名存在,就只返回注册表中的那个对象
注意这些容易失效的情况
readResolve 不是万能的,以下场景会让它形同虚设:
立即学习“Java免费学习笔记(深入)”;
- 类实现了
Externalizable接口:此时readResolve完全不触发,必须在readExternal中手动返回单例 - 使用 Kryo、Jackson 等非标准序列化库:默认不识别
readResolve,需显式开启支持 - Android 混淆(如 ProGuard)删掉了该方法:需在 keep 规则中保留
readResolve - 字段含
transient或不可序列化类型(如ThreadLocal):这些字段反序列化后为null,readResolve不负责恢复
别忘了反射攻击也要一起防
仅防序列化还不够,反射仍可调用私有构造器新建实例:
- 在私有构造器中加入状态校验(如用静态 flag 标记是否已初始化)
- 但更推荐直接用枚举——它从语言层面禁止反射创建,连
setAccessible(true)都无效 - 如果业务需要运行时注入依赖(如 Spring 管理 Bean),枚举不适用,此时应结合容器托管 + readResolve + 构造器防护


















