你可记得你在使用阿里的Dubbo的时候,有时候会遇到一个异常:
java.io.NotSerializableException?
为啥会报这个错? 因为对象想要在网络上传输,或者持久化到磁盘,必须将它转成字节流。也就是说,需要序列化成字节流。而当你的JAVA类实现了 java.io.Serializable 后,NotSerializableException 就消失了。
当时Java刚推出 RMI (远程方法调用),Sun公司的设计团队有一个核心愿景:让网络调用像本地调用一样透明。
为了实现这个愿景,他们必须解决对象如何在网络间传输的问题。 他们的设计哲学是侵入性最小化。他们不希望开发者为了传输数据去写复杂的解析代码(像C++那样),因此发明了 Marker Interface(标记接口) 模式。
JAVA类只需要 implements Serializable,JVM的底层通过反射机制就会自动把对象转换成字节流。这种设计在当时,极大地降低了分布式开发的门槛。
public interface Serializable { // 里面是空的! }
那么当你的JAVA类实现了这个接口后,就等于是在告诉JAVA的虚拟机,把这个类的内部数据(包括私有字段)读取出来,变成字节流发送出去。
好,到这里,就可以简单说一下序列化和反序列化的概念了。
1. 核心概念
序列化 (Serialization)
- 定义:将内存中的对象(Object)变成字节流(byte[])的过程。
- 目的:
- 持久化:只有变成了字节流,才能写入磁盘文件或数据库。
- 网络传输:通过网络发送给其他进程或服务(如RPC)。
- Dubbo 里的角色:服务 A 把
User对象打碎成字节流,塞进网络。
反序列化 (Deserialization)
- 定义:将收到的字节流,重新组装成内存对象的过程。
- 目的:恢复对象,让程序能继续使用。
- Dubbo 里的角色:服务 B 从网络收到一堆字节,重新在自己的内存里
new出一个一模一样的对象。
2. Java 序列化的默认规则与生命周期
writeObject 和 readObject 方法,在日常的业务代码中,几乎从来都没有见过的,因为默认就够用了,不需要专门去覆盖这两个方法的。它们是Java序列化机制中的“钩子”方法。
下面我用微服务A去调用微服务B的 findUser(UserDTO userDto) 接口作为例子来说明整个过程。
微服务A:发请求之前,序列化发生
- 你在微服务A里写业务代码
userService.findUser(userDto); - A 端的 RPC 代理(动态代理/Stub)拦截调用,把它包装成一个请求对象;
- RPC 框架准备把请求发到网络上,触发序列化:创建
ObjectOutputStream并调用ObjectOutputStream.writeObject(request);
userDto 被发送到网络之前,会触发 writeObject。
微服务B:收到请求之后 ——反序列化发生
- B 端网络线程收到字节流(byte[])
- RPC 框架开始反序列化:创建
ObjectInputStream,调用ObjectInputStream.readObject(),读回request对象图,其中包含userDto
userDto 入参到达业务方法之前,触发 readObject(钩子)。
3. 深入理解 Java 序列化机制
3.1 serialVersionUID:版本控制的关键
serialVersionUID 是一个 long 类型的静态常量,用于在序列化和反序列化过程中验证类的版本兼容性。
- 作用:当序列化一个对象时,JVM 会把
serialVersionUID写入字节流。反序列化时,JVM 会比较字节流中的serialVersionUID和当前类的serialVersionUID。 - 缺失的后果:如果你没有显式声明
serialVersionUID,JVM 会根据类的结构(字段、方法等)自动生成一个。一旦类的结构发生变化(即使只是添加一个私有字段),自动生成的serialVersionUID就会改变,导致反序列化时抛出InvalidClassException。 - 最佳实践:
- 显式声明:建议为所有实现
Serializable接口的类显式声明private static final long serialVersionUID = 1L;。 - 版本升级:当类的结构发生重大、不兼容的变化时,可以递增
serialVersionUID的值,以明确表示这是一个不兼容的版本。
- 显式声明:建议为所有实现
3.2 transient 关键字:排除敏感数据或不需要序列化的字段
-
作用:用
transient关键字修饰的字段,在对象序列化时,其值不会被写入到字节流中。反序列化时,这些transient字段会被赋予其数据类型的默认值(例如,对象类型为null,基本类型为0或false)。 -
常见用途:
- 敏感信息:如密码、密钥等,不希望被持久化或网络传输。
- 临时数据:只在当前对象生命周期内有效,不需要跨进程或持久化。
- 不可序列化对象:如果一个字段本身是不可序列化的(例如
Thread对象),但又必须作为成员变量存在,可以将其标记为transient。
public class User implements Serializable { private static final long serialVersionUID = 1L; private String username; private transient String password; // 密码不参与序列化 private int age; }
3.3 自定义序列化逻辑:writeObject 和 readObject
虽然默认序列化通常够用,但在某些高级场景下,你可能需要自定义序列化和反序列化的过程。
-
何时需要自定义:
- 加密/解密:在序列化前对敏感数据进行加密,反序列化后解密。
- 处理不可序列化字段:如果类中包含不可序列化的字段,但你又希望在序列化时保存其“状态”,可以在
writeObject中手动处理,并在readObject中恢复。 - 优化存储格式:例如,压缩数据以减少字节流大小。
- 兼容旧版本:处理不同版本类结构间的兼容性问题。
-
如何自定义 (简述):
通过在类中声明private void writeObject(ObjectOutputStream out) throws IOException;和private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;方法,JVM 在序列化/反序列化时会自动调用它们,而不是默认的机制。在这些方法中,你可以使用out.defaultWriteObject()和in.defaultReadObject()来调用默认的序列化/反序列化逻辑,并在其前后添加自定义处理。
4. Java 序列化的局限性与替代方案
Java 内置的序列化机制虽然方便,但也存在一些显著的局限性,尤其是在高性能、跨语言和大规模分布式系统中。
4.1 局限性
- 性能问题:Java 序列化通常比其他序列化框架慢,因为它涉及大量的反射操作和复杂的对象图遍历。
- 字节流体积大:生成的字节流通常包含大量的元数据(如类名、字段名等),导致数据包较大,占用更多网络带宽和存储空间。
- 跨语言兼容性差:Java 序列化是 Java 特有的,无法直接与其他语言(如 Python, Go, C++)进行数据交换。
- 安全风险:这是最严重的问题之一,Java 反序列化漏洞是常见的攻击向量,恶意构造的字节流可能导致远程代码执行 (RCE)。
4.2 常见替代方案
鉴于上述局限性,在现代分布式系统中,尤其是 RPC 框架如 Dubbo 中,通常会采用更高效、更安全的序列化协议。
-
Kryo:
- 特点:高性能、高效率的 Java 序列化库。
- 优势:速度快,生成的字节流小,支持循环引用。
- 适用场景:主要用于 Java 内部系统,如缓存、RPC 数据传输。
-
Protobuf (Protocol Buffers):
- 特点:Google 开发的语言无关、平台无关、可扩展的序列化数据结构。
- 优势:非常高效,字节流极小,支持多种语言,通过
.proto文件定义数据结构,具有良好的向前和向后兼容性。 - 适用场景:跨语言 RPC 通信、数据存储。
-
Hessian:
- 特点:轻量级的 RPC 协议,支持多种语言。
- 优势:基于 HTTP 协议,易于穿越防火墙,性能较好,字节流相对紧凑。
- 适用场景:Web Service、跨语言 RPC。
-
JSON (Jackson/Gson):
- 特点:文本格式,人类可读。
- 优势:通用性强,几乎所有语言都支持,易于调试。
- 劣势:字节流体积相对较大,解析性能不如二进制协议。
- 适用场景:RESTful API、Web 前后端数据交互、配置存储。
5. 安全警示:反序列化漏洞
Java 反序列化漏洞是一个长期存在的严重安全问题。攻击者可以构造恶意的序列化字节流,当应用程序对其进行反序列化时,可能触发应用程序中的特定代码执行,从而导致远程代码执行 (RCE) 等严重后果。
- 风险描述:攻击者利用反序列化过程中调用的某些方法(如
readObject)来执行任意代码。 - 防范措施:
- 避免对不可信来源的数据进行反序列化。
- 使用白名单机制:只允许反序列化已知且安全的类。
- 升级到更安全的序列化框架:如 Protobuf、Kryo 等,它们通常不涉及 Java 对象的任意代码执行。
- 使用最新的 Java 版本和安全补丁。
6. 总结与建议
- 理解核心:序列化是将对象转换为字节流,反序列化是将其恢复。
- Java
Serializable:标记接口,方便但有局限。 serialVersionUID:务必显式声明,用于版本兼容性。transient:排除不需要序列化的字段。- 自定义:在特殊场景下通过
writeObject/readObject增强控制。 - 选择合适的协议:在分布式系统中,根据性能、跨语言和安全需求,优先考虑 Kryo、Protobuf、Hessian 或 JSON 等替代方案。
- 安全至上:警惕反序列化漏洞,不要反序列化不可信数据。
1144

被折叠的 条评论
为什么被折叠?



