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

用 Kotlin 玩转 Protocol Buffers(proto3)

1. 为什么选择 Protocol Buffers

为“结构化数据序列化/反序列化”这一老问题,常见方案有:

  • kotlinx.serialization
    与 C++/Python 等跨语言互通不理想。它有 protobuf 模式,但不具备 protobuf 官方的全部特性
  • 临时自定义编码
    例如把 4 个 int 编成 "12:3:-23:67"。实现简单但需要“手写”解析,易错、难维护,只适合极简数据。
  • XML
    人类可读、生态广,但体积膨胀编解码开销大,DOM 操作也复杂。

Protocol Buffers(protobuf) 提供了一个高效、紧凑、可演进的二进制格式:
你只需定义 .proto,编译器就会生成类(这里是 Java 与 Kotlin),自动完成编码/解析,并为字段生成访问器。更重要的是,格式天然支持演进,新旧版本能平滑兼容。

2. 示例与目标

我们实现一组命令行工具来管理 地址簿 文件(protobuf 编码):

  • add_person_kotlin:向文件添加一个联系人;
  • list_people_kotlin:读取并打印所有联系人。

完整示例在官方仓库 examples 目录可找到(本文给出可运行版本)。

3. 定义协议:addressbook.proto(proto3)

3.1 文件内容

syntax = "proto3";
package tutorial;import "google/protobuf/timestamp.proto";message Person {string name = 1;int32 id   = 2;  // Unique ID number for this person.string email = 3;enum PhoneType {PHONE_TYPE_UNSPECIFIED = 0;PHONE_TYPE_MOBILE = 1;PHONE_TYPE_HOME   = 2;PHONE_TYPE_WORK   = 3;}message PhoneNumber {string number = 1;PhoneType type = 2;}repeated PhoneNumber phones = 4;google.protobuf.Timestamp last_updated = 5;
}message AddressBook {repeated Person people = 1;
}

3.2 关键点速记

  • 包名package tutorial; 用于避免跨项目命名冲突。
  • 消息与字段message 是字段的聚合;可以嵌套 messageenum
  • 标签号(tag)= 1, = 2 ... 是字段的二进制标识。1–15 更省空间,优先给常用或 repeated 字段。
  • 默认值:未设置时,数值=0,字符串=“”,布尔=false;嵌套消息默认是“空原型实例”。
  • repeated 语义:可出现任意次(含 0),顺序保留,可视作动态数组。
  • 继承:protobuf 不支持类继承。

4. 生成代码:必须同时 --java_out--kotlin_out

4.1 安装 protoc

按官方 README 安装对应平台的 Protocol Buffer Compiler

4.2 一条命令生成 Java + Kotlin

protoc -I=$SRC_DIR \--java_out=$DST_DIR \--kotlin_out=$DST_DIR \$SRC_DIR/addressbook.proto
  • 这会在你指定的目录下生成:

    • com/example/tutorial/protos/(若按 java_package 配置)里的 .java.kt 文件;
  • Kotlin 代码是在 Java 生成功能之上提供额外 Kotlin API(两者并存,方便 Java/Kotlin 混用)。

当前不支持将 protobuf 直接生成 Kotlin/JS 或 Kotlin/Native 目标。

5. Kotlin 侧 API 你会拿到什么?

编译 addressbook.proto 后,你会获得:

  • Java API(Kotlin 也可直接调用)

    • AddressBook(Kotlin 视角:peopleList: List<Person>
    • Person(Kotlin 视角:name, id, email, phonesList
    • 嵌套类 Person.PhoneNumbernumber, type
    • 嵌套枚举 Person.PhoneType
  • Kotlin 扩展 API(DSL 工厂方法)

    • addressBook { ... }person { ... }
    • PersonKt.phoneNumber { ... }

这意味着你既可以像写 Java 一样操作对象,也能用 Kotlin DSL 写出更简洁的构造代码。

6. 写入示例:add_person_kotlin.kt(可运行)

说明:proto3 对标量字段默认不生成 hasXxx() 存在性判断(除非声明为 optional)。所以下文打印逻辑以是否为空字符串等方式判断是否展示。

@file:JvmName("add_person_kotlin")package cliimport com.example.tutorial.protos.AddressBook
import com.example.tutorial.protos.Person
import com.example.tutorial.protos.addressBook
import com.example.tutorial.protos.person
import com.example.tutorial.protos.PersonKt.phoneNumber
import com.google.protobuf.Timestamp
import java.io.File
import java.time.Instant// 交互式读取并构造一个 Person
private fun promptPerson(): Person = person {print("Enter person ID: ")id = readln().trim().toInt()print("Enter name: ")name = readln().trim()print("Enter email address (blank for none): ")val emailInput = readln().trim()if (emailInput.isNotEmpty()) email = emailInputwhile (true) {print("Enter a phone number (or leave blank to finish): ")val number = readln().trim()if (number.isEmpty()) breakprint("Is this a mobile, home, or work phone? ")val type = when (readln().trim().lowercase()) {"mobile" -> Person.PhoneType.PHONE_TYPE_MOBILE"home"   -> Person.PhoneType.PHONE_TYPE_HOME"work"   -> Person.PhoneType.PHONE_TYPE_WORKelse -> {println("Unknown phone type. Using HOME.")Person.PhoneType.PHONE_TYPE_HOME}}phones += phoneNumber {this.number = numberthis.type = type}}// 设置最后更新时间(避免额外依赖 util 包,直接用 Timestamp.Builder)val now = Instant.now()lastUpdated = Timestamp.newBuilder().setSeconds(now.epochSecond).setNanos(now.nano).build()
}// 读取文件 → 添加一个 Person → 写回文件
fun main(args: Array<String>) {if (args.size != 1) {println("Usage: add_person_kotlin ADDRESS_BOOK_FILE")return}val file = File(args[0])val initial = if (file.exists()) {file.inputStream().use { AddressBook.newBuilder().mergeFrom(it).build() }} else {println("${file.path}: not found, creating a new file.")addressBook { }}val updated = initial.toBuilder().apply {addPeople(promptPerson())}.build()file.outputStream().use { updated.writeTo(it) }println("Saved to ${file.path}")
}

7. 读取示例:list_people_kotlin.kt(可运行)

@file:JvmName("list_people_kotlin")package cliimport com.example.tutorial.protos.AddressBook
import com.example.tutorial.protos.Person
import java.io.Fileprivate fun printBook(book: AddressBook) {for (p in book.peopleList) {println("Person ID: ${p.id}")println("  Name: ${p.name}")if (p.email.isNotEmpty()) {println("  Email: ${p.email}")}for (ph in p.phonesList) {val label = when (ph.type) {Person.PhoneType.PHONE_TYPE_MOBILE -> "Mobile"Person.PhoneType.PHONE_TYPE_HOME   -> "Home"Person.PhoneType.PHONE_TYPE_WORK   -> "Work"else -> "Unknown"}println("  $label phone #: ${ph.number}")}if (p.hasLastUpdated()) {println("  Updated: ${p.lastUpdated.seconds}.${p.lastUpdated.nanos}")}}
}fun main(args: Array<String>) {if (args.size != 1) {println("Usage: list_people_kotlin ADDRESS_BOOK_FILE")return}val file = File(args[0])if (!file.exists()) {println("File not found: ${file.path}")return}val book = file.inputStream().use {AddressBook.newBuilder().mergeFrom(it).build()}printBook(book)
}

说明:示例中 hasLastUpdated() 可用,因为它是消息类型,proto3 对消息字段保留 presence。

8. 协议演进(兼容性)硬规则

当你需要升级 .proto 时,为了做到新版本向后兼容/旧版本向前兼容,请遵守:

  1. 不要修改任何既有字段的 tag
  2. 可以删除字段;
  3. 可以新增字段,但必须使用从未使用过的 tag(包括被删字段曾用过的编号也不可重用)。

遵循以上:

  • 旧代码能读新消息并忽略不认识的新字段
  • 新代码能透明读旧消息;
  • 新增字段在旧消息中不存在,读取时会得到类型默认值(字符串空串、布尔 false、数值 0),需要在业务层作合理处理。

9. 常见坑与排查

  • 忘记 --java_out:生成 Kotlin 代码时必须同时--java_out--kotlin_out,因为 Kotlin API 是在 Java 生成物之上补充的。
  • proto3 的 presence:标量字段默认没有 hasXxx(),除非写成 optional。打印时用“是否空值”判断更稳妥。
  • 跨语言互通:Kotlin 与 Java 可直接共享同一份消息对象(无需额外转换),这也是官方 Kotlin 生成器的设计初衷。
  • Tag 规划:1–15 更省空间,尽量留给高频或 repeated。新增字段永远不要复用历史 tag。
  • Kotlin/JS & Native:当前不支持直接生成这些目标

10. 小结:落地清单(Checklist)

  • 写好 addressbook.proto(proto3 + 合理 tag);
  • protoc 同时生成 Java + Kotlin
  • 熟悉 Kotlin 侧 DSL 工厂addressBook {}person {}phoneNumber {}
  • 完成读写 CLI:add_person_kotlin / list_people_kotlin
  • 严守演进规则:不改 tag、可删、可加新字段但用全新 tag

至此,你已经掌握了从 .proto → 生成代码 → Kotlin 读写 → 版本演进 的完整闭环。把本文示例直接丢进你的工程,就能“跑起来、用起来”。祝编码愉快!


文章转载自:

http://dshgJMek.mdpLm.cn
http://iHADb75K.mdpLm.cn
http://0gYQp5Xl.mdpLm.cn
http://vKsBjCYf.mdpLm.cn
http://WuJY8Crn.mdpLm.cn
http://zjggGFBy.mdpLm.cn
http://OrHVBeKt.mdpLm.cn
http://Rul6oYCQ.mdpLm.cn
http://RoDZTsRV.mdpLm.cn
http://dIBIWKc0.mdpLm.cn
http://Te3nC9Bj.mdpLm.cn
http://zOmaVnaH.mdpLm.cn
http://n7sT63Ae.mdpLm.cn
http://Yt09bttp.mdpLm.cn
http://IMMXSI65.mdpLm.cn
http://sGEWvH33.mdpLm.cn
http://9y7aZHVP.mdpLm.cn
http://S2GHdFFA.mdpLm.cn
http://1v3KWDJk.mdpLm.cn
http://Wf2jEIE3.mdpLm.cn
http://csbp6gXO.mdpLm.cn
http://zw2Y338t.mdpLm.cn
http://djXnHyaz.mdpLm.cn
http://RXQLN7Gi.mdpLm.cn
http://3sL61BmB.mdpLm.cn
http://l8Hjwbag.mdpLm.cn
http://VLoDOclQ.mdpLm.cn
http://urVF0t2b.mdpLm.cn
http://RI40v4CJ.mdpLm.cn
http://SjoIIpSl.mdpLm.cn
http://www.dtcms.com/a/385570.html

相关文章:

  • leecode73 矩阵置零
  • SELECT INTO 和 INSERT INTO SELECT 区别
  • dhtmlx-gantt
  • Spring如何巧妙解决循环依赖问题
  • 第四章:职业初印象:打造你的个人品牌(1)
  • (九)Python高级应用-文件与IO操作
  • FFmpeg06:SDL渲染
  • javadoc命令 错误: 编码 GBK 的不可映射字符 (0x80)
  • 【面试场景题】自增主键、UUID、雪花算法都有什么问题
  • 数据整理器(Data Collators)总结 (95)
  • 代码评价:std::shared_ptr用法分析
  • 23种设计模式案例
  • AI Agent案例与实践全解析:字节智能运维
  • MyBatis-Plus分页插件实现导致total为0问题
  • S32DS仿真环境问题
  • 黑马JavaWeb+AI笔记 Day07 Web后端实战(部门管理模块)
  • 【AI开发】【前后端全栈】[特殊字符] AI 时代的快速开发思维
  • kimi-k2论文阅读笔记
  • [SC]一个使用前向声明的SystemC项目例子
  • Gunicorn 部署与调优全指南(2025 版)
  • 第二十一篇|新宿平和日本语学校的结构化解读:费用函数、文化网络与AI教育建模
  • 数据结构(C语言篇):(十五)二叉树OJ题
  • RIFE.py代码学习 自学
  • Gateway-路由-规则配置
  • 低端影视官网入口 - 免费看影视资源网站|网页版|电脑版地址
  • 【Python3教程】Python3高级篇之日期与时间
  • 计算机网络——传输层(25王道最新版)
  • 5-14 forEach-数组简易循环(实例:数组的汇总)
  • 【智能体】rStar2-Agent
  • ego(5)---Astar绕障