杰瑞科技汇

Java序列化为何选Protobuf而非原生序列化?

两者核心概念对比

什么是 Java 原生序列化?

Java 原生序列化是 Java 语言自带的机制,它允许将任何实现了 java.io.Serializable 接口的对象转换为字节流,以便可以存储到文件、数据库中,或者通过网络传输,反序列化则是将字节流恢复成原始对象。

Java序列化为何选Protobuf而非原生序列化?-图1
(图片来源网络,侵删)
  • 核心思想:将对象“原封不动”地转换成字节流,这个过程是“深度”的,会递归地序列化对象及其所有引用的对象。
  • 实现方式:实现 Serializable 接口,JVM 会自动处理序列化逻辑,也可以实现 Externalizable 接口来获得完全的控制权。

什么是 Protocol Buffers (Protobuf)?

Protobuf 是 Google 公司开发的一种语言无关、平台无关的序列化协议,它类似于 XML、JSON,但更小、更快、更简单,你首先需要定义 .proto 文件来描述你的数据结构,然后使用 Protobuf 编译器生成特定语言的类(如 Java 类)。

  • 核心思想:基于一个结构化的定义文件(.proto),生成高效的序列化和反序列化代码,它将数据序列化为二进制格式,而不是文本格式。
  • 实现方式
    1. 编写 .proto 文件。
    2. 使用 protoc 编译器生成目标语言(如 Java)的源代码。
    3. 在 Java 代码中使用生成的类来构建对象和序列化/反序列化。

核心差异对比 (一张图看懂)

特性 Java 原生序列化 Protocol Buffers (Protobuf)
性能 极快 (比 Java 序列化快 20-100 倍)
数据大小 (包含大量元数据) 极小 (二进制格式,非常紧凑)
可读性 (二进制格式,不可读) (二进制格式,不可读,但有文本格式 TextFormat)
跨语言/平台 (主要限于 JVM) 极佳 (支持 Java, Python, Go, C++, C# 等 10+ 种语言)
向前/向后兼容性 (修改类结构极易破坏兼容性) 优秀 (通过字段编号管理,增删字段非常安全)
使用门槛 (只需实现接口,无需额外工具) (需要定义 .proto 文件,使用编译器)
安全性 (可能执行恶意代码,存在安全漏洞) (反序列化过程是安全的,不会执行任意代码)
版本控制 困难 (依赖 serialVersionUID) 简单 (通过 .proto 文件版本管理)
灵活性 (可以序列化几乎任何对象) 较低 (只能序列化在 .proto 文件中定义的数据结构)

详细解释关键差异

a. 性能与数据大小

这是 Protobuf 最大的优势。

  • Java 序列化:在序列化时,除了数据本身,还会写入大量元数据,比如类的描述信息、字段名、类型签名等,这使得数据包非常臃肿。
  • Protobuf:序列化后是紧凑的二进制格式,它不存储字段名,而是存储每个字段的数字编号name= "Alice" 会被编码成类似 [1, 5, 'A', 'l', 'i', 'c', 'e'] 的形式,1name 字段的编号,5 是字符串的长度,这使得数据量非常小,序列化和反序列化的速度也极快。

b. 跨语言/平台

  • Java 序列化:是 JVM 内部的协议,一个在 Java 中序列化的对象,无法被 Python 或 Go 直接反序列化,除非你用 Java 写一个网关服务来转换数据。
  • Protobuf:天生就是为了跨语言设计的,你可以用 Java 定义数据结构,然后用 Python 序列化,再用 Go 反序列化,完全没有障碍。

c. 向后/向前兼容性

这是 Protobuf 在分布式系统中的“杀手锏”。

  • Java 序列化:非常脆弱,如果你在 Person 类中增加了一个新字段 age,那么旧的、没有 age 字段的序列化字节流在反序列化时就会抛出异常,虽然有 serialVersionUID 可以控制版本,但它只解决类的标识问题,无法解决字段变更的问题。

    Java序列化为何选Protobuf而非原生序列化?-图2
    (图片来源网络,侵删)
  • Protobuf:非常健壮,规则是:

    1. 不要重用已删除字段的编号
    2. 不要更改任何现有字段的编号
    3. 可以安全地添加新字段(旧代码会忽略新字段,新代码可以读取旧数据)。
    4. 可以安全地标记字段为 deprecated(旧代码会忽略它,新代码最终会停止使用它)。

    这个特性使得服务升级变得非常平滑,无需一次性更新所有服务。


如何在 Java 中使用 Protobuf (实战演练)

步骤 1: 定义 .proto 文件

创建一个文件 person.proto

// syntax = "proto3"; // 指定使用 proto3 语法
package com.example; // Java 包名
// 定义一个消息,类似于 Java 的类
message Person {
  int32 id = 1;      // 字段编号 1
  string name = 2;   // 字段编号 2
  string email = 3;  // 字段编号 3
  // 嵌套消息
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }
  repeated PhoneNumber phones = 4; // repeated 表示数组/列表
  // 枚举
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
}

步骤 2: 添加 Maven 依赖

在你的 pom.xml 中添加 Protobuf Maven 插件和运行时依赖。

Java序列化为何选Protobuf而非原生序列化?-图3
(图片来源网络,侵删)
<properties>
  <protobuf.version>3.25.1</protobuf.version>
  <maven.compiler.plugin.version>3.11.0</maven.compiler.plugin.version>
</properties>
<dependencies>
  <!-- Protobuf Java 运行时依赖 -->
  <dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>${protobuf.version}</version>
  </dependency>
</dependencies>
<build>
  <plugins>
    <!-- Protobuf Maven 插件 -->
    <plugin>
      <groupId>org.xolstice.maven.plugins</groupId>
      <artifactId>protobuf-maven-plugin</artifactId>
      <version>0.6.1</version>
      <configuration>
        <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <protoSourceRoot>${basedir}/src/main/proto</protoSourceRoot>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
    <!-- 为了编译生成的 Java 文件,需要添加此插件 -->
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>${maven.compiler.plugin.version}</version>
      <configuration>
        <source>8</source>
        <target>8</target>
      </configuration>
    </plugin>
  </plugins>
</build>

步骤 3: 编译 .proto 文件

在项目根目录下执行 Maven 命令:

mvn clean compile

执行后,插件会自动调用 protoc 编译器,并在 target/generated-sources/protobuf/java/com/example/ 目录下生成 Person.javaPersonOrBuilder.java 等文件,你需要将这些生成的源文件添加到项目的源码路径中(现代 IDE 如 IntelliJ IDEA 通常会自动识别)。

步骤 4: 在 Java 代码中使用生成的类

你可以像使用普通 Java 类一样使用 Person 了。

import com.example.Person;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class ProtobufDemo {
    public static void main(String[] args) {
        // 1. 创建一个 Person 对象
        Person person = Person.newBuilder()
                .setId(123)
                .setName("Alice")
                .setEmail("alice@example.com")
                .addPhones(
                    Person.PhoneNumber.newBuilder()
                        .setNumber("138-8888-8888")
                        .setType(Person.PhoneType.MOBILE)
                        .build()
                )
                .build();
        System.out.println("Created Person: " + person);
        System.out.println("Person size in bytes: " + person.getSerializedSize());
        // 2. 序列化到文件
        String filename = "person.data";
        try (FileOutputStream output = new FileOutputStream(filename)) {
            person.writeTo(output);
            System.out.println("Person has been serialized to " + filename);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 3. 从文件反序列化
        try (FileInputStream input = new FileInputStream(filename)) {
            Person parsedPerson = Person.parseFrom(input);
            System.out.println("\nParsed Person from file:");
            System.out.println("ID: " + parsedPerson.getId());
            System.out.println("Name: " + parsedPerson.getName());
            System.out.println("Email: " + parsedPerson.getEmail());
            System.out.println("Phone: " + parsedPerson.getPhones(0).getNumber() + " (" + parsedPerson.getPhones(0).getType() + ")");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

总结与选型建议

场景 推荐方案 理由
微服务间通信 Protobuf (或 gRPC/Thrift) 首选,性能高、数据小、跨语言、强兼容性,是分布式系统的标配。
RPC 框架 Protobuf (gRPC) gRPC 使用 Protobuf 作为接口定义语言和序列化格式,提供了高性能、强类型的 RPC 体验。
持久化存储 Protobuf 当存储空间和读写性能是关键时,Protobuf 比 JSON/XML 更优。
简单的配置文件 JSON/YAML 人类可读性是首要考虑因素,JSON/YAML 更直观。
简单的 JVM 内部对象传递 Java 序列化 代码简单,无需额外工具,但仅限 JVM 内部,不推荐用于网络或持久化。
遗留系统维护 Java 序列化 如果系统已经深度依赖 Java 序列化,重构成本可能很高,只能继续维护。

在现代 Java 开发中,除非是极特殊、简单的场景,否则都应该优先选择 Protocol Buffers 而不是 Java 原生序列化,Protobuf 在性能、兼容性和跨平台性方面的巨大优势,使其成为构建高性能、可扩展服务的基石,Java 原生序列化更多地存在于历史遗留代码中,或者是一些仅在 JVM 内部进行简单对象传递的场景。

分享:
扫描分享到社交APP
上一篇
下一篇