Java 生成的代码指南

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

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

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

编译器调用

当使用 --java_out= 命令行标志调用时,协议缓冲区编译器会生成 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,则协议缓冲区编译器将生成文件 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 的消息并返回它。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,而 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 生成为嵌套在 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() 并将结果传递给方法。进一步修改传递给 setFoo 的子构建器将不会反映在消息类的构建器中。消息类的构建器“拥有”该子消息。

如果字段未设置,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 情况生成一个 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 情况是 FOO_INT,则返回 true
  • int getFooInt(): 如果 oneof 情况是 FOO_INT,则返回 foo 的当前值。否则,返回此字段的默认值。
  • ChoiceCase getChoiceCase(): 返回指示哪个字段已设置的枚举。如果未设置任何字段,则返回 CHOICE_NOT_SET

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

  • Builder setFooInt(int value): 将 Foo 设置为此值,并将 oneof 情况设置为 FOO_INT。调用此方法后,hasFooInt() 将返回 truegetFooInt() 将返回 valuegetChoiceCase() 将返回 FOO_INT
  • Builder clearFooInt():
    • 如果 oneof 情况不是 FOO_INT,则不会有任何更改。
    • 如果 oneof 情况是 FOO_INT,则将 Foo 设置为 null,并将 oneof 情况设置为 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 引用可能会因后续对 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;
}

协议缓冲区编译器将生成一个名为 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 实现。

Stub

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

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

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

stub 还将服务的每个方法实现为围绕 channel 的包装器。调用其中一个方法仅调用 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 的 stub 实现,该实现向特定的 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 的未来版本中可能会发生变化。

工具类

协议缓冲区提供了用于消息比较、JSON 转换以及处理周知类型(用于常见用例的预定义协议缓冲区消息)工具类