本页面准确说明了协议缓冲区编译器针对任何给定协议定义生成的 Java 代码。proto2 和 proto3 生成的代码之间的任何差异都会突出显示 - 请注意,这些差异位于生成的代码中,如本文档中所述,而不是在两个版本中相同的基本消息类/接口。在阅读本文档之前,您应阅读 proto2 语言指南和/或 proto3 语言指南。
请注意,除非另行指定,否则所有 Java 协议缓冲区方法都不接受或返回 null。
编译器调用
协议缓冲区编译器在使用 --java_out=
命令行 flag 调用时会生成 Java 输出。--java_out=
选项的参数是您希望编译器写入 Java 输出的目录。对于每个 .proto
文件输入,编译器都会创建一个封装容器 .java
文件,其中包含一个表示 .proto
文件本身的 Java 类。
如果 .proto
文件包含如下所示的行:
option java_multiple_files = true;
然后,编译器还会为 .proto
文件中声明的每个顶级消息、枚举和服务创建一个单独的 .java
文件。
否则(即,当 java_multiple_files
选项为 false 时,该值为默认值),上述封装容器类也会用作外部类,并且 .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/com
和 build/gen/com/example
目录。不过,它不会创建 build/gen
或 build
;它们必须已存在。您可以在一次调用中指定多个 .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
声明不应以反向域名开头。
信息
假设有一个简单的消息声明:
message Foo {}
协议缓冲区编译器会生成一个名为 Foo
的类,该类会实现 Message
接口。该类已声明为 final
;不允许进一步创建子类。Foo
扩展了 GeneratedMessage
,但这应被视为实现细节。默认情况下,Foo
会将 GeneratedMessage
的许多方法替换为专用版本,以最大限度地提高速度。不过,如果 .proto
文件包含以下代码行:
option optimize_for = CODE_SIZE;
那么 Foo
将仅替换正常运行所需的最少的方法,并依赖其余方法基于 GeneratedMessage
的实现。这会显著缩减所生成代码的大小,但也会降低性能。
或者,如果 protoc 使用的 Java 输出目录带有 lite:
前缀(例如 --java_out=lite:project/protos
),则 Foo
将包含所有方法的快速实现,但会实现 MessageLite
接口,后者仅包含 Message
方法的子集。特别是,它不支持描述符或反射。不过,在此模式下,生成的代码只需要链接到 libprotobuf-lite.jar
(而非 libprotobuf.jar
)。
“精简版”库比完整库小得多,而且更适合用于手机等资源有限的系统。
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
,该类可用于构建 Foo
。Foo.Builder
可实现 Message.Builder
接口。它扩展了 GeneratedMessage.Builder
类,但再次将此视为实现细节。与 Foo
一样,Foo.Builder
可能依赖于 GeneratedMessage.Builder
中的通用方法实现,或者在使用 optimize_for
选项时,可以更快地生成自定义代码。
Foo.Builder
未定义任何静态方法。它的接口与 Message.Builder
接口的定义完全相同,但返回类型较为具体:Foo.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
。
在一些特殊情况中,如果方法名称与 Java 中的预留字词或 protobuf 库中定义的方法冲突,系统会附加额外的下划线。例如,名为 class
的字段的 getter 为 getClass_
,以避免与 java.lang.Object
中的 getClass
方法冲突。
与访问器方法一样,编译器会为包含其字段编号的每个字段生成一个整数常量。常量名称是转换为大写后跟 _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 foo = 1;
编译器会在消息类及其构建器中生成以下访问器方法:
int getFooCount()
:返回当前字段中的元素数量。String getFoo(int index)
:返回位于指定索引(从零开始)处的元素。ProtocolStringList getFooList()
:以ProtocolStringList
的形式返回整个字段。如果该字段未设置,则返回空列表。
编译器仅会在消息的构建器中生成以下方法:
Builder setFoo(int index, String value)
:设置元素在给定索引(从零开始)处的值。Builder addFoo(String value)
:将具有指定值的字段附加新元素。Builder addAllFoo(Iterable<? extends String> value)
:将给定Iterable
中的所有元素附加到该字段。Builder clearFoo()
:从该字段中移除所有元素。调用此方法后,getFooCount()
将返回零。
对于其他简单字段类型,将根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,类型是消息或枚举类。
重复的嵌入式消息字段
对于消息类型,setFoo()
和 addFoo()
也接受消息的构建器类型的实例作为参数。这只是一个快捷方式,相当于对构建器调用了 .build()
并将结果传递给了该方法。此外,还有其他生成的方法:
Builder addFoo(int index, Field value)
:在指定的从零开始的索引处插入新元素。将当前在该位置处的元素(如果有)向右移动任何后续元素(向其索引添加 1 个元素)。
此外,对于消息类型,编译器会在消息类及其构建器中生成以下额外的访问器方法,让您可以访问相关的子构建器:
FooOrBuilder getFooOrBuilder(int index)
:返回指定元素的构建器(如果有),如果不存在,则返回该元素。如果从消息类调用此函数,则始终会返回消息,而不是构建器。在构建器上调用此方法不会为该字段创建子构建器。List<FooOrBuilder> getFooOrBuilderList()
:将整个字段作为不可修改的构建器(如果有)列表返回,否则返回消息。如果从消息类中调用该对象,则将始终返回不可变的消息列表,而不是不可修改的构建器列表。
编译器仅会在消息的构建器中生成以下方法:
Builder getFooBuilder(int index)
:返回指定索引处元素的构建器。Builder addFooBuilder(int index)
:为指定索引处的默认消息实例插入并返回构建器。现有条目会移至较高的索引,以便为插入的构建器腾出空间。Builder addFooBuilder()
:为默认消息实例附加并返回构建器。Builder removeFoo(int index)
:移除位于指定索引(从零开始)处的元素。List<Builder> getFooBuilderList()
:将整个字段作为不可修改的构建器列表返回。
重复的枚举字段(仅限 proto3)
编译器会在消息类及其构建器中生成以下附加方法:
int getFooValue(int index)
:返回指定索引处的枚举整数值。List<java.lang.Integer> getFooValueList()
:以整数列表的形式返回整个字段。
编译器仅会在消息的构建器中生成以下附加方法:
Builder setFooValue(int index, int value)
:设置指定索引处的枚举整数值。
名称冲突
如果另一个非重复字段的名称与某个重复字段生成的方法冲突,那么这两个字段名称都会附加其 protobuf 字段编号。
对于这些字段定义:
int32 foo_count = 1; repeated string foo = 2;
编译器会先将其重命名为以下内容:
int32 foo_count_1 = 1; repeated string foo_2 = 2;随后,系统将按照上述方式生成访问器方法。
单字段
对于此一次性字段定义:
oneof example_name { int32 foo = 1; ... }
编译器会在消息类及其构建器中生成以下访问器方法:
boolean hasFoo()
(仅限 proto2):如果其中之一为FOO
,则返回true
。int getFoo()
:如果任一项为FOO
,则返回example_name
的当前值。否则,返回此字段的默认值。
编译器仅会在消息的构建器中生成以下方法:
Builder setFoo(int value)
:将example_name
设置为此值,并将单例设置为FOO
。调用此函数后,hasFoo()
将返回true
,getFoo()
将返回value
,getExampleNameCase()
将返回FOO
。Builder clearFoo()
:- 如果此单一请求的情况不是
FOO
,则不会发生任何变化。 - 如果 oneof case 为
FOO
,则将example_name
设置为 null 并将 oneof 大小写设置为EXAMPLENAME_NOT_SET
。调用此方法后,hasFoo()
将返回false
,getFoo()
将返回默认值,getExampleNameCase()
将返回EXAMPLENAME_NOT_SET
。
- 如果此单一请求的情况不是
对于其他简单的字段类型,请根据标量值类型表选择相应的 Java 类型。对于消息和枚举类型,系统会将值类型替换为消息或枚举类。
映射字段
对于此映射字段定义:
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
。请注意,多次调用该方法可能会返回不同的地图实例。 返回的地图引用可能会因任何对 Builder 的后续方法调用而失效。
不限
假设有一个 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; }
Oneof
某个如下定义:
oneof example_name { int32 foo_int = 4; string foo_string = 9; ... }
example_name
其中的所有字段都将使用单个私有字段作为其值。此外,协议缓冲区编译器将为上述一种情况生成 Java 枚举类型,如下所示:
public enum ExampleNameCase implements com.google.protobuf.Internal.EnumLite { FOO_INT(4), FOO_STRING(9), ... EXAMPLENAME_NOT_SET(0); ... };
此枚举类型的值具有以下特殊方法:
int getNumber()
:返回 .proto 文件中定义的对象的数值。static ExampleNameCase forNumber(int value)
:返回与给定数值对应的枚举对象(对于其他数值,返回null
)。
编译器还会在消息类及其构建器中生成以下访问器方法:
ExampleNameCase getExampleNameCase()
:返回指示要设置哪个字段的枚举。如果未设置任何值,则返回EXAMPLENAME_NOT_SET
。
编译器仅会在消息的构建器中生成以下方法:
Builder clearExampleName()
:将 oneof 的私有字段重置为 null,并将 oneof 大小写设置为EXAMPLENAME_NOT_SET
。
枚举
假设有如下枚举定义:
enum Foo { A = 0; B = 5; 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; }
在这种情况下,BAZ
是 BAR
的同义词。在 Java 中,BAZ
将被定义为一个静态最终字段,如下所示:
static final Foo BAZ = BAR;
因此,BAR
和 BAZ
是相等的,BAZ
绝不应出现在 Switch 语句中。编译器始终选择用给定数值定义的第一个符号作为该符号的“规范”版本;所有后续编号相同的符号都只是别名。
可以定义消息类型中嵌套的枚举。编译器会生成嵌套在消息类型的类中的 Java 枚举定义。
扩展(仅限 proto2)
假设一条具有扩展范围的消息:
message Foo { extensions 100 to 199; }
协议缓冲区编译器将使 Foo
扩展 GeneratedMessage.ExtendableMessage
,而不是常见的 GeneratedMessage
。同样,Foo
的构建器将扩展 GeneratedMessage.ExtendableBuilder
。切勿按名称引用这些基本类型(GeneratedMessage
被视为实现细节)。不过,这些父类定义了许多可用于操纵扩展的额外方法。
具体而言,Foo
和 Foo.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; } }
在此例中,在 Baz
的声明中声明了标识符为 foo_ext
且类型为 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
参数是隐含参数,并且 request
和 done
指定了其确切类型。
Foo
是 Service
接口的子类。协议缓冲区编译器会自动生成 Service
方法的实现,如下所示:
getDescriptorForType
:返回服务的ServiceDescriptor
。callMethod
:根据提供的方法描述符确定调用的是哪个方法,并直接调用该方法,从而将请求消息和回调类型转换为正确的类型。getRequestPrototype
和getResponsePrototype
:返回指定方法的请求类型或正确类型的默认响应实例。
还会生成以下静态方法:
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
协议缓冲区编译器还会为每个服务接口生成一个“存根”实现,供希望向已实现服务的服务器发送请求的客户端使用。对于 Foo
服务(如上所示),桩实现 Foo.Stub
将被定义为嵌套类。
Foo.Stub
是 Foo
的子类,该子类也实现了以下方法:
Foo.Stub(RpcChannel channel)
:构建用于在指定通道上发送请求的新桩。RpcChannel getChannel()
:返回此桩的渠道,即传递给构造函数。
此外,桩还会将服务的每个方法实现为渠道的封装容器。调用其中一个方法只会调用 channel.callMethod()
。
协议缓冲区库不包含 RPC 实现。不过,它包含将生成的服务类挂接到您选择的任意 RPC 实现所需的所有工具。您只需提供 RpcChannel
和 RpcController
的实现即可。
阻塞接口
上述 RPC 类都存在非阻塞语义:当您调用方法时,您需要提供一个回调对象,该对象将在方法完成后调用。通常情况下,使用阻塞语义编写代码会更加容易(但可能伸缩性较差),在这种情况下,该方法只有在完成之后才会返回。为了适应这种情况,协议缓冲区编译器还会生成服务类的屏蔽版本。Foo.BlockingInterface
相当于 Foo.Interface
,不过每种方法只是返回结果,而不是调用回调。例如,bar
定义为:
abstract FooResponse bar(RpcController controller, FooRequest request) throws ServiceException;
与非阻塞服务类似,Foo.newReflectiveBlockingService(Foo.BlockingInterface)
会返回封装了一些 Foo.BlockingInterface
的 BlockingService
。最后,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
),以确保涵盖所有用例。
请勿生成依赖标准代码生成器声明的私有类成员的代码,因为这些实现细节在未来的协议缓冲区版本中可能会发生变化。
实用程序类
协议缓冲区为消息比较、JSON 转换以及常见类型(常见用例的预定义协议缓冲区消息)提供实用程序类。