Java 生成代码指南

描述了协议缓冲区编译器为任何给定的协议定义生成的 Java 代码的具体内容。

proto2 和 proto3 生成代码之间的任何差异都将突出显示——请注意,这些差异在于本文档中描述的生成代码,而不是基本消息类/接口,它们在两个版本中是相同的。在阅读本文档之前,您应该阅读 proto2 语言指南 和/或 proto3 语言指南

请注意,除非另有说明,否则没有 Java 协议缓冲区方法接受或返回 null。

编译器调用

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

如果 .proto 文件包含如下行

option java_multiple_files = true;

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

否则(当 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,则协议缓冲区编译器将生成文件 build/gen/com/example/FooProtos.java。如果需要,协议缓冲区编译器将自动创建 build/gen/combuild/gen/com/example 目录。但是,它不会创建 build/genbuild;它们必须已经存在。您可以在单个调用中指定多个 .proto 文件;所有输出文件将一次生成。

当输出 Java 代码时,协议缓冲区编译器直接输出到 JAR 归档文件的能力尤其方便,因为许多 Java 工具能够直接从 JAR 文件中读取源代码。要输出到 JAR 文件,只需提供以 .jar 结尾的输出位置。请注意,只有 Java 源代码会放置在归档文件中;您仍然必须单独编译它以生成 Java 类文件。

生成的类放置在基于 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 声明不应以反向域名开头。

消息

如果您正在设计新的协议缓冲区模式,请参阅 Java proto 名称的建议

给定一个简单的消息声明

message Foo {}

协议缓冲区编译器会生成一个名为 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() 所得到的内容相同(因此所有单数字段都未设置,所有重复字段都为空)。请注意,消息的默认实例可以通过调用其 newBuilderForType() 方法用作工厂。
  • static Descriptor getDescriptor():返回类型的描述符。其中包含有关类型的信息,包括它具有哪些字段以及它们的类型是什么。这可以与 Message 的反射方法一起使用,例如 getField()
  • static Foo parseFrom(...):从给定的源解析类型为 Foo 的消息并返回它。Message.Builder 接口中的每个 mergeFrom() 变体都对应一个 parseFrom 方法。请注意,parseFrom() 永远不会抛出 UninitializedMessageException;如果解析的消息缺少必需字段,它会抛出 InvalidProtocolBufferException。这使其与调用 Foo.newBuilder().mergeFrom(...).build() 略有不同。
  • static Parser parser():返回 Parser 的实例,它实现了各种 parseFrom() 方法。
  • Foo.Builder newBuilder():创建一个新的构建器(如下所述)。
  • Foo.Builder newBuilder(Foo prototype):创建一个新的构建器,其中所有字段都初始化为与 prototype 中相同的值。由于嵌入的消息和字符串对象是不可变的,因此它们在原始对象和副本之间共享。

构建器

消息对象(例如上述 Foo 类的实例)是不可变的,就像 Java String 一样。要构造消息对象,您需要使用构建器。每个消息类都有自己的构建器类——因此在我们的 Foo 示例中,协议缓冲区编译器生成了一个嵌套类 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 {
  optional int32 val = 1;
  // some other fields.
}

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

message Baz {
  optional 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 内部的内部类。

字段

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

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

  • 对于名称中的每个下划线,都将删除下划线,并将后面的字母大写。
  • 如果名称将附加前缀(例如 “get”),则第一个字母将大写。否则,它将小写。
  • 方法名称中每个数字后的字母都将大写。

因此,字段 foo_bar_baz 变为 fooBarBaz。如果以 get 为前缀,则为 getFooBarBaz。而 foo_ba23r_baz 变为 fooBa23RBaz

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

单数字段 (proto2)

对于这些字段定义中的任何一个

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() 并将结果传递给该方法。

如果字段未设置,getFoo() 将返回一个 Foo 实例,其中未设置任何字段(可能是 Foo.getDefaultInstance() 返回的实例)。

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

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

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

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

单数字段 (proto3)

对于此字段定义

int32 foo = 1;

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

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

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

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

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

嵌入消息字段

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

  • boolean hasFoo():如果字段已设置,则返回 true

setFoo() 也接受消息的构建器类型的实例作为参数。这只是一个快捷方式,等效于在构建器上调用 .build() 并将结果传递给该方法。

如果字段未设置,getFoo() 将返回一个 Foo 实例,其中未设置任何字段(可能是 Foo.getDefaultInstance() 返回的实例)。

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

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

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

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

枚举字段

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

  • 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 中的所有字段将使用单个私有字段来存储其值。此外,协议缓冲区编译器将为 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 字段定义

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 引用可能会被对构建器的任何后续方法调用所无效。

消息值 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;
}

协议缓冲区编译器将生成一个名为 Foo 的 Java 枚举类型,其中包含相同的值集。如果您使用的是 proto3,它还会向枚举类型添加特殊值 UNRECOGNIZED。生成的枚举类型的值具有以下特殊方法

  • 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 中,如果传递了未知值描述符,则返回 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 文件中枚举上设置的任何选项之间有所不同。

扩展 (仅 proto2)

给定具有扩展范围的消息

message Foo {
  extensions 100 to 199;
}

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

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

给定扩展定义

extend Foo {
  optional int32 bar = 123;
}

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

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

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

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

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

message Baz {
  extend Foo {
    optional 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;

服务

如果 .proto 文件包含以下行

option java_generic_services = true;

那么协议缓冲区编译器将根据文件中找到的服务定义生成代码,如本节所述。但是,生成的代码可能是不理想的,因为它未绑定到任何特定的 RPC 系统,因此比为某个系统量身定制的代码需要更多的间接级别。如果您不希望生成此代码,请将此行添加到文件中

option java_generic_services = false;

如果未给出上述任何一行,则该选项默认为 false,因为通用服务已弃用。(请注意,在 2.4.0 之前的版本中,该选项默认为 true

基于 .proto 语言服务定义的 RPC 系统应提供 插件,以生成适合该系统的代码。这些插件可能需要禁用抽象服务,以便它们可以生成同名的自己的类。插件在 2.3.0 版本(2010 年 1 月)中是新增功能。

本节的其余部分描述了在启用抽象服务时协议缓冲区编译器生成的内容。

接口

给定服务定义

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

协议缓冲区编译器将生成一个抽象类 Foo 来表示此服务。Foo 将为服务定义中定义的每个方法都具有一个抽象方法。在本例中,方法 Bar 定义为

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

这些参数等效于 Service.CallMethod() 的参数,只是 method 参数是隐含的,而 requestdone 指定了它们的精确类型。

Foo 继承了 Service 接口。协议缓冲区编译器自动生成 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 实现。

协议缓冲区编译器还会为每个服务接口生成一个“存根”实现,客户端希望使用它向实现该服务的服务器发送请求。对于 Foo 服务(如上所述),存根实现 Foo.Stub 将被定义为嵌套类。

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

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

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

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

阻塞接口

上面描述的 RPC 类都具有非阻塞语义:当您调用一个方法时,您提供一个回调对象,该对象将在方法完成时被调用。通常,使用阻塞语义编写代码更容易(尽管可能扩展性较差),在这种情况下,方法在完成之前不会返回。为了适应这一点,协议缓冲区编译器还生成了服务类的阻塞版本。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 转换以及使用 众所周知的类型(用于常见用例的预定义协议缓冲区消息)。