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

注意:如果您使用的是已弃用的 protobuf API 的 v1 版本,则无论与消息名称是否存在冲突,都会添加OuterClass

除了任何嵌套类之外,包装类本身还将具有以下 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 类文件。

例如,如果.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选项是因为不希望正常的.protopackage声明以反向域名开头。

消息

给定一个简单的消息声明

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的消息并返回它。有一个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中相同的​​值。由于嵌入式消息和字符串对象是不可变的,因此它们在原始副本和副本之间共享。

构建器

消息对象(例如上面描述的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方法返回类型Foo.Builder,并且build()返回类型Foo

修改构建器内容的方法(包括字段设置器)始终返回对构建器的引用(即它们“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 生成为主类内部的嵌套内部类。

字段

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

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

  • 对于名称中的每个下划线,都会删除下划线,并将后面的字母大写。
  • 如果名称将附加前缀(例如“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() 将返回 true,而 getFoo() 将返回 value
  • Builder clearFoo():清除字段的值。调用此方法后,hasFoo() 将返回 false,而 getFoo() 将返回默认值。

对于其他简单字段类型,相应的 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,则返回 true
  • int getFooInt():如果 oneof case 为 FOO,则返回 foo 的当前值。否则,返回此字段的默认值。
  • ChoiceCase getChoiceCase():返回指示哪个字段已设置的枚举。如果它们都没有设置,则返回 CHOICE_NOT_SET

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

  • Builder setFooInt(int value):将 Foo 设置为此值并将 oneof case 设置为 FOO。调用此方法后,hasFooInt() 将返回 truegetFooInt() 将返回 value,而 getChoiceCase() 将返回 FOO
  • Builder clearFooInt():
    • 如果 oneof case 不是 FOO,则不会更改任何内容。
    • 如果 oneof case 为 FOO,则将 Foo 设置为 null 并将 oneof case 设置为 FOO_NOT_SET。调用此方法后,hasFooInt() 将返回 falsegetFooInt() 将返回默认值,而 getChoiceCase() 将返回 FOO_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();:返回映射中元素的数量。

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

  • Builder putWeight(int key, int value);:将权重添加到此字段。
  • Builder putAllWeight(Map<Integer, Integer> value);:将给定映射中的所有条目添加到此字段。
  • Builder removeWeight(int key);:从此字段中删除权重。
  • Builder clearWeight();:从此字段中删除所有权重。
  • @Deprecated Map<Integer, Integer> getMutableWeight();:返回一个可修改的 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指定了它们的精确类型。

FooService接口的子类。协议缓冲区编译器自动生成Service方法的实现,如下所示

  • getDescriptorForType:返回服务的ServiceDescriptor
  • callMethod:根据提供的 method 描述符确定正在调用哪个方法,并直接调用它,将请求消息和回调向下转换为正确的类型。
  • 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 实现。但是,它包含所有将生成的 service 类连接到您选择的任何任意 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 转换以及处理知名类型(用于常见用例的预定义协议缓冲区消息)。