序列化和反序列化

你可记得你在使用阿里的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 序列化的默认规则与生命周期

writeObjectreadObject 方法,在日常的业务代码中,几乎从来都没有见过的,因为默认就够用了,不需要专门去覆盖这两个方法的。它们是Java序列化机制中的“钩子”方法。

下面我用微服务A去调用微服务B的 findUser(UserDTO userDto) 接口作为例子来说明整个过程。

微服务A:发请求之前,序列化发生

  1. 你在微服务A里写业务代码 userService.findUser(userDto);
  2. A 端的 RPC 代理(动态代理/Stub)拦截调用,把它包装成一个请求对象;
  3. RPC 框架准备把请求发到网络上,触发序列化:创建 ObjectOutputStream 并调用 ObjectOutputStream.writeObject(request)

userDto 被发送到网络之前,会触发 writeObject


微服务B:收到请求之后 ——反序列化发生

  1. B 端网络线程收到字节流(byte[])
  2. 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,基本类型为 0false)。

  • 常见用途

    • 敏感信息:如密码、密钥等,不希望被持久化或网络传输。
    • 临时数据:只在当前对象生命周期内有效,不需要跨进程或持久化。
    • 不可序列化对象:如果一个字段本身是不可序列化的(例如 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 自定义序列化逻辑:writeObjectreadObject

虽然默认序列化通常够用,但在某些高级场景下,你可能需要自定义序列化和反序列化的过程。

  • 何时需要自定义

    • 加密/解密:在序列化前对敏感数据进行加密,反序列化后解密。
    • 处理不可序列化字段:如果类中包含不可序列化的字段,但你又希望在序列化时保存其“状态”,可以在 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 局限性

  1. 性能问题:Java 序列化通常比其他序列化框架慢,因为它涉及大量的反射操作和复杂的对象图遍历。
  2. 字节流体积大:生成的字节流通常包含大量的元数据(如类名、字段名等),导致数据包较大,占用更多网络带宽和存储空间。
  3. 跨语言兼容性差:Java 序列化是 Java 特有的,无法直接与其他语言(如 Python, Go, C++)进行数据交换。
  4. 安全风险:这是最严重的问题之一,Java 反序列化漏洞是常见的攻击向量,恶意构造的字节流可能导致远程代码执行 (RCE)。

4.2 常见替代方案

鉴于上述局限性,在现代分布式系统中,尤其是 RPC 框架如 Dubbo 中,通常会采用更高效、更安全的序列化协议。

  1. Kryo

    • 特点:高性能、高效率的 Java 序列化库。
    • 优势:速度快,生成的字节流小,支持循环引用。
    • 适用场景:主要用于 Java 内部系统,如缓存、RPC 数据传输。
  2. Protobuf (Protocol Buffers)

    • 特点:Google 开发的语言无关、平台无关、可扩展的序列化数据结构。
    • 优势:非常高效,字节流极小,支持多种语言,通过 .proto 文件定义数据结构,具有良好的向前和向后兼容性。
    • 适用场景:跨语言 RPC 通信、数据存储。
  3. Hessian

    • 特点:轻量级的 RPC 协议,支持多种语言。
    • 优势:基于 HTTP 协议,易于穿越防火墙,性能较好,字节流相对紧凑。
    • 适用场景:Web Service、跨语言 RPC。
  4. 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 等替代方案。
  • 安全至上:警惕反序列化漏洞,不要反序列化不可信数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值