Java 生成的代码指南

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

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

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

编译器调用

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

如果 .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 模式,请参阅有关 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 类型的消息并返回它。每个 parseFrom 方法对应 Message.Builder 接口中的一个 mergeFrom() 变体。请注意,parseFrom() 永远不会抛出 UninitializedMessageException;如果解析的消息缺少必需字段,它会抛出 InvalidProtocolBufferException。这使得它与调用 Foo.newBuilder().mergeFrom(...).build() 有细微差别。
  • static Parser parser():返回一个 Parser 的实例,该实例实现了各种 parseFrom() 方法。
  • Foo.Builder newBuilder():创建一个新的构建器(如下所述)。
  • Foo.Builder newBuilder(Foo prototype):创建一个新的构建器,其所有字段都初始化为与 prototype 中相同的值。由于嵌入的消息和字符串对象是不可变的,它们在原始对象和副本之间是共享的。

构建器 (Builder)

消息对象——例如上面描述的 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 中修改构建器的方法返回类型为 Foo.Builder,而 build() 返回类型为 Foo

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

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

子构建器 (Sub-Builder)

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

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;

以下部分分为显式存在(explicit presence)和隐式存在(implicit presence)。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):设置枚举的整数值。

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

重复字段

对于此字段定义:

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 case 生成一个 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);:返回键对应的值,如果不存在则返回默认值。
  • int getWeightOrThrow(int key);:返回键对应的值,如果不存在则抛出 IllegalArgumentException。
  • boolean containsWeight(int 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);

  // 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 编译器还会为每个服务接口生成一个“存根”(stub)实现,供希望向实现该服务的服务器发送请求的客户端使用。对于 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 消息)