readobject抛异常能阻止反序列化,因其在反序列化初始阶段主动中断流程,跳过字段还原;但仅对本类生效,需严格匹配签名、正确声明异常,且依赖serialversionuid一致才触发。

为什么 readObject 抛异常能阻止反序列化
Java 反序列化流程中,只要类实现了 Serializable,JVM 就会尝试调用其私有的 readObject 方法(如果存在)来重建对象。你主动定义它并抛异常,等于在反序列化链最开始就掐断执行——不是“不让序列化”,而是“一读就炸”,连字段还原都跳过。
注意:这只能防反序列化,不影响 writeObject 或正常构造;且仅对本类生效,不阻止子类或代理类绕过。
- 必须声明为
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException,签名错一个字(比如少throws或参数类型不对)就无效 - 异常必须是
IOException或ClassNotFoundException的实例,抛RuntimeException会被 JVM 吞掉或转成InvalidClassException,行为不可控 - 方法体第一行建议加
throw new InvalidClassException("Deserialization is not allowed");,比new RuntimeException()更明确
如何让 readObject 真正生效而不被忽略
IDE 自动生成的 readObject 常漏掉关键细节,导致你以为挡住了,其实没拦住。最常见原因是:没调用 in.defaultReadObject() 之前就抛异常——但这不是问题,反而是正确姿势;真正失效的情况是方法根本没被识别到。
- 方法必须严格匹配签名:
private void readObject(ObjectInputStream in),不能是public、不能带多余参数、不能返回值 - 类不能是
record(Java 14+),record 的序列化逻辑绕过自定义readObject - 如果父类也定义了
readObject,子类必须显式重写,否则继承父类逻辑(可能不抛异常) - 使用
serialver工具检查serialVersionUID是否一致,版本不匹配时可能跳过该方法
readObject 和 serialVersionUID 配合使用的实际效果
serialVersionUID 本身不阻止反序列化,但它和 readObject 是组合技:前者控制“是否允许跨版本读”,后者控制“读到一半就终止”。两者一起用,才能覆盖更多攻击面。
立即学习“Java免费学习笔记(深入)”;
- 不设
serialVersionUID时,JVM 自动生成,但类结构稍变(比如加个字段)就会导致InvalidClassException,这时你的readObject根本不会被执行 - 显式声明
private static final long serialVersionUID = 1L;后,JVM 才会走到readObject这一步,你的异常才有机会抛出 - 如果目标是彻底禁用,建议把
serialVersionUID设成随机大数(如0xDEADBEEFL),再配合readObject抛异常,双重保险
哪些场景下 readObject 会失效
它不是银弹。遇到反射调用、第三方序列化库(如 Kryo、Jackson)、或 ObjectInputStream 被包装/重写时,这个钩子大概率被跳过。
- Spring 的
GenericJackson2JsonRedisSerializer不走 Java 原生序列化,readObject完全无效 - Apache Commons Collections 等老漏洞利用链,常通过
AnnotationInvocationHandler等内置可序列化类做跳板,你的业务类哪怕写了readObject也拦不住 - 使用
Unsafe.allocateInstance()+ 字段反射直接构造对象,完全绕过反序列化流程 - 如果类还实现了
Externalizable,则readObject被忽略,必须改用readExternal
真正难防的是那些不经过 ObjectInputStream 的路径。别只盯着 readObject,得看清楚你用的到底是不是原生序列化。










