Java 生成代码指南

精确描述了 protocol buffer 编译器为任何给定协议定义生成的 Java 代码。

proto2、proto3 和 Editions 生成的代码之间的任何差异都会被强调——请注意,这些差异存在于本文档中描述的生成代码中,而不是基础消息类/接口中,基础消息类/接口在所有版本中都是相同的。在阅读本文档之前,您应该阅读proto2 语言指南proto3 语言指南和/或Editions 语言指南

请注意,除非另有说明,否则 Java protocol buffer 方法不接受或返回 null。

编译器调用

当使用 --java_out= 命令行标志调用时,protocol buffer 编译器会生成 Java 输出。--java_out= 选项的参数是您希望编译器写入 Java 输出的目录。对于每个 .proto 文件输入,编译器会创建一个包装器 .java 文件,其中包含一个 Java 类,该类表示 .proto 文件本身。

如果 .proto 文件包含以下行

option java_multiple_files = true;

那么编译器还会为它为 .proto 文件中声明的每个顶级消息、枚举和服务生成的每个类/枚举创建单独的 .java 文件。

否则(当 java_multiple_files 选项被禁用时,这是默认值),上述包装器类也将用作外部类,并且为 .proto 文件中声明的每个顶级消息、枚举和服务生成的类/枚举都将嵌套在外部包装器类中。因此,编译器只会为整个 .proto 文件生成一个 .java 文件,并且它在包中会有一个额外的层级

包装器类的名称选择如下:如果 .proto 文件包含以下行

option java_outer_classname = "Foo";

那么包装器类名称将是 Foo。否则,包装器类名称通过将 .proto 文件基本名称转换为驼峰命名法来确定。例如,foo_bar.proto 将生成一个名为 FooBar 的类。如果文件中有同名的服务、枚举或消息(包括嵌套类型),则会在包装器类名称后附加“OuterClass”。示例

  • 如果 foo_bar.proto 包含一个名为 FooBar 的消息,则包装器类将生成一个名为 FooBarOuterClass 的类。
  • 如果 foo_bar.proto 包含一个名为 FooService 的服务,并且 java_outer_classname 也设置为字符串 FooService,则包装器类将生成一个名为 FooServiceOuterClass 的类。

除了任何嵌套类之外,包装器类本身将具有以下 API(假设包装器类名为 Foo,并且是从 foo.proto 生成的)

public final class Foo {
  private Foo() {}  // Not instantiable.

  /** Returns a FileDescriptor message describing the contents of {@code foo.proto}. */
  public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor();
  /** Adds all extensions defined in {@code foo.proto} to the given registry. */
  public static void registerAllExtensions(com.google.protobuf.ExtensionRegistry registry);
  public static void registerAllExtensions(com.google.protobuf.ExtensionRegistryLite registry);

  // (Nested classes omitted)
}

Java 包名称的选择如下所述

输出文件通过连接 --java_out= 的参数、包名称(将 . 替换为 /)和 .java 文件名来选择。

因此,举例来说,假设您像下面这样调用编译器:

protoc --proto_path=src --java_out=build/gen src/foo.proto

如果 foo.proto 的 Java 包是 com.example 并且它没有启用 java_multiple_files 且其外部类名为 FooProtos,那么 protocol buffer 编译器将生成文件 build/gen/com/example/FooProtos.java。protocol buffer 编译器将根据需要自动创建 build/gen/combuild/gen/com/example 目录。但是,它不会创建 build/genbuild;它们必须已经存在。您可以在单个调用中指定多个 .proto 文件;所有输出文件将一次性生成。

在输出 Java 代码时,protocol buffer 编译器直接输出到 JAR 归档文件的功能特别方便,因为许多 Java 工具能够直接从 JAR 文件读取源代码。要输出到 JAR 文件,只需提供一个以 .jar 结尾的输出位置。请注意,只有 Java 源代码会放置在归档文件中;您仍然必须单独编译它才能生成 Java 类文件。

包(Packages)

生成的类将根据 java_package 选项放置在 Java 包中。如果省略该选项,则使用 package 声明。

例如,如果 .proto 文件包含

package foo.bar;

那么生成的 Java 类将放置在 Java 包 foo.bar 中。但是,如果 .proto 文件还包含 java_package 选项,如下所示

package foo.bar;
option java_package = "com.example.foo.bar";

那么该类将放置在 com.example.foo.bar 包中。提供 java_package 选项是因为普通的 .proto package 声明预计不会以反向域名开头。

消息

如果您正在设计新的 protocol buffer schema,请参阅Java proto 名称的建议

给定一个简单的消息声明:

message Foo {}

protocol buffer 编译器生成一个名为 Foo 的类,该类实现了 Message 接口。该类被声明为 final;不允许进一步子类化。Foo 扩展了 GeneratedMessage,但这应被视为实现细节。默认情况下,Foo 重写了 GeneratedMessage 的许多方法,使用专门的版本以实现最大速度。但是,如果 .proto 文件包含以下行

option optimize_for = CODE_SIZE;

那么 Foo 将只重写运行所需的最少方法集,并依赖 GeneratedMessage 中基于反射的其余方法实现。这显著减少了生成代码的大小,但也降低了性能。或者,如果 .proto 文件包含

option optimize_for = LITE_RUNTIME;

那么 Foo 将包含所有方法的快速实现,但将实现 MessageLite 接口,该接口包含 Message 的方法子集。特别是,它不支持描述符、嵌套构建器或反射。但是,在此模式下,生成的代码只需要链接到 libprotobuf-lite.jar 而不是 libprotobuf.jar。“lite”库比完整库小得多,更适用于资源受限的系统,如手机。

Message 接口定义了允许您检查、操作、读取或写入整个消息的方法。除了这些方法之外,Foo 类还定义了以下静态方法

  • static Foo getDefaultInstance(): 返回 Foo 的*单例*实例。此实例的内容与您调用 Foo.newBuilder().build() 获得的内容完全相同(因此所有 singular 字段都未设置,所有 repeated 字段都为空)。请注意,消息的默认实例可以通过调用其 newBuilderForType() 方法作为工厂使用。
  • static Descriptor getDescriptor(): 返回类型的描述符。这包含有关类型的信息,包括它有哪些字段以及它们的类型。这可以与 Message 的反射方法(如 getField())一起使用。
  • static Foo parseFrom(...): 从给定源解析 Foo 类型的消息并返回。每个 Message.Builder 接口中 mergeFrom() 的变体都有一个对应的 parseFrom 方法。请注意,parseFrom() 从不抛出 UninitializedMessageException;如果解析的消息缺少 required 字段,它会抛出 InvalidProtocolBufferException。这使其与调用 Foo.newBuilder().mergeFrom(...).build() 略有不同。
  • static Parser parser(): 返回 Parser 的实例,该实例实现各种 parseFrom() 方法。
  • Foo.Builder newBuilder(): 创建一个新的构建器(如下所述)。
  • Foo.Builder newBuilder(Foo prototype): 创建一个新的构建器,所有字段都初始化为 prototype 中它们所拥有的相同值。由于嵌入式消息和字符串对象是不可变的,因此它们在原始对象和副本之间共享。

构建器

消息对象——例如上面描述的 Foo 类的实例——是不可变的,就像 Java String 一样。要构造消息对象,您需要使用*构建器*。每个消息类都有自己的构建器类——因此在我们的 Foo 示例中,protocol buffer 编译器生成了一个嵌套类 Foo.Builder,可用于构建 FooFoo.Builder 实现了 Message.Builder 接口。它扩展了 GeneratedMessage.Builder 类,但同样,这应该被视为实现细节。像 Foo 一样,Foo.Builder 可能依赖于 GeneratedMessage.Builder 中的通用方法实现,或者,当使用 optimize_for 选项时,生成速度更快的自定义代码。您可以通过调用静态方法 Foo.newBuilder() 获取 Foo.Builder

Foo.Builder 不定义任何静态方法。它的接口与 Message.Builder 接口定义完全相同,不同之处在于返回类型更具体:修改构建器的方法返回类型为 Foo.Builder,而 build() 返回类型为 Foo

修改构建器内容的方法——包括字段 setter——总是返回对构建器的引用(即它们“return this;”)。这允许在同一行中链接多个方法调用。例如:builder.mergeFrom(obj).setFoo(1).setBar("abc").clearBaz();

请注意,构建器不是线程安全的,因此当多个不同线程需要修改单个构建器的内容时,应使用 Java 同步。

子构建器

对于包含子消息的消息,编译器还会生成子构建器。这允许您重复修改深层嵌套的子消息而无需重新构建它们。例如

message Foo {
  int32 val = 1;
  // some other fields.
}

message Bar {
  Foo foo = 1;
  // some other fields.
}

message Baz {
  Bar bar = 1;
  // some other fields.
}

如果您已经有一个 Baz 消息,并且想要更改 Foo 中深层嵌套的 val。而不是

baz = baz.toBuilder().setBar(
    baz.getBar().toBuilder().setFoo(
        baz.getBar().getFoo().toBuilder().setVal(10).build()
    ).build()).build();

您可以写入

Baz.Builder builder = baz.toBuilder();
builder.getBarBuilder().getFooBuilder().setVal(10);
baz = builder.build();

嵌套类型

消息可以声明在另一个消息内部。例如:

message Foo {
  message Bar { }
}

在这种情况下,编译器只是将 Bar 作为 Foo 中嵌套的内部类生成。

字段

除了上一节中描述的方法之外,protocol buffer 编译器还会为 .proto 文件中消息内定义的每个字段生成一组访问器方法。读取字段值的方法在消息类及其相应的构建器中都定义;修改值的方法只在构建器中定义。

请注意,方法名称总是使用驼峰命名法,即使 .proto 文件中的字段名称使用带下划线的小写字母(应该如此)。大小写转换如下

  • 对于名称中的每个下划线,下划线被移除,其后的字母大写。
  • 如果名称将带有前缀(例如“get”),则首字母大写。否则,小写。
  • 方法名称中每个数字后面的字母都会大写。

因此,字段 foo_bar_baz 变为 fooBarBaz。如果加上 get 前缀,它将是 getFooBarBaz。而 foo_ba23r_baz 变为 fooBa23RBaz

除了访问器方法之外,编译器还会为每个字段生成一个整数常量,其中包含其字段编号。常量名称是字段名称转换为大写,后跟 _FIELD_NUMBER。例如,给定字段 int32 foo_bar = 5;,编译器将生成常量 public static final int FOO_BAR_FIELD_NUMBER = 5;

以下各节分为显式存在和隐式存在。Proto2 具有显式存在,proto3 默认隐式存在。Editions 默认显式存在,但您可以使用features.field_presence覆盖它。

具有显式存在性的单一字段

对于任何这些字段定义

optional int32 foo = 1;
required int32 foo = 1;

编译器将在消息类及其构建器中生成以下访问器方法

  • boolean hasFoo(): 如果字段已设置,则返回 true
  • int getFoo(): 返回字段的当前值。如果字段未设置,则返回默认值。

编译器将仅在消息的构建器中生成以下方法

  • Builder setFoo(int value): 设置字段的值。调用此方法后,hasFoo() 将返回 truegetFoo() 将返回 value
  • Builder clearFoo(): 清除字段的值。调用此方法后,hasFoo() 将返回 falsegetFoo() 将返回默认值。

对于其他简单字段类型,根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,值类型被替换为消息或枚举类。

内嵌消息字段

对于消息类型,setFoo() 也接受消息构建器类型的实例作为参数。这只是一个快捷方式,相当于在构建器上调用 .build() 并将结果传递给该方法。进一步修改传递给 setFoo 的子构建器将**不会**反映在消息类的构建器中。消息类的构建器“拥有”子消息。

如果字段未设置,getFoo() 将返回一个所有字段均未设置的 Foo 实例(可能是 Foo.getDefaultInstance() 返回的实例)。

此外,编译器生成了两个访问器方法,允许您访问消息类型的相关子构建器。以下方法在消息类及其构建器中都生成

  • FooOrBuilder getFooOrBuilder(): 返回字段的构建器(如果已存在),否则返回消息。在构建器上调用此方法不会为字段创建子构建器。

编译器只在消息的构建器中生成以下方法。

  • Builder getFooBuilder(): 返回字段的构建器。

具有隐式存在性的单一字段

对于此字段定义:

int32 foo = 1;

编译器将在消息类及其构建器中生成以下访问器方法

  • int getFoo(): 返回字段的当前值。如果字段未设置,则返回该字段类型的默认值。

编译器将仅在消息的构建器中生成以下方法

  • Builder setFoo(int value): 设置字段的值。调用此方法后,getFoo() 将返回 value
  • Builder clearFoo(): 清除字段的值。调用此方法后,getFoo() 将返回该字段类型的默认值。

对于其他简单字段类型,根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,值类型被替换为消息或枚举类。

枚举字段

对于枚举字段类型,消息类及其构建器中会生成一个额外的访问器方法

  • int getFooValue(): 返回枚举的整数值。

编译器将仅在消息的构建器中生成以下附加方法

  • Builder setFooValue(int value): 设置枚举的整数值。

此外,如果枚举值为未知——这是 proto3 编译器添加到生成的枚举类型中的特殊附加值,则 getFoo() 将返回 UNRECOGNIZED

重复字段

对于此字段定义:

repeated string foos = 1;

编译器将在消息类及其构建器中生成以下访问器方法

  • int getFoosCount(): 返回字段中当前元素的数量。
  • String getFoos(int index): 返回给定零基索引处的元素。
  • ProtocolStringList getFoosList(): 将整个字段作为 ProtocolStringList 返回。如果字段未设置,则返回空列表。

编译器将仅在消息的构建器中生成以下方法

  • Builder setFoos(int index, String value): 设置给定零基索引处元素的值。
  • Builder addFoos(String value): 将一个新元素附加到具有给定值的字段。
  • Builder addAllFoos(Iterable<? extends String> value): 将给定 Iterable 中的所有元素附加到字段。
  • Builder clearFoos(): 从字段中删除所有元素。调用此方法后,getFoosCount() 将返回零。

对于其他简单字段类型,根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,类型是消息或枚举类。

重复内嵌消息字段

对于消息类型,setFoos()addFoos() 也接受消息构建器类型的实例作为参数。这只是一个快捷方式,相当于在构建器上调用 .build() 并将结果传递给该方法。还有一个额外的生成方法

  • Builder addFoos(int index, Field value): 在给定的零基索引处插入一个新元素。将当前在该位置的元素(如果有)和任何后续元素向右移动(将它们的索引加一)。

此外,编译器在消息类及其构建器中为消息类型生成以下额外的访问器方法,允许您访问相关的子构建器

  • FooOrBuilder getFoosOrBuilder(int index): 返回指定元素的构建器(如果已存在),否则如果不存在则抛出 IndexOutOfBoundsException。如果从消息类调用此方法,它将始终返回消息(或抛出异常)而不是构建器。在构建器上调用此方法不会为字段创建子构建器。
  • List<FooOrBuilder> getFoosOrBuilderList(): 将整个字段作为构建器(如果可用)或消息的不可修改列表返回。如果从消息类调用此方法,它将始终返回一个不可变的消息列表,而不是不可修改的构建器列表。

编译器将仅在消息的构建器中生成以下方法

  • Builder getFoosBuilder(int index): 返回指定索引处元素的构建器,如果索引超出范围,则抛出 IndexOutOfBoundsException
  • Builder addFoosBuilder(int index): 在指定索引处插入并返回重复消息的默认消息实例的构建器。现有条目将移至更高的索引以为插入的构建器腾出空间。
  • Builder addFoosBuilder(): 附加并返回重复消息的默认消息实例的构建器。
  • Builder removeFoos(int index): 删除给定零基索引处的元素。
  • List<Builder> getFoosBuilderList(): 将整个字段作为不可修改的构建器列表返回。

重复枚举字段 (仅限 proto3)

编译器将在消息类及其构建器中生成以下附加方法

  • int getFoosValue(int index): 返回指定索引处枚举的整数值。
  • List<java.lang.Integer> getFoosValueList(): 将整个字段作为整数列表返回。

编译器将仅在消息的构建器中生成以下附加方法

  • Builder setFoosValue(int index, int value): 设置指定索引处枚举的整数值。

名称冲突

如果另一个非重复字段的名称与重复字段的某个生成方法冲突,则两个字段名称的末尾都将附加其 protobuf 字段编号。

对于这些字段定义

int32 foos_count = 1;
repeated string foos = 2;

编译器将首先将它们重命名为以下内容

int32 foos_count_1 = 1;
repeated string foos_2 = 2;

然后,将按照上述描述生成访问器方法。

Oneof 字段

对于此 oneof 字段定义

oneof choice {
    int32 foo_int = 4;
    string foo_string = 9;
    ...
}

choice oneof 中的所有字段都将使用一个私有字段作为其值。此外,protocol buffer 编译器将为 oneof 情况生成一个 Java 枚举类型,如下所示

public enum ChoiceCase
        implements com.google.protobuf.Internal.EnumLite {
      FOO_INT(4),
      FOO_STRING(9),
      ...
      CHOICE_NOT_SET(0);
      ...
    };

此枚举类型的值具有以下特殊方法

  • int getNumber(): 返回 .proto 文件中定义的对象数字值。
  • static ChoiceCase forNumber(int value): 返回与给定数字值对应的枚举对象(或其他数字值的 null)。

编译器将在消息类及其构建器中生成以下访问器方法

  • boolean hasFooInt(): 如果 oneof case 是 FOO_INT,则返回 true
  • int getFooInt(): 如果 oneof case 是 FOO_INT,则返回 foo 的当前值。否则,返回此字段的默认值。
  • ChoiceCase getChoiceCase(): 返回指示哪个字段已设置的枚举。如果它们都没有设置,则返回 CHOICE_NOT_SET

编译器将仅在消息的构建器中生成以下方法

  • Builder setFooInt(int value): 将 Foo 设置为此值,并将 oneof case 设置为 FOO_INT。调用此方法后,hasFooInt() 将返回 truegetFooInt() 将返回 valuegetChoiceCase() 将返回 FOO_INT
  • Builder clearFooInt():
    • 如果 oneof case 不是 FOO_INT,则不会有任何更改。
    • 如果 oneof case 是 FOO_INT,则将 Foo 设置为 null,并将 oneof case 设置为 CHOICE_NOT_SET。调用此方法后,hasFooInt() 将返回 falsegetFooInt() 将返回默认值,并且 getChoiceCase() 将返回 CHOICE_NOT_SET
  • Builder.clearChoice(): 重置 choice 的值,并返回构建器。

对于其他简单字段类型,根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,值类型被替换为消息或枚举类。

映射字段

对于此 map 字段定义:

map<int32, int32> weight = 1;

编译器将在消息类及其构建器中生成以下访问器方法

  • Map<Integer, Integer> getWeightMap();: 返回一个不可修改的 Map
  • int getWeightOrDefault(int key, int default);: 返回 key 的值,如果不存在则返回默认值。
  • int getWeightOrThrow(int key);: 返回 key 的值,如果不存在则抛出 IllegalArgumentException。
  • boolean containsWeight(int key);: 指示此字段中是否存在 key。
  • int getWeightCount();: 返回 map 中的元素数量。

编译器将仅在消息的构建器中生成以下方法

  • Builder putWeight(int key, int value);: 将权重添加到此字段。
  • Builder putAllWeight(Map<Integer, Integer> value);: 将给定 map 中的所有条目添加到此字段。
  • Builder removeWeight(int key);: 从此字段中删除权重。
  • Builder clearWeight();: 从此字段中删除所有权重。
  • @Deprecated Map<Integer, Integer> getMutableWeight();: 返回一个可变 Map。请注意,多次调用此方法可能会返回不同的 map 实例。返回的 map 引用可能会因后续对 Builder 的任何方法调用而失效。

消息值 Map 字段

对于值类型为消息类型的 Map,编译器将在消息的构建器中生成一个额外的方法

  • Foo.Builder putFooBuilderIfAbsent(int key);: 确保映射中存在 key,如果不存在则插入一个新的 Foo.Builder。对返回的 Foo.Builder 的更改将反映在最终消息中。

Any

给定一个像这样的 Any 字段

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  google.protobuf.Any details = 2;
}

在我们的生成代码中,details 字段的 getter 返回一个 com.google.protobuf.Any 实例。这提供了以下特殊方法来打包和解包 Any 的值

class Any {
  // Packs the given message into an Any using the default type URL
  // prefix “type.googleapis.com”.
  public static Any pack(Message message);
  // Packs the given message into an Any using the given type URL
  // prefix.
  public static Any pack(Message message,
                         String typeUrlPrefix);

  // Checks whether this Any message’s payload is the given type.
  public <T extends Message> boolean is(class<T> clazz);

  // Checks whether this Any message’s payload has the same type as the given
  // message.
  public boolean isSameTypeAs(Message message);

  // Unpacks Any into a message with the same type as the given messsage.
  // Throws exception if the type doesn’t match or parsing the payload fails.
  public <T extends Message> T unpackSameTypeAs(T message)
      throws InvalidProtocolBufferException;

  // Unpacks Any into the given message type. Throws exception if
  // the type doesn’t match or parsing the payload has failed.
  public <T extends Message> T unpack(class<T> clazz)
      throws InvalidProtocolBufferException;
}

枚举

给定一个枚举定义,例如:

enum Foo {
  VALUE_A = 0;
  VALUE_B = 5;
  VALUE_C = 1234;
}

protocol buffer 编译器将生成一个名为 Foo 的 Java 枚举类型,其中包含相同的值集。如果您使用 proto3,它还会向枚举类型添加特殊值 UNRECOGNIZED。在 Editions 中,OPEN 枚举也有 UNRECOGNIZED 值,而 CLOSED 枚举则没有。生成的枚举类型的值具有以下特殊方法

  • int getNumber(): 返回 .proto 文件中定义的对象数字值。
  • EnumValueDescriptor getValueDescriptor(): 返回值的描述符,其中包含有关值的名称、编号和类型的信息。
  • EnumDescriptor getDescriptorForType(): 返回枚举类型的描述符,其中包含例如每个定义值的信息。

此外,Foo 枚举类型包含以下静态方法

  • static Foo forNumber(int value): 返回与给定数字值对应的枚举对象。当没有对应的枚举对象时返回 null。
  • static Foo valueOf(int value): 返回与给定数字值对应的枚举对象。此方法已弃用,建议使用 forNumber(int value),并将在即将发布的版本中删除。
  • static Foo valueOf(EnumValueDescriptor descriptor): 返回与给定值描述符对应的枚举对象。可能比 valueOf(int) 更快。在 proto3 和 OPEN 枚举中,如果传递未知值描述符,则返回 UNRECOGNIZED
  • EnumDescriptor getDescriptor(): 返回枚举类型的描述符,其中包含例如每个定义值的信息。(这与 getDescriptorForType() 的区别仅在于它是一个静态方法。)

每个枚举值还会生成一个带后缀 _VALUE 的整数常量。

请注意,.proto 语言允许多个枚举符号具有相同的数字值。具有相同数字值的符号是同义词。例如

enum Foo {
  BAR = 0;
  BAZ = 0;
}

在这种情况下,BAZBAR 的同义词。在 Java 中,BAZ 将定义为如下所示的静态 final 字段

static final Foo BAZ = BAR;

因此,BARBAZ 比较相等,并且 BAZ 不应出现在 switch 语句中。编译器总是选择第一个具有给定数字值的符号作为该符号的“规范”版本;所有后续具有相同数字的符号都只是别名。

枚举可以在消息类型内嵌套定义。编译器将在该消息类型的类中嵌套生成 Java 枚举定义。

注意:生成 Java 代码时,protobuf 枚举的最大值数量可能出奇的低——在最坏的情况下,最大值略高于 1,700 个。这个限制是由于 Java 字节码的每方法大小限制,并且它因 Java 实现、protobuf 套件的不同版本以及 .proto 文件中枚举设置的任何选项而异。

扩展

给定一个带有扩展范围的消息

edition = "2023";

message Foo {
  extensions 100 to 199;
}

protocol buffer 编译器将使 Foo 扩展 GeneratedMessage.ExtendableMessage 而不是通常的 GeneratedMessage。同样,Foo 的构建器将扩展 GeneratedMessage.ExtendableBuilder。您不应按名称引用这些基类型(GeneratedMessage 被视为实现细节)。但是,这些超类定义了许多您可以用来操作扩展的额外方法。

特别是 FooFoo.Builder 将继承方法 hasExtension()getExtension()getExtensionCount()。此外,Foo.Builder 将继承方法 setExtension()clearExtension()。每个这些方法都将其第一个参数作为扩展标识符(如下所述),该标识符标识一个扩展字段。其余参数和返回值与为具有与扩展标识符相同类型的正常(非扩展)字段生成的相应访问器方法完全相同。

给定一个扩展定义:

edition = "2023";

import "foo.proto";

extend Foo {
  int32 bar = 123;
}

protocol buffer 编译器生成一个名为 bar 的“扩展标识符”,您可以将其与 Foo 的扩展访问器一起使用以访问此扩展,如下所示

Foo foo =
  Foo.newBuilder()
     .setExtension(bar, 1)
     .build();
assert foo.hasExtension(bar);
assert foo.getExtension(bar) == 1;

(扩展标识符的确切实现很复杂,并且涉及泛型的神奇用法——但是,您无需担心扩展标识符的工作原理即可使用它们。)

请注意,bar 将被声明为 .proto 文件的包装器类的静态字段,如上所述;在示例中我们省略了包装器类名称。

扩展可以在另一个类型的范围内声明,以作为其生成的符号名称的前缀。例如,一种常见的模式是通过在字段类型声明*内部*的字段来扩展消息

edition = "2023";

import "foo.proto";

message Baz {
  extend Foo {
    Baz foo_ext = 124;
  }
}

在这种情况下,具有标识符 foo_ext 和类型 Baz 的扩展在 Baz 的声明内部声明,并且引用 foo_ext 需要添加 Baz. 前缀

Baz baz = createMyBaz();
Foo foo =
  Foo.newBuilder()
     .setExtension(Baz.fooExt, baz)
     .build();
assert foo.hasExtension(Baz.fooExt);
assert foo.getExtension(Baz.fooExt) == baz;

当解析可能具有扩展的消息时,您必须提供一个ExtensionRegistry,您已在该注册表中注册了您希望能够解析的任何扩展。否则,这些扩展将被视为未知字段,并且观察扩展的方法将表现为它们不存在。

ExtensionRegistry registry = ExtensionRegistry.newInstance();
registry.add(Baz.fooExt);
Foo foo = Foo.parseFrom(input, registry);
assert foo.hasExtension(Baz.fooExt);
ExtensionRegistry registry = ExtensionRegistry.newInstance();
Foo foo = Foo.parseFrom(input, registry);
assert foo.hasExtension(Baz.fooExt) == false;

服务(Services)

如果 .proto 文件包含以下行:

option java_generic_services = true;

然后 protocol buffer 编译器将根据文件中找到的服务定义生成代码,如本节所述。但是,生成的代码可能不理想,因为它不与任何特定的 RPC 系统绑定,因此比针对一个系统定制的代码需要更多的间接层。如果您不希望生成此代码,请将此行添加到文件中

option java_generic_services = false;

如果上述两行都未给出,该选项默认为 false,因为通用服务已被弃用。(请注意,在 2.4.0 之前,该选项默认为 true

基于 .proto 语言服务定义的 RPC 系统应提供插件来生成适合该系统的代码。这些插件很可能要求禁用抽象服务,以便它们可以生成自己同名的类。

本节的其余部分描述了当启用抽象服务时,Protocol Buffer 编译器会生成什么。

接口

给定一个服务定义:

service Foo {
  rpc Bar(FooRequest) returns(FooResponse);
}

protocol buffer 编译器将生成一个抽象类 Foo 来表示此服务。Foo 将为服务定义中定义的每个方法提供一个抽象方法。在这种情况下,方法 Bar 定义为

abstract void bar(RpcController controller, FooRequest request,
                  RpcCallback<FooResponse> done);

参数等同于 Service.CallMethod() 的参数,只是 method 参数是隐含的,并且 requestdone 指定它们的精确类型。

FooService 接口的子类。Protocol Buffer 编译器会自动生成 Service 方法的实现,如下所示:

  • getDescriptorForType: 返回服务的 ServiceDescriptor
  • callMethod: 根据提供的方法描述符确定正在调用的方法并直接调用它,将请求消息和回调向下转换为正确的类型。
  • getRequestPrototypegetResponsePrototype: 返回给定方法正确类型的请求或响应的默认实例。

还生成了以下静态方法

  • static ServiceDescriptor getServiceDescriptor(): 返回类型的描述符,其中包含有关此服务具有哪些方法以及它们的输入和输出类型的信息。

Foo 还将包含一个嵌套接口 Foo.Interface。这是一个纯接口,它再次包含与服务定义中每个方法对应的方法。但是,此接口不扩展 Service 接口。这是一个问题,因为 RPC 服务器实现通常是使用抽象 Service 对象编写的,而不是您的特定服务。为了解决这个问题,如果您有一个实现 Foo.Interface 的对象 impl,您可以调用 Foo.newReflectiveService(impl) 来构造一个 Foo 实例,该实例只是委托给 impl,并实现了 Service

总结一下,当实现您自己的服务时,您有两种选择

  • 子类化 Foo 并适当地实现其方法,然后将您的子类实例直接传递给 RPC 服务器实现。这通常是最简单的,但有些人认为它“不够纯粹”。
  • 实现 Foo.Interface 并使用 Foo.newReflectiveService(Foo.Interface) 构造一个包装它的 Service 实例,然后将包装器传递给您的 RPC 实现。

存根

protocol buffer 编译器还会生成每个服务接口的“存根”实现,客户端使用它向实现该服务的服务器发送请求。对于 Foo 服务(上面),存根实现 Foo.Stub 将定义为嵌套类。

Foo.StubFoo 的子类,它还实现了以下方法

  • Foo.Stub(RpcChannel channel): 构造一个新的存根,它通过给定通道发送请求。
  • RpcChannel getChannel(): 返回此存根的通道,如传递给构造函数。

存根还实现了服务的每个方法,作为通道的包装器。调用其中一个方法只是调用 channel.callMethod()

Protocol Buffer 库不包含 RPC 实现。但是,它包含了您将生成的服务类连接到您选择的任何任意 RPC 实现所需的所有工具。您只需要提供 RpcChannelRpcController 的实现。

阻塞接口

上面描述的 RPC 类都具有非阻塞语义:当您调用方法时,您提供一个回调对象,该对象将在方法完成时被调用。通常,使用阻塞语义编写代码更容易(尽管可伸缩性可能较差),即方法在完成之前不会返回。为了适应这种情况,protocol buffer 编译器还会生成服务类的阻塞版本。Foo.BlockingInterface 等效于 Foo.Interface,只是每个方法简单地返回结果而不是调用回调。因此,例如,bar 定义为

abstract FooResponse bar(RpcController controller, FooRequest request)
                         throws ServiceException;

类似于非阻塞服务,Foo.newReflectiveBlockingService(Foo.BlockingInterface) 返回一个包装某些 Foo.BlockingInterfaceBlockingService。最后,Foo.BlockingStub 返回一个 Foo.BlockingInterface 的存根实现,该实现向特定的 BlockingRpcChannel 发送请求。

插件插入点

代码生成器插件如果希望扩展 Java 代码生成器的输出,可以使用给定的插入点名称插入以下类型的代码。

  • outer_class_scope: 属于文件包装器类的成员声明。
  • class_scope:TYPENAME: 属于消息类的成员声明。TYPENAME 是完整的 proto 名称,例如 package.MessageType
  • builder_scope:TYPENAME: 属于消息构建器类的成员声明。TYPENAME 是完整的 proto 名称,例如 package.MessageType
  • enum_scope:TYPENAME: 属于枚举类的成员声明。TYPENAME 是完整的 proto 枚举名称,例如 package.EnumType
  • message_implements:TYPENAME: 消息类的类实现声明。TYPENAME 是完整的 proto 名称,例如 package.MessageType
  • builder_implements:TYPENAME: 构建器类的类实现声明。TYPENAME 是完整的 proto 名称,例如 package.MessageType

生成的代码不能包含 import 语句,因为这些语句容易与生成的代码本身中定义的类型名称发生冲突。相反,当引用外部类时,您必须始终使用其完全限定名。

Java 代码生成器中确定输出文件名的逻辑相当复杂。您可能应该查看 protoc 源代码,特别是 java_headers.cc,以确保您涵盖了所有情况。

不要生成依赖于标准代码生成器声明的私有类成员的代码,因为这些实现细节在未来版本的 Protocol Buffers 中可能会改变。

工具类

Protocol buffer 提供了工具类用于消息比较、JSON 转换以及处理众所周知类型(用于常见用例的预定义 protocol buffer 消息)