反序列化会破坏单例,因objectinputstream绕过构造函数直接新建实例;readresolve通过“事后拦截+替换”修复该漏洞,需严格遵循签名规范;枚举单例因jvm硬编码保护天然免疫。

为什么反序列化会破坏单例
Java 默认反序列化机制会绕过构造函数,直接通过反射新建对象实例——哪怕你的类用 private 构造器 + 静态内部类/枚举等方式保证了“构造唯一”,ObjectInputStream 仍会调用 ObjectStreamClass.newInstance() 创建新实例。结果就是:同一个类,Singleton.getInstance() 返回一个对象,而反序列化出来的却是另一个。
readResolve 是怎么堵住这个漏洞的
在反序列化流程末尾,JVM 会检查类是否定义了 readResolve 方法(签名必须是 private Object readResolve())。如果存在,就用它返回的对象替换刚创建的反序列化实例。本质是“事后拦截+替换”。
实操建议:
- 方法必须是
private、返回类型为Object、无参数,名字严格为readResolve - 返回值应是你单例持有的那个唯一实例,通常是
INSTANCE或getInstance() - 不能抛出受检异常;若逻辑出错可抛
RuntimeException,但会导致反序列化失败 - 注意:该方法只对当前类生效,父类或子类不继承,需各自实现
示例:
立即学习“Java免费学习笔记(深入)”;
private Object readResolve() {
return INSTANCE;
}
枚举单例为什么天然免疫
因为 Java 规范明确禁止对枚举常量进行反序列化重建——Enum.valueOf() 总是返回已有实例,JVM 层面硬编码了这一行为。你甚至不需要写 readResolve。
但要注意:
- 枚举单例无法继承(
extends)、不能实现延迟初始化逻辑(如依赖外部配置) - 如果已有类是普通类且已上线,改枚举成本高,此时
readResolve是更现实的补救方案 - 某些框架(如早期 Hessian)可能绕过枚举保护,不过标准 JDK
ObjectInputStream安全
容易被忽略的兼容性细节
readResolve 看似简单,但几个边界情况常导致失效:
- 类实现了
Externalizable接口时,readResolve不会被调用——必须改用readExternal中手动返回单例 - 使用 Kryo、Jackson 等非标准序列化库时,
readResolve默认不生效,需显式注册或配置支持 - 如果单例持有不可序列化字段(如
ThreadLocal、Socket),反序列化后这些字段为null,readResolve无法恢复它们,得配合transient+ 自定义readObject - Android 上部分低版本 Runtime 对
readResolve支持不稳定,建议搭配 ProGuard 检查方法是否被误删
真正麻烦的不是加一行 readResolve,而是确认它在你用的序列化路径里确实被执行了——别只测 ObjectOutputStream,要覆盖所有实际走的链路。








