Skip to content
文章大纲

1.Java 序列化介绍

1.1 Java 序列化的不足

所谓序列化(Serialization)是将对象转换为字节流的过程,以便将其存储到磁盘、在网络上传输、持久化存储或跨平台通信。反序列化(Deserialization)则是将字节流转换回对象的过程,以便恢复对象的状态和数据。

Java 在 JDK 1.1 提供了序列化支持,序列化对象只需要实现 java.io.Serializable 接口,该接口没有任何方法,仅仅是一个标记接口。当一个类实现了 Serializable 接口时,它的对象就可以通过 Java 序列化机制进行序列化和反序列化。Java 序列化的过程是通过将对象的状态写入字节流中来完成的。可以使用 ObjectOutputStream 将对象写入输出流,该流可以是文件流、网络流或内存流。反序列化的过程是通过从字节流中读取对象的状态来恢复对象。可以使用 ObjectInputStream 从输入流中读取字节并重构原始对象。尽管 Java 序列化提供了一种方便的方式来序列化和反序列化对象,但存在着如下不足:

  • 无法跨语言。由于 Java 反序列技术是 Java 语言内部的私有协议,其它语言并不支持,对于 Java 序列化后的字节数组,其它语言也无法进行反序列化。在进程服务调用时,服务提供者可能基于其它语言开发,使用 Java 序列化无法实现异构语言的调用。

  • 序列化后的体积大,不适合存储。在 Java 序列化过程中,生成的序列化数据可能会比较大,导致序列化对象的体积增加。这主要是由于 Java 序列化机制的一些特性引起的,包括以下几个方面:

    • 对象头信息:Java 序列化会将对象的类信息、字段信息等一并序列化,这些附加的元数据会占用额外的空间。尤其是在有继承关系的类结构中,继承的父类信息也会被序列化。
    • 内部引用:如果序列化对象中包含其他对象的引用,那么这些引用也会被序列化并包含在序列化数据中。这可能会导致冗余的数据复制,增加了序列化数据的体积。
    • 字段值的存储:Java 序列化会将对象的字段值按照特定的格式进行存储,这可能会占用更多的空间。例如,基本类型可能会被存储为它们的完整表示形式,而不是紧凑的二进制形式。
    • 对象图的复制:如果序列化的对象中包含对象图(即对象之间相互引用),那么在序列化过程中,整个对象图会被复制,这会导致序列化数据的体积增加。
  • 序列化性能差,读写 IO 开销大。Java 序列化过程涉及将对象的状态转换为字节流,并将其写入到输出流中。这涉及到对象图的遍历和递归操作,可能会引起性能开销,并且在大型对象图的情况下,序列化和反序列化的速度可能较慢。

  • 升级 JDK 版本或修改序列化对象可能会导致反序列化失败。Java 的序列化机制与具体的 JDK 版本紧密绑定,不同版本间的序列化兼容性较差。升级 JDK 版本后,之前序列化的数据可能无法反序列化。如果对序列化类定义做了修改,比如增加/删除字段,修改类名等,那么和原类定义不匹配,反序列化会失败。serialVersionUID 用于标识序列化版本,如果版本不一致,反序列化同样会失败。

1.2 主流编解码工具

由于 Java 序列化的不足,在实际开发中通常会使用支持跨语言、高性能等特点的第三方编解码库,主流的编解码库如下:

  • ProtoBuf:ProtoBuf,全称为 Protocol Buffers(协议缓冲区),是由 Google 开发的一种高效的序列化框架。它可以用于结构化数据的序列化和反序列化,旨在实现跨平台、跨语言的数据交换。ProtoBuf 使用一个类似于 IDL(Interface Definition Language)的语言来定义数据结构和消息格式。通过定义消息的结构和字段类型,ProtoBuf 编译器会生成针对不同编程语言的消息类文件,这些类文件用于序列化和反序列化消息。ProtoBuf 的特点如下:

    • 高效的数据编码。ProtoBuf 使用二进制格式进行数据编码,相比于文本格式,它的编码效率更高,生成的序列化数据体积更小。
    • 跨平台和跨语言支持。ProtoBuf 支持多种编程语言,包括 Java、C++、Python、Go 等,这使得不同平台和语言之间的数据交换变得更加容易。
    • 灵活的数据模型。ProtoBuf 的数据模型可以轻松地进行扩展和演化,可以在不破坏向后兼容性的前提下添加、删除或修改字段。
    • 代码生成。ProtoBuf 编译器根据定义的消息结构生成特定编程语言的消息类文件,这些类文件提供了方便的 API 来序列化和反序列化消息,简化了开发过程。
    • 提供压缩支持。ProtoBuf 支持对序列化数据进行压缩,可以进一步减小数据体积。
  • Thrift:Thrift 是由 Apache 开发的一种高效的跨语言的 RPC(远程过程调用)框架。它提供了一种简单的定义文件语言来描述数据类型和服务接口,并生成用于不同编程语言的类型和服务代码。Thrift 的特点如下:

    • 跨语言支持。Thrift 支持多种编程语言,包括 Java、C++、Python、Go 等,这使得不同语言之间的通信变得更加容易。每种语言都有相应的 Thrift 编译器生成的类型和服务代码。
    • 高性能。Thrift 使用紧凑的二进制协议来序列化和反序列化数据,提供了高性能的数据传输和处理能力。它使用了自定义的二进制编码格式,相比于文本格式的序列化框架,Thrift 在网络传输和数据解析方面更高效。
    • 动态和静态类型支持。Thrift 支持动态和静态类型语言,可以使用静态类型语言(如 Java、C++)来实现高性能的服务,同时使用动态类型语言(如 Python)来进行脚本开发和快速原型验证。
    • 多协议支持。Thrift 支持多种协议,包括二进制协议(TBinaryProtocol)、压缩协议(TCompactProtocol)、JSON 协议(TJSONProtocol)等。这使得 Thrift 可以根据具体需求选择合适的协议进行数据传输。
    • 可扩展性和版本管理。Thrift 提供了丰富的数据类型定义和版本管理机制,使得数据模型可以轻松扩展和演化。当数据结构发生变化时,可以通过向后兼容的方式进行升级和版本管理。
    • 支持复杂数据结构。Thrift 支持复杂的数据结构,包括嵌套的结构体、集合类型(如数组、列表、映射)等。这使得 Thrift 适用于处理复杂的数据模型和领域对象。
  • MessagePack: MessagePack 是一种高效、紧凑和可扩展的二进制序列化框架,类似于 JSON。用于将结构化数据转换为紧凑的字节序列,以实现跨平台和跨语言的数据交换。MessagePack 的特点如下:

    • 序列化数据体积小。MessagePack 以二进制形式表示数据,并具有较小的序列化数据体积和高性能的序列化和反序列化操作。MessagePack 适用于分布式系统、网络通信、高性能计算、缓存存储等。它可以用于传输大量结构化数据,如配置信息、日志记录、传感器数据、消息等
    • 数据表示丰富。MessagePack 支持多种数据类型,包括整数、浮点数、字符串、布尔值、数组、映射等。它使用了一种紧凑的编码格式,可以有效地压缩数据,减小数据体积。
    • 跨语言和跨平台支持。MessagePack 支持多种编程语言,包括 Java、C++、Python、Go、C#等,这使得不同语言之间的数据交换变得更加容易。每种语言都有相应的 MessagePack 库或插件。
    • 性能好。由于使用了紧凑的二进制编码格式,MessagePack 具有高效的序列化和反序列化性能。相比于文本格式的序列化框架,如 JSON 或 XML,MessagePack 通常能够提供更快的序列化和反序列化速度,同时减少网络传输和存储的开销。
    • 良好的可读性和可扩展性。尽管 MessagePack 是以二进制形式表示数据,但它仍然具有较好的可读性,便于调试和解析。此外,MessagePack 提供了一些扩展机制,如扩展类型和用户定义类型,以支持更复杂的数据结构和自定义类型。

对于追求高性能和紧凑的数据表示,且需要跨语言和跨平台支持,可以选择 ProtoBuf 或 MessagePack。对于需要构建跨语言的服务和通信框架,并提供全功能的 RPC 支持,可以选择 Thrift。对于性能要求不是特别高,但仍需要跨语言的数据交换,可以选择 MessagePack。

2.Netty 集成 ProtoBuf

2.1 PortoBuf 环境搭建

  • 下载 ProtoBuf 编译器。ProtoBuf 编译器(protoc)是 Protocol Buffers 的官方编译器工具,用于将定义在 .proto 文件中的消息结构和相关信息编译成针对特定编程语言的代码。下载对应的平台安装包进行解压,在 Windows 系统下需要将解压后/bin/protoc.exe 拷贝至 C:\Windows\System32 目录下,输入protoc --version打印 protoc 版本则说明安装成功。
  • 创建 ProtoBuf 文件并定义 ProtoBuf 消息结构。ProtoBuf 提供了协议缓冲区语言来构建 协议缓冲区数据,其方式与其它编程语言类似,包括文件语法以及如何从文件访问类生成数据。以创建 book.proto 为例:
proto
// proto句法版本,proto支持proto2和proto3
syntax = "proto3";

/**
 * 定义一个名为Book的消息,Book中定义了
 */
message Book {
  /**
   * 声明字段,string表示字段类型,对应Java的String类型,title表示字段名,
   * 1表示字段编号,用于在序列化和反序列化时识别字段,编号必须是一个正整数,
   * 并且在消息定义内必须是唯一的。
   */
  string title = 1;
  int32 page_number = 2;
  int32 total_page = 3;
}
  • 使用 ProtoBuf 编译器编译 ProtoBuf 文件。通过如下命令使用 protoc 编译器将 .proto 文件编译为相应编程语言的代码:
shell
# $SRC_DIR 表示proto文件所在的源目录,$DST_DIR表示编译为相应编程语言代码的输出目录
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

# 例如
protoc -I=D:\package\java-repo\netty-example\netty-hello\proto
--java_out=D:\package\java-repo\netty-example\netty-hello\proto D:\package\java-repo\netty-example\netty-hello\proto\book.proto
  • 添加 ProtoBuf 库依赖。
  • 使用 ProtoBuf 进行序列化和反序列化。

Released under the MIT License.