首页
/ Protocol Buffers Java 运行时实战指南:代码生成、Maven/Android Lite/Bazel 集成与 Kotlin 支持

Protocol Buffers Java 运行时实战指南:代码生成、Maven/Android Lite/Bazel 集成与 Kotlin 支持

2026-09-04 21:37:48作者:平淮齐Percy

在 Java/Kotlin 项目中使用 Protocol Buffers 时,你需要理解三件事:如何用 protoc 生成 Java 代码、如何引入对应版本的运行时库(Maven/Bazel),以及何时应选择 Lite 运行时来压缩移动端包体积。本文以 protobuf 仓库中 java/README.md 为主线,结合仓库内 java/pom.xmljava/lite.mdexamples/BUILD.bazel 等实际构建文件,完整讲解从代码生成到构建集成的全流程与版本兼容规则。

一、核心工作流:用 protoc 生成 Java 代码

使用 protobuf 处理 .proto 文件的第一步是获取协议编译器 protoc(安装方式见仓库顶层 README.md),然后用它为目标 .proto 文件生成 Java 代码:

$ protoc --java_out=${OUTPUT_DIR} path/to/your/proto/file

生成的 Java 文件包含进你的项目源码树后,还需要在构建系统中添加对 protobuf Java 运行时的依赖。这里有一条贯穿全文的版本约束原则:运行时库的版本号必须与 protoc 的版本号相同(或更新),否则会因代码生成器与运行时不匹配而出错。当前仓库开发版本可参考 version.json,其中 java 语言运行时标记为 4.37-dev;文档中给出的稳定版示例为 4.28.2

二、Maven 集成

2.1 核心运行时 protobuf-java

Maven 项目中添加核心运行时依赖:

<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-java</artifactId>
  <version><!--version--></version>
</dependency>

<!--version--> 替换为 Maven 仓库中 protobuf-java 的实际版本号(如 4.28.2),并确保它不旧于你使用的 protoc。

从仓库结构看,java/pom.xml 是 Maven 多模块工程的父 POM(protobuf-parent,当前版本 4.37.0),其 <modules> 声明了六个子模块:

<modules>
  <module>bom</module>
  <module>lite</module>
  <module>core</module>
  <module>util</module>
  <module>kotlin</module>
  <module>kotlin-lite</module>
</modules>

对应关系非常清晰:core 即完整 Java 运行时(发布为 protobuf-java),lite 是 Lite 运行时(发布为 protobuf-javalite),util 提供 JSON 等扩展能力(发布为 protobuf-java-util),kotlinkotlin-lite 分别是 Kotlin 支持及其 Lite 配对包,bom 则用于依赖版本统一管理。

2.2 扩展包 protobuf-java-util

如果需要 JsonFormat 这类 JSON 转换、well-known types 处理等特性,还需额外添加 protobuf-java-util

<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-java-util</artifactId>
  <version><!--version--></version>
</dependency>

该包对应仓库中的 java/util/ 目录,其源码与 proto 文件(如 duration.prototimestamp.proto 等扩展定义)均位于其中。

三、Android 场景:Java Lite 运行时

文档明确建议:Android 用户应使用 protobuf Java Lite 运行时。理由有三:

  1. 更小的代码体积:Lite 是专为移动端设计的独立运行时,设计目标就是小二进制体积和更低的峰值内存;
  2. 与 ProGuard/R8 更友好:它不依赖 Java 反射,代码被裁剪(stripping)的能力更强;
  3. API 是完整运行时的子集:从 examples/BUILD.bazel 的注释可以看到,由于 Lite API 是常规 Java API 的子集,只要你只用这个子集,代码可以同时编译进服务端构建和 Android 构建,实现双端共享。

Lite 运行时的取舍细节记录在 java/lite.md 中,值得完整了解:

  • 功能子集:无反射 API、无 ProtoJSON、无 TextFormat 支持;
  • 运行时性能特征可能慢于完整运行时;
  • 在 API/ABI 稳定性上不做保证(为性能和体积让路);
  • 它依赖 sun.misc.Unsafe 来优化性能,因此在 sun.misc.Unsafe 不可用的 JVM 环境中无法使用 Lite 运行时(这也意味着它不适合在标准 JVM 服务端使用——文档明确 Lite 不应用于服务端,服务端应始终用完整运行时)。

3.1 生成 Lite 代码与引入依赖

生成 Lite 版本的 Java 代码时,在输出参数前加上 lite: 前缀:

$ protoc --java_out=lite:${OUTPUT_DIR} path/to/your/proto/file

Maven 依赖改为:

<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-javalite</artifactId>
  <version><!--version--></version>
</dependency>

在 Bazel 中这一行为同样内建:java/lite/BUILD.bazel 里定义的 proto_lang_toolchain 目标,其编译命令行正是 --java_out=lite:$(OUT),运行时指向 :lite 目标,并对 well-known protos(any、api、duration、struct 等)做了黑名单排除——因为 Lite 运行时不支持这些类型。

3.2 R8/ProGuard 保活规则

Lite 运行时内部使用反射来避免为每个消息生成 hashCode/equals/parse/serialize 方法(从 GeneratedMessageLite.javacom.google.protobuf.Internal 包的引用可以看到这种"运行时查找"式设计)。而 R8 默认会混淆字段名,导致反射失败,抛出形如 java.lang.RuntimeException: Field {NAME}_ for {CLASS} not found 的异常(源头在 MessageSchema.java)。

java/lite.md 给出的缓解方案是在 proguard-rules.pro 中添加:

-keep class * extends com.google.protobuf.GeneratedMessageLite { *; }

保留所有 GeneratedMessageLite 子类的字段名,即可避免混淆破坏反射路径。

四、Bazel 集成

Bazel 有原生的 protobuf 构建规则,Java 场景使用:

  • java_proto_library:服务端,基于完整运行时;
  • java_lite_proto_library:Android/移动端,基于 Lite 运行时。

仓库的 examples/BUILD.bazel 是官方给出的完整示例,核心模式是"一个语言无关的 proto_library + 每个语言一个生成目标":

# proto_library 不绑定具体语言,只描述 .proto 依赖图,
# 并向编译器提供 .proto 源文件。
proto_library(
    name = "addressbook_proto",
    srcs = ["addressbook.proto"],
    deps = ["@com_google_protobuf//:timestamp_proto"],
)

# Java 完整运行时(服务端)
java_proto_library(
    name = "addressbook_java_proto",
    deps = [":addressbook_proto"],
)

# Java Lite(Android/移动端)
java_lite_proto_library(
    name = "addressbook_java_lite_proto",
    deps = [":addressbook_proto"],
)

java_binary 再依赖对应的 java_proto_library/java_lite_proto_library 目标即可,例如 add_person_java 依赖 :addressbook_java_proto@com_google_protobuf//java/util

关于包体积收益,examples/BUILD.bazel 中的注释给出了可复现的对比方式:

$ bazel build :add_person_java_deploy.jar :add_person_java_lite_deploy.jar
$ ls -l bazel-bin/*_deploy.jar
# add_person_java_deploy.jar   1230797 字节
# add_person_java_lite_deploy.jar  236166 字节

示例中 Lite 版 jar 比完整版小约 6 倍;若再配合 ProGuard 的 inlining/stripping,差距会更大。规则的实现位于 bazel/java_proto_library.bzlbazel/java_lite_proto_library.bzl

五、从源码构建 Java 运行时

大多数用户直接按上述方式使用已发布的运行时即可。但如果你是 protobuf 贡献者,或想使用尚未正式发布的版本,可以从源码构建。注意该流程不需要安装 Maven,会跳过单元测试,且只安装核心库(不含 util 包):

  1. 构建 C++ 代码或获取 protoc 二进制发行版(参考顶层 README.md)。若安装二进制发行版,务必确认其版本与 Java 包版本一致,可运行 protoc --version 检查。若从源码构建了 C++ 代码但未安装,编译器二进制位于 ../src 目录下;

  2. 调用 protoc 生成 DescriptorProtos.java

    $ protoc --java_out=core/src/main/java -I../src \
        ../src/google/protobuf/descriptor.proto
    
  3. 用任意方式编译 core/src/main/java 下的代码;

  4. 将编译产物安装到你希望的位置。

第 2 步的原理在于:Java 运行时的描述符支持代码本身就是由 descriptor.proto 生成的,所以自举构建的第一步就是用 protoc 把描述符协议编译成 Java。

六、Kotlin Protocol Buffers

java/ 目录同时提供 Kotlin 支持。Kotlin protobuf 构建在 Java protobuf 之上,有两个硬约束:

  • 必须依赖 Java protobuf 运行时;
  • 每个 .proto 文件必须同时生成 Java 和 Kotlin 代码

生成命令是两个 --*_out 并用:

$ protoc --java_out=${OUTPUT_DIR} --kotlin_out=${OUTPUT_DIR} path/to/your/proto/file

Kotlin protobuf 的目标是为 Kotlin 提供符合语言习惯(idiomatic)的构建与读取方式(如 DSL 风格的构建器、不可变集合扩展等)。

6.1 Maven 依赖

Kotlin 场景需要同时引入 Java 与 Kotlin 两个运行时,且版本相互一致、并匹配(或不旧于)protoc:

<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-java</artifactId>
  <version><!--version--></version>
</dependency>

<dependency>
  <groupId>com.google.protobuf</groupId>
  <artifactId>protobuf-kotlin</artifactId>
  <version><!--version--></version>
</dependency>

6.2 Kotlin Lite(Android)

Android 上推荐继续使用 Java Lite 运行时。仓库为此提供了 protobuf-kotlin-lite 包(Maven 与 Bazel 均有),与 Java Lite 运行时配对使用。从 java/kotlin-lite/pom_template.xml 可以看到它的依赖声明:

<dependencies>
  <dependency>
    <groupId>{groupId}</groupId>
    <artifactId>protobuf-javalite</artifactId>
    <version>{version}</version>
  </dependency>
  <dependency>
    <groupId>org.jetbrains.kotlin</groupId>
    <artifactId>kotlin-stdlib</artifactId>
    <version>${kotlin.version}</version>
  </dependency>
</dependencies>

protobuf-kotlin-lite 本身也要求依赖 protobuf-javalite

6.3 仓库中的 Kotlin 源码结构

java/kotlin/BUILD.bazel 可以看出 Kotlin 运行时的组织方式:

七、兼容性须知(必须遵守的七条规则)

java/README.md 的 "Compatibility Notice" 是升级运行时前必读的内容,逐条继承如下:

  1. 次版本(minor)发布向后兼容:如果你的代码能在旧版本上构建/运行,按照本节指南操作,就应能在新版本上构建/运行。次版本发布同时保证二进制兼容与源码兼容;
  2. 主版本(major)发布可能向后兼容上一个主版本的最后一次发布,具体见该次发布的发布说明;
  3. @ExperimentalApi 标注的 API 可随时变更,包括任意修改甚至直接删除。如果项目需要兼容性就不要使用它们;如果你的代码本身是库(出现在你无法控制的下游用户的 CLASSPATH 上),更不应使用实验性 API,除非你重新打包(例如用 ProGuard 混淆包名);
  4. 已弃用的非实验 API 会在首次弃用发布两年后移除。你必须在期限内修复引用,否则后果不可预测(不保证会给出编译错误);
  5. 不要自行继承消息接口/类。Protobuf 消息接口/类设计为仅由生成的 protobuf 代码继承,后续版本可能新增方法从而破坏你的自定义子类;
  6. 不要使用任何标注"used by generated code only"的方法/类,它们随时可能变更。仓库中这类 API 的典型代表就是 Internal.java 所在包的内部工具类——例如 GeneratedMessageLite 就大量引用 com.google.protobuf.Internal.*IntListBooleanList 等);
  7. LITE 运行时 API 尚不稳定,即使在次版本发布中也可能变更。

八、延伸阅读

小结:Java 侧的 protobuf 使用路径可以归纳为一句话——服务端用 protoc --java_out + protobuf-java(需要 JSON 能力再加 protobuf-java-util),移动端用 --java_out=lite: + protobuf-javalite(并配置 R8 保活规则),Kotlin 则在两者之上叠加 protobuf-kotlin/protobuf-kotlin-lite 并保持每个 proto 双语言生成;同时严格遵守运行时版本不低于 protoc 以及第七节的兼容性规则,即可在任意版本升级中保持构建稳定。

登录后查看全文
热门项目推荐
相关项目推荐