当前位置: 首页 > news >正文

用 Java 学会 Protocol Buffers从 0 到 1 的完整实战

1. 什么是 Protocol Buffers(protobuf)

Protocol Buffers 是一种高效、可演进的结构化数据序列化机制。你先用 .proto 描述数据结构,编译器会为你生成对应语言(这里是 Java)的类,自动完成高效二进制的编码与解析,并提供字段的 getter/setter、整体读写等能力。

2. 为什么不用 Java 序列化 / 手写编码 / XML?

  1. Java 序列化
    内置方便,但与跨语言(C++/Python 等)互通较差,且存在众所周知的问题(参见 Effective Java)。

  2. 手写“简易编码”
    例如把 int 拼成 "12:3:-23:67",虽灵活但需要专门的编码/解析代码,易错且维护成本高。

  3. XML
    可读性尚可且语言绑定多,但空间占用大、编解码开销高,DOM 访问也更复杂。

protobuf 的优势:二进制高效 + 自动生成读写代码 + 良好的前后兼容演进

3. 示例工程与学习目标

  • 示例是一个“地址簿 AddressBook”应用:读写文件中的联系人(姓名、ID、邮箱、电话)。

  • 学会:

    • .proto 中定义消息;
    • 使用 protoc 生成 Java 类;
    • 用 Java API 读写消息对象。

4. 定义协议:从 addressbook.proto 开始

4.1 完整示例

syntax = "proto2";package tutorial;option java_multiple_files = true;
option java_package = "com.example.tutorial.protos";
option java_outer_classname = "AddressBookProtos";message Person {optional string name = 1;optional int32 id = 2;optional string email = 3;enum PhoneType {PHONE_TYPE_UNSPECIFIED = 0;PHONE_TYPE_MOBILE = 1;PHONE_TYPE_HOME = 2;PHONE_TYPE_WORK = 3;}message PhoneNumber {optional string number = 1;optional PhoneType type = 2 [default = PHONE_TYPE_HOME];}repeated PhoneNumber phones = 4;
}message AddressBook {repeated Person people = 1;
}

4.2 package 与 Java 专用选项

  • package tutorial;:避免跨项目命名冲突。
  • option java_package:指定生成 Java 类的包名(建议使用域名式前缀)。
  • option java_outer_classname:指定外层包装类名(未指定时由文件名驼峰化而来)。
  • option java_multiple_files = true:为每个生成类型单独产出 .java 文件(推荐)。

4.3 消息、字段与类型

  • message 就是一组带类型的字段;简单类型有 bool/int32/float/double/string 等;
  • 字段可嵌套其他消息或枚举(如 Person.PhoneNumberPerson.PhoneType);
  • 字段标签号(tag)= 1, = 2 ... 在二进制编码中唯一标识字段。1–15 更省空间,优先分配给高频或 repeated 字段。

4.4 字段修饰符与默认值

  • optional:可不设置;未设置时使用默认值(可自定义或系统默认:数值 0、字符串空、布尔 false;消息为默认实例)。
  • repeated:可出现 0~N 次,顺序保留,类似动态数组
  • required必须设置,否则构建或解析会抛异常。

⚠️ 强烈建议避免 required(proto3 已取消)
一旦使用 required,将来把它改回 optional 会破坏兼容性:旧读者会把缺字段的消息视为“不完整”。

5. 用 protoc 生成 Java 代码

  1. 安装编译器 protoc(按 README 指南)。
  2. 运行命令(例):
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto

生成的 Java 文件位于 $DST_DIR/com/example/tutorial/protos/

6. 认识生成代码与 Java API

6.1 访问器(getter/setter/has/clear)

  • 消息类:只包含 getter;
  • Builder:包含 getter + setter + clearXxx()
  • 每个标量字段都有 hasXxx() 判断是否已设置;
  • repeated 字段额外提供:getCount()(实际是列表大小)、按索引 get/set、add()addAll() 等。

命名风格.proto 字段名使用下划线小写,生成的 Java 方法会自动转为驼峰,保证符合 Java 习惯(其它语言亦然)。

6.2 枚举与嵌套类

  • Person.PhoneType 生成为 Java enum(嵌套在 Person 内);
  • Person.PhoneNumber 生成为 Person 的嵌套类。

6.3 Builder vs. Message(可变 vs. 不可变)

  • 消息类不可变;需要通过 Builder 设置字段后 build() 得到最终对象;
  • Builder 的 setter 返回自身,支持链式调用

6.4 常用通用方法

  • isInitialized():检查所有 required 是否就绪;
  • toString():友好字符串(便于调试);
  • mergeFrom(Message other)(Builder):合并另一个消息(覆盖标量、合并复合、拼接 repeated);
  • clear()(Builder):清空到初始状态。

6.5 解析与序列化

  • toByteArray():序列化为 byte[]
  • parseFrom(byte[]) / parseFrom(InputStream):从字节数组/输入流解析;
  • writeTo(OutputStream):写入输出流。
    (更多变体详见 Message API。)

7. 写入示例:交互式添加联系人并保存

import com.example.tutorial.protos.AddressBook;
import com.example.tutorial.protos.Person;
import java.io.*;class AddPerson {static Person PromptForAddress(BufferedReader stdin, PrintStream stdout) throws IOException {Person.Builder person = Person.newBuilder();stdout.print("Enter person ID: ");person.setId(Integer.valueOf(stdin.readLine()));stdout.print("Enter name: ");person.setName(stdin.readLine());stdout.print("Enter email address (blank for none): ");String email = stdin.readLine();if (email.length() > 0) {person.setEmail(email);}while (true) {stdout.print("Enter a phone number (or leave blank to finish): ");String number = stdin.readLine();if (number.length() == 0) break;Person.PhoneNumber.Builder phoneNumber =Person.PhoneNumber.newBuilder().setNumber(number);stdout.print("Is this a mobile, home, or work phone? ");String type = stdin.readLine();if (type.equals("mobile")) {phoneNumber.setType(Person.PhoneType.PHONE_TYPE_MOBILE);} else if (type.equals("home")) {phoneNumber.setType(Person.PhoneType.PHONE_TYPE_HOME);} else if (type.equals("work")) {phoneNumber.setType(Person.PhoneType.PHONE_TYPE_WORK);} else {stdout.println("Unknown phone type.  Using default.");}person.addPhones(phoneNumber);}return person.build();}public static void main(String[] args) throws Exception {if (args.length != 1) {System.err.println("Usage:  AddPerson ADDRESS_BOOK_FILE");System.exit(-1);}AddressBook.Builder addressBook = AddressBook.newBuilder();try {addressBook.mergeFrom(new FileInputStream(args[0]));} catch (FileNotFoundException e) {System.out.println(args[0] + ": File not found.  Creating a new file.");}addressBook.addPerson(PromptForAddress(new BufferedReader(new InputStreamReader(System.in)), System.out));FileOutputStream output = new FileOutputStream(args[0]);addressBook.build().writeTo(output);output.close();}
}

8. 读取示例:打印地址簿中所有联系人

import com.example.tutorial.protos.AddressBook;
import com.example.tutorial.protos.Person;
import java.io.FileInputStream;class ListPeople {static void Print(AddressBook addressBook) {for (Person person: addressBook.getPeopleList()) {System.out.println("Person ID: " + person.getId());System.out.println("  Name: " + person.getName());if (person.hasEmail()) {System.out.println("  E-mail address: " + person.getEmail());}for (Person.PhoneNumber phoneNumber : person.getPhonesList()) {switch (phoneNumber.getType()) {case PHONE_TYPE_MOBILE: System.out.print("  Mobile phone #: "); break;case PHONE_TYPE_HOME:   System.out.print("  Home phone #: ");   break;case PHONE_TYPE_WORK:   System.out.print("  Work phone #: ");   break;}System.out.println(phoneNumber.getNumber());}}}public static void main(String[] args) throws Exception {if (args.length != 1) {System.err.println("Usage:  ListPeople ADDRESS_BOOK_FILE");System.exit(-1);}AddressBook addressBook = AddressBook.parseFrom(new FileInputStream(args[0]));Print(addressBook);}
}

9. 架构与设计建议:面向对象如何与 protobuf 协作

  • 生成的 protobuf 类本质上是数据载体(像 C 的 struct),不适合作为有复杂行为的“领域对象”。
  • 若需要更丰富的行为,请用应用层类进行封装(wrapper),在其中持有/代理 protobuf 对象,暴露更贴合业务的接口。
  • 不要继承生成类添加行为,这会破坏内部机制,也违背良好的 OOP 实践。

10. 协议演进(前后兼容)黄金法则

当你要“升级”消息定义以适配新需求时:

  • 不要修改任何既有字段的 tag
  • 不要添加或删除任何 required 字段;
  • 可以删除 optional / repeated 字段;
  • 可以新增 optional / repeated 字段,但必须使用从未用过的 tag(包括删掉的旧字段占用过的 tag 也不能再用)。

遵循这些规则:

  • 旧代码读取新消息时,会忽略不认识的新字段;
  • 新代码也能透明读取旧消息;
  • 新增 optional 在旧消息中不存在:要么 hasXxx() 检查,要么在 .proto 里用 [default = ...] 指定默认值;
  • 新增 repeated 在新代码中无法区分“被留空”还是“从未设置”,因为没有 has_ 标志。

11. 高级用法:反射与跨格式转换

  • Message / Message.Builder 接口提供反射 API,可不绑定具体类型地遍历与操作字段;
  • 常见用途:与 JSON/XML 之间的互转、差异对比、按“模式”匹配消息等;
  • 通过反射,protobuf 可被应用到比“序列化”更广泛的场景中。

12. 实战小结与最佳实践清单

  • 风格.proto 字段统一用下划线小写,生成代码自动遵循各语言风格(Java 驼峰)。
  • 标签号分配:1–15 留给高频/重复字段,节省空间。
  • 少用/慎用 required:优先用 optional + 应用层校验。
  • 面向对象封装而非继承生成类型。
  • 演进:遵守兼容性规则,提前预留 tag 空间。
  • 构建流程.protoprotoc --java_out → Java Builder 构造对象 → writeTo/parseFrom 持久化与传输。

13. FAQ(基于文中要点)

  • Q:为什么我的字段 getter 是驼峰?
    A:编译器会把 .proto 的下划线命名转换为 Java 的驼峰命名,符合语言风格。

  • Q:如何把消息写入文件并再读出来?
    A:message.writeTo(OutputStream) 写入;Message.parseFrom(InputStream) 读取。

  • Q:如何保证老版本还能读新消息?
    A:别改已有 tag,不要动 required,新增字段用未使用过的 tag 即可。


文章转载自:

http://1axEKd7b.bqmsm.cn
http://dXolzwoi.bqmsm.cn
http://fI7Bs8wj.bqmsm.cn
http://VDo1mbxE.bqmsm.cn
http://BROkN11f.bqmsm.cn
http://EmNOy73D.bqmsm.cn
http://z9fPEjFd.bqmsm.cn
http://Ng16M4LD.bqmsm.cn
http://DgCxmvie.bqmsm.cn
http://lkQS9EI4.bqmsm.cn
http://cgeD3dBb.bqmsm.cn
http://q1sCGYu8.bqmsm.cn
http://3OMK0ZfI.bqmsm.cn
http://EyNbCyrE.bqmsm.cn
http://LninUMMQ.bqmsm.cn
http://ErGGbttF.bqmsm.cn
http://TBbAHZ71.bqmsm.cn
http://L9qwta1b.bqmsm.cn
http://5t7BpTjp.bqmsm.cn
http://GsJuE293.bqmsm.cn
http://Zcfmc991.bqmsm.cn
http://uBFFswiD.bqmsm.cn
http://LtGLwGmH.bqmsm.cn
http://Mf3Qp7Cm.bqmsm.cn
http://oETMPlzn.bqmsm.cn
http://HCdsBGSN.bqmsm.cn
http://n0ZvLuil.bqmsm.cn
http://PoPEz6ch.bqmsm.cn
http://86wlFbCW.bqmsm.cn
http://R1Ej94jr.bqmsm.cn
http://www.dtcms.com/a/383766.html

相关文章:

  • 237.删除链表中的节点
  • 【Vue2手录14】导航守卫
  • Qt如何读写xml文件,几种方式对比,读写xml的Demo工程
  • 子网划分专项训练-1,eNSP实验,vlan/dhcp,IP规划
  • 云原生改造实战:Spring Boot 应用的 Kubernetes 迁移全指南
  • 看门狗的驱动原理
  • [论文阅读] 人工智能 + 软件工程 | 大语言模型驱动的多来源漏洞影响库识别研究解析
  • 【前缀和+哈希表】P3131 [USACO16JAN] Subsequences Summing to Sevens S
  • 05.【Linux系统编程】进程(进程概念、进程状态(注意僵尸和孤儿)、进程优先级、进程切换和调度)
  • 【从零开始java学习|小结】记录学习和编程中的问题
  • 图像拼接案例,抠图案例
  • 分层解耦讲解
  • 安装Hadoop中遇到的一些问题和解决
  • 音视频-色域
  • 返利软件的分布式缓存架构:Redis集群在高并发场景下的优化策略
  • 如何让知识上传与查询更便捷
  • set/multiset容器
  • 区块链:搭建简单Fabric网络并调用智能合约
  • Keepalived的详细实操安装流程及其配置文件选项的详解
  • windows下,podman迁移镜像文件位置
  • 技能补全之正则表达式
  • Altium Designer(AD24)打开工程文件的几种方法
  • 26考研——内存管理(3)
  • 知识库缺乏维护和清理机制会造成哪些后果
  • android studio 华为 安装app 层层验证
  • 机器学习(三):决策树
  • 气缸夹爪机构分析
  • np.sum(e_x, axis=-1, keepdims=True)
  • kafka--基础知识点--5.3--producer事务
  • SCI论文组成部分