NotSerializableException 是运行时异常,由 ObjectOutputStream.writeObject() 或 ObjectInputStream.readObject() 触发,暴露序列化链中首个不可序列化类型;transient 仅跳过字段序列化,不解决逻辑风险,需配合 readObject 自定义重建;预防应优先于修复。

NotSerializableException 不是编译错误,而是在调用 ObjectOutputStream.writeObject() 或 ObjectInputStream.readObject() 时才触发的运行时异常。它真正暴露的是“序列化链中最先遇到的不可序列化类型”,而非你正在操作的主对象本身。transient 关键字不是绕过问题的捷径,而是有明确语义的控制手段——它跳过字段序列化,但不解决字段缺失引发的后续逻辑风险。
NotSerializableException 的典型触发路径
异常总在流操作那一刻抛出,原因在于 Java 序列化机制只做**运行时深度检查**:即使类声明了 implements Serializable,只要其任意一个非 transient、非 static 字段的类型(包括嵌套字段)未实现 Serializable,就会中断流程。
- 常见“隐形炸弹”字段:SLF4J 的
Logger、Thread、Socket、Connection、DateFormat(非SimpleDateFormat)、自定义工具类实例等 - 堆栈中显示的类名(如
com.example.MyService)通常是被引用的成员类型,不是你显式序列化的那个对象 - IDET 的引用查找容易漏掉间接依赖,比如 A → B → C → Logger,C 没标记 transient,异常就报在 Logger 上
transient 的真实作用与边界
transient 是一种显式排除机制,告诉 JVM:“这个字段我不打算持久化,别管它”。但它不改变字段本身的类型约束,也不修复设计缺陷。
- 适用场景明确:日志器、线程局部变量、数据库连接、缓存容器、回调监听器、临时计算结果、敏感字段(如密码)
- 反序列化后该字段值为 null(引用类型)或默认值(基本类型),不会保留原值
- 不能加在 final 字段上再指望它有值——final 字段不走构造器也不走默认赋值,反序列化后必为默认值
- static 字段天然不参与序列化,加 transient 无意义
比 transient 更进一步的可控处理
当字段必须存在且不能为 null 时,仅靠 transient 不够,需配合自定义序列化逻辑。
- 在类中声明私有方法
private void readObject(ObjectInputStream in) - 第一行必须调用
in.defaultReadObject()完成其他字段的默认反序列化 - 随后手动重建 transient 字段,例如:
this.logger = LoggerFactory.getLogger(getClass()); - 若字段依赖外部上下文(如 Spring Bean),需确保反序列化环境已就绪,否则可能 NPE
预防优于修复的实践建议
从设计阶段降低序列化风险,比事后打补丁更可靠。
- 实体类优先使用 POJO 结构,避免直接持有
Service、Repository等运行期组件 - 对第三方类型字段保持警惕,查 Javadoc 确认是否实现
Serializable - 使用
serialVersionUID并定期更新,让版本不兼容问题提前暴露,而不是静默失败 - 单元测试中加入序列化/反序列化断言,尤其覆盖含 logger、connection 等典型字段的用例

















