您的位置:首页 > 编程语言 > Java开发

gRPC Java代码生成

2016-08-24 14:30 676 查看
本文讲述protocol buffer编译器会由协议定义文件生成什么样的代码。proto2和proto3的区别将被高亮——注意,本文说的是存在于生成代码中的区别,不是基本的消息类/接口——它们在这两个版本中是一样的。在开始本文之前,你应该先看一下proto2的语言指南和proto3语言指南。

编译器调用

Protocol buffer 编译器在遇到 –java_out= 命令行标识时会产生Java输出。

–java_out=选项参数指定编译器的输出目录。编译器为每一个.proto文件创建一个.java文件。这个文件包含一个java外部类,其中定义了一些基于.proto文件中声明的内部类和一些静态字段。

外部类的名产生规则:如果.proto文件包含了如下的一行:

option java_outer_classname = "Foo";


外部类的名字将会是 Foo。否则,外部类的名字会变成由.proto文件名转换而成的驼峰形式。例如,foo_bar.proto将会变成FooBar。如果该文件中已经有一个消息有同样的名字,“OuterClass”将会被追加到外部类的名字后边。例如,如果foo_bar.proto包含一个叫FooBar的消息,外部类会变成FooBarOuterClass。

Java的包名会由下边的Package来决定。

输出什么样的文件完全由–java_out选项,包名 ( .s 被替换成 /s),和.java文件名来决定。

举个例子,你可以像下边这样调用编译器:

protoc --proto_path=src --java_out=build/gen src/foo.proto


如果foo.proto的java包是com.example,它的外部类名是FooProtos,那么protocol buffer编译器会生成文件:build/gen/com/example/FooProtos.java。Protocol buffer编译器将会自动创建build/gen/com/example目录,如果需要的话。但是,它不会创建build/gen或者build目录;它们必须已经存在。你可以在单个命令调用中指定多个.proto文件;所有输出文件会一次生成。

protoc的使用:

$ protoc.exe -h
Usage: D:\DEV\grpc\protoc-3.0.0-win32\bin\protoc.exe [OPTION] PROTO_FILES
Parse PROTO_FILES and generate output based on the options given:
-IPATH, --proto_path=PATH   Specify the directory in which to search for
imports.  May be specified multiple times;
directories will be searched in order.  If not
given, the current working directory is used.
--version                   Show version info and exit.
-h, --help                  Show this text and exit.
--encode=MESSAGE_TYPE       Read a text-format message of the given type
from standard input and write it in binary
to standard output.  The message type must
be defined in PROTO_FILES or their imports.
--decode=MESSAGE_TYPE       Read a binary message of the given type from
standard input and write it in text format
to standard output.  The message type must
be defined in PROTO_FILES or their imports.
--decode_raw                Read an arbitrary protocol message from
standard input and write the raw tag/value
pairs in text format to standard output.  No
PROTO_FILES should be given when using this
flag.
-oFILE,                     Writes a FileDescriptorSet (a protocol buffer,
--descriptor_set_out=FILE defined in descriptor.proto) containing all of
the input files to FILE.
--include_imports           When using --descriptor_set_out, also include
all dependencies of the input files in the
set, so that the set is self-contained.
--include_source_info       When using --descriptor_set_out, do not strip
SourceCodeInfo from the FileDescriptorProto.
This results in vastly larger descriptors that
include information about the original
location of each decl in the source file as
well as surrounding comments.
--dependency_out=FILE       Write a dependency output file in the format
expected by make. This writes the transitive
set of input file paths to FILE
--error_format=FORMAT       Set the format in which to print errors.
FORMAT may be 'gcc' (the default) or 'msvs'
(Microsoft Visual Studio format).
--print_free_field_numbers  Print the free field numbers of the messages
defined in the given proto files. Groups share
the same field number space with the parent
message. Extension ranges are counted as
occupied fields numbers.

--plugin=EXECUTABLE         Specifies a plugin executable to use.
Normally, protoc searches the PATH for
plugins, but you may specify additional
executables not in the path using this flag.
Additionally, EXECUTABLE may be of the form
NAME=PATH, in which case the given plugin name
is mapped to the given executable even if
the executable's own name differs.
--cpp_out=OUT_DIR           Generate C++ header and source.
--csharp_out=OUT_DIR        Generate C# source file.
--java_out=OUT_DIR          Generate Java source file.
--javanano_out=OUT_DIR      Generate Java Nano source file.
--js_out=OUT_DIR            Generate JavaScript source.
--objc_out=OUT_DIR          Generate Objective C header and source.
--python_out=OUT_DIR        Generate Python source file.
--ruby_out=OUT_DIR          Generate Ruby source file.


包——Packages

生成的类会放到基于java_package选项的Java包路径下。如果java_package选项省略,package声明会被使用。

例如,.proto文件包含了:

package foo.bar;


生成的java文件会被放到Java 包foo.bar中。然而,如果.proto文件也包含了一个java_package选项,如下:

package foo.bar;
option java_package = "com.example.foo.bar";


java文件会放到com.example.foo.bar包中。由于正常的.proto package声明不能以一个倒序的域名作为开始,才会有一个java_package选项。

消息——Messages

写一个最简单的消息声明:

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.jar链接到libprotobuf-lite.jar即可。“lite”库比完整库小得多,也更适合资源受限的系统比如手机端。

Message接口定义的方法让你可以检查,操作,读写整个消息。除了这些方法,Foo类定义了如下的静态方法:

static Foo getDefaultInstance()
: 返回Foo的一个单例,如果你调用
Foo.newBuilder().build()
,其结果是一样的 (因此所有的单个的字段都是未赋值的,所有的重复字段都是空的)。请注意,一个消息的默认实例可以被当做一个工厂,可以调用它(实例对象)的newBuilderForType()方法。



static Descriptor getDescriptor():
返回消息类型的描述器。它包含类型的信息,包括它有哪些字段和字段类型。可以通过调用Message的反射方法,比如getField()。这个返回的descriptor是HelloWorldProto类的一个实例。



static Foo Foo parseFrom(...)
: 从给的资源解析消息类型Foo并返回。在Message.Builder接口中都有一个parseFrom方法对应于一个mergeFrom。请注意,parseFrom() 从不抛出UninitializedMessageException;它抛出InvalidProtocolBufferException,如果解析的消息丢失要求的字段。这使得它和调用
Foo.newBuilder().mergeFrom(...).build()
有细微的差别。





static Parser parser()
: 返回一个Parser的实例,它实现了各种parseFrom()接口。



Foo.Builder newBuilder()
: 创建一个新的builder



Foo.Builder newBuilder(Foo prototype)
: 使用和prototype的同样的字段值来创建一个新的builder。由于内嵌的消息和字符对象是不可变的,他们在原始和副本之间共享。

见上图

构造器——Builders



消息对象——比如上述Foo类的实例——是不可变的,就像Java中的String。要构造一个消息对象,你需要使用一个构造器。每个消息类都有他自己的构造类——因此在你的Foo例子中,protocol buffer编译器生成了一个内嵌的Foo.Builder类,它可被用来构造一个Foo对象。Foo.Builder实现了Message.Builder 接口。它继承了GeneratedMessage.Builder类,但是,再次强调,它不是一个具体实现。和Foo一样,Foo.Builder 可能会依赖GeneratedMessage.Builder中的通用方法实现,当optimize_for选项使用时,生成的自定义代码会更快。

Foo.Builder不会定义任何静态方法。它的接口完全由Message.Builder接口定义,不同的是,返回类型更具体:Foo.Builder中修改builder的方法返回类型为Foo.Builder,而build()返回类型Foo。

修改builder内容的方法——包括字段的setter方法——总是返回一个builder的引用(例如它们 “return this;”)。这允许多个方法在一行中进行链式调用。比如:

builder.mergeFrom(obj).setFoo(1).setBar("abc").clearBaz();


子构造器——Sub Builders

由于消息包含子消息,编译器也可以生成子构造器。这允许你重复修改深度内嵌的子消息而不用重新构造他们。例如:

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();


内嵌类型——Nested Types

一个消息可以被声明在另外一个消息内部。比如:

message Foo { message Bar { } }


这种情况下,编译器会简单生成Bars作为内嵌在Foo中的内部类

字段——Fields

除了上一部分描述的方法,protocol buffer编译器为每个定义在.proto文件的message中的字段生成一系列访问的方法。这些方法读取message类和它响应的builder中都会定义的字段值;这些方法修改只修改定义在builder中字段值。

注意:这些方法名字总是使用驼峰命名规则,即使在.proto文件中的字段名使用小写和下划线(_)组合。大小写转换流程如下:

对于名字中的每个下划线,下划线被干掉,后续的字母使用大写。

如果名字有一个前缀(比如”get”),首字母大写,否则小写。

这样,字段foo.bar_baz变成fooBarBaz。如果前缀是get,它会变成getFooBarBaz。

和访问方法一样,编译器为每一个字段(包括字段标识)生成一个整形常量。常量名字是字段名转换成大写后边跟一个_FIELD_NUMBER。例如, 给定一个字段可选的
int32 foo_bar = 5;
,编译器会生成常量

public static final int FOO_BAR_FIELD_NUMBER = 5;.


单个字段——Singular Fields (proto3)

对于一个字段定义:

int32 foo = 1;


编译器会同时在消息类和它的builder中生成如下的访问方法:

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


编译器只会在message的builder中生成如下的方法:

Builder setFoo(int value)
: 设置字段的值。调用之后,
hasFoo()
将会返回
true
getFoo()
将会返回
value


Builder clearFoo()
: 清除字段的值。调用之后,
getFoo()
将会返回字段类型的默认值。

对于其他简单字段类型,相应的Java类型会根据scalar value表进行选择。对于消息和枚举类型,其值类型会被消息或者枚举类型的类替换掉。

内嵌消息字段——Embedded Message Fields

对于消息字段类型,
setFoo()
也接受一个消息构造器类型的实例作为参数。这仅是构造器的
.build()
调用的快捷方式,结果回传给setFoo()方法。

如果字段未设值,
getFoo()
会返回一个带有字段集合的Foo实例(可能会是由
Foo.getDefaultInstance()返回
的实例)。

另外,编译器生成两个访问器方法,允许你访问相关的消息的子构建器。下列方法由消息类和它的构造器生成:

FooOrBuilder getFooOrBuilder()
: 返回字段的构造器,如果它已存在,或者消息不存在。

编译器生成仅在消息的构造器中生成如下的方法:

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

枚举字段——Enum Fields

对于枚举字段类型,一个额外的访问器方法会由消息类和它的构造器生成:

int getFooValue()
: 返回枚举的整型值

编译器仅会在消息的构造器中生成如下的额外方法:

Builder setFooValue(int value): 设置枚举的整形值。

另外,
getFoo()
会返回
UNRECOGNIZED
如果枚举类型是未知的——这是一个由proto3编译器为生成的枚举类相关添加的特殊额外的值。

重复字段——Repeated Fields

对于这个字段定义:

repeated int32 foo = 1;


编译器会同时在message类和它的builder中生成如下的访问方法:

int getFooCount(): 返回当前字段中的元素个数。

int getFoo(int index): 返回0为起点的索引处的元素

List getFooList(): 以列表的形式返回整个字段。如果字段未设置,返回空的列表。对于message类和返回的列表是不可变的(immutable),对于message builer类,是不可修改的(unmodifiable)。

编译器会在message的builder中生成如下的方法:

Builder setFoo(int index, int value):
设置以0起始的索引处的元素的值

Builder addFoo(int value):
用给定的值为子弹追加新元素。

Builder addAllFoo(Iterable<? extends Integer> value): 使用
给定的Iterable追加新元素到字段中。

Builder clearFoo():
从field中删除所有元素。此方法调用后,getFooCount() 会返回零。

对于其他简单的字段类型,相应的Java类型会根据scalar 值类型表进行选择。对于消息和枚举类型,类型是消息或者枚举类。

重复内嵌字段——Repeated Embedded Message Fields

对于消息类型,setFoo() and addFoo()也会接受一个消息的builder 类型作为参数。这仅是builder的
.build()
调用的快捷方式,结果回传给调用方法。

另外,编译器会在messge类和它的builder中,生成如下的额外访问器方法,允许你访问相关的子构造器:

FooOrBuilder getFooOrBuilder(int index)
: 返回指定元素的builder,如果它已经存在,或者元素不存在。如果这是由一个message类调用,它会总是返回一个消息而不是一个builder。

List<FooOrBuilder> getFooOrBuilderList()
: 以一个不可修改(unmodifiable)列表的形式返回builder(如果可用)或者message(如果builder不可用)的整个字段。如果这个是由一个message类来调用的,它会总是返回一个消息的不可变列表而不是builder是的一个不可修改列表。

编译器仅会在消息的builder中生成如下的方法:

Builder getFooBuilder(int index)
: 返回指定索引处元素的builder。

Builder addFooBuilder(int index)
: 在指定索引处追加一个默认消息实例的builder并返回

Builder addFooBuilder()
: 追加一个默认消息实例的builder。

Builder removeFoo(int index)
: 删除以0为起始的索引处的元素

List<Builder> getFooBuilderList()
: 以一个不可修改的builder的列表返回整个字段。

重复枚举类型——Repeated Enum Fields (仅proto3 )

编译器会同时在message类和它的builder中生成如下额外的方法:

int getFooValue(int index):
返回指定索引处的枚举类型的值。

List getFooValueList():
以整数列表的形式返回整个字段。

编译器仅会在消息builder中生成如下额外的方法:

Builder setFooValue(int index, int value)
: 为指定索引出的枚举类型设置整形变量

Oneof 字段

对于oneof字段的定义:

oneof oneof_name {
int32 foo = 1;
...
}


编译器会同时在message类和它的builder中生成如下额外的方法:

boolean hasFoo() (proto2 only): 如果oneof case是FOO,返回true

int getFoo(): 如果oneof case 是FOO则返回oneof_name的当前值。否则返回这个字段的默认值。

编译器仅会在消息builder中生成如下额外的方法:

Builder setFoo(int value): 设置oneof_name为给定值,并设置oneof case为FOO。当调用这个方法的时候,hasFoo()将会返回true,getFoo()将会返回value代表的值,getOneofNameCase()返回FOO。

Builder clearFoo(): 如果oneof case不是FOO,不改变任何东西。如果oneof case是FOO,设置oneof_name为null,oneof case 为
ONEOF_NAME_NOT_SET
。调用之后, hasFoo()会返回false,getFoo() will将会返回默认值,getOneofNameCase()将会返回
ONEOF_NAME_NOT_SET


对于其他简单字段类型,相应的java类型根据scalar 值类型表进行选择。 对于消息和枚举类型,值类型会被message或者enum类替换。

Map 字段

对于map类型字段的定义:

map<int32, int32> weight = 1;


编译器会同时在message类和它的builder中生成如下额外的方法:

Map<Integer, Integer> getWeightMap()
: 返回不可修改的Map。

int getWeightOrDefault(int key, int default)
: 返回给定的key对应的value,如果不存在,则返回默认值。

int getWeightOrThrow(int key)
: 返回给定的key对应的value,如果不存在,则抛出异常
IllegalArgumentException


boolean containsWeight(int key)
: 指出field中给定的key是否存在。

int getWeightCount()
: 返回map中元素的个数。

编译器仅会在消息builder中生成如下额外的方法:

Builder putWeight(int key, int value)
: 添加权重到这个字段。

Builder putAllWeight(Map<Integer, Integer> value)
: 添加给定map中所有的entries到这个字段中。

Builder removeWeight(int key)
: 从这个字段中删除权重。

Builder clearWeight()
: 从这个字段中删除所有权重。

@Deprecated Map<Integer, Integer> getMutableWeight()
: 返回一个可变的map. 注意,多次调用这个方法可能返回不同的map实例。返回的map引用可能因任何后续的到Builder的方法调用而失效。

Any

Any 字段定义像这样:

import "google/protobuf/any.proto";

message ErrorStatus {
string message = 1;
google.protobuf.Any details = 2;
}


在我们生成的代码中,具体字段的getter方法返回一个com.google.protobuf.Any的实例。提供了如下的pack和unpackAny类型值的特殊方法:

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;
}


Oneofs

oneofs定义:

oneof oneof_name {
int32 foo_int = 4;
string foo_string = 9;
...
}


所有在oneof_name oneof中的字段将会使用共享的oneof_name对象作为它们的值。另外,protocol buffer编译器会为oneof case生成Java枚举类型,如下:

public enum OneofNameCase
implements com.google.protobuf.Internal.EnumLite {
FOO_INT(4),
FOO_STRING(9),
...
ONEOF_NAME_NOT_SET(0);
...
};


枚举类型的值会有如下的特殊方法:

int getNumber()
: 返回与.proto文件中定义一样的对象的数值类型的值

static OneofNameCase forNumber(int value)
:根据给定的数值返回枚举对象。

编译器会同时在message类和它的builder中生成如下额外的方法:

OneofNameCase getOneofNameCase()
: 返回枚举指明哪个字段是集合。如果他们都都不是集合,返回ONEOF_NAME_NOT_SET。

编译器仅会在消息builder中生成如下额外的方法:

Builder clearOneofName()
: 设置oneof_name为null,设置oneof case为ONEOF_NAME_NOT_SET。

枚举

枚举定义:

enum Foo {
VALUE_A = 0;
VALUE_B = 5;
VALUE_C = 1234;
}


服务——Services

如果.proto文件包含如下行:

option java_generic_services = true;


那么,protocol buffer 编译器会根据在.proto中定义的服务生成代码。然而,生成的代码可能会不好用,因为它它未绑定到任何特定的RCP系统,这样的话,绑定到一个RPC系统会需要更多层次的间接的代码调整。如果你不想生成代码,把这行添加到.proto文件中:

option java_generic_services = false;


如果上边的语句未给出,这个选项默认是false,因为通用服务已经过时。(注: 在2.4.0版本之前, 这个选项默认是true)

基于.proto-语言服务定义的RPC系统,应该提供一个插件用来生成适用于该系统的代码。这些插件会要求抽象服务是disabled,这样他们才能生成他们自己同名的类。插件在2.3.0 (January 2010)版本中是新的。

剩下的章节描述了当抽象服务是enable的情况下protocol buffer编译器会生成什么东西。

接口——Interface

给一个服务定义:

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


protocol buffer编译器会生成抽象类Foo来表示这个服务。对于定义在服务中的每个方法,Foo类都会有一个抽象方法与之对应。这种情况下,Bar方法是这样定义的:

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


参数和 Service.CallMethod()的参数是等同的,除了方法参数是隐式,
request
done
都明确地指定了类型。

Foo实现了Service 接口。Protocol buffer 编译器自动生成如下接口方法的实现:

getDescriptorForType
: 返回服务的
ServiceDescriptor


callMethod
: 基于提供的方法描述符决定哪个方式被调用,然后直接调用该方法,向下转型请求消息和回调为正确的类型。

getRequestPrototype and getRequestPrototype
: 为给定的方法返回正确类型的请求或者响应的默认实例

如下的静态方法也被生成:

static ServiceDescriptor getDescriptor()
: 返回这个类型的描述符,它包含了关于这个服务中有哪些方法以及方法的输入和输出类型等信息。

Foo 还包含了一个内嵌的接口
Foo.Interface
。这个是一个纯接口,再一次包含了定义于你的服务中的每个方法。然而,这个接口不继承自Service 接口。这个问题是由于RPC服务器实现,通常会用抽象服务的对象写,而不是你特定服务。为了解决这个问题,如果你有一个
object impl
实现了
Foo.Interface
,你可以调用
Foo.newReflectiveService(impl)


来构建一个作为impl的简单代理的Foo类的实例,并且实现服务。

总括来说, 当实现你自己的服务的时候,你有两个选项:

继承Foo,然后视情况而定,实现它的方法,然后持有直接到RPC服务器实现的子类的实例。这通常很简单,但是有些人认为它不够“纯净”。

实现Foo.Interface接口,使用
Foo.newReflectiveService(Foo.Interface)
来构造一个
Service wrapping
,然后传递这个
wrapper
到你的RPC实现。

存根——Stub

Protocol buffer编译器也会为每一个服务接口生成“存根”的实现,它被客户机用来发送请求服务端。对于
Foo service
(上述),存根实现
Foo.Stub
会被定义为内部类。

Foo.Stub
是Foo的一个子类,也实现了如下的方法:

Foo.Stub(RpcChannel channel)
: 构造一个通过给定的通道发送请求的新的stub。

RpcChannel getChannel()
: 返回传递给构造函数的当前stub的通道。

Stub包装了通道,实现了每个service定义的方法,对每个方法的调用,最终都是对
channel.callMethod()
的简单调用。

Protocol Buffer 类库不包含RPC的实现。然而,它包含了所有你需要的工具,连接一个生成的服务类到任意RPC实现。你只需要提供
RpcChannel
RpcController
的实现。

阻塞接口——Blocking Interfaces

上述的RPC类都有非阻塞(non-blocking )语义:当你调用一个方法的时候,提供一个方法完成时调用的回调对象。通常编写阻塞(blocking)语义的代码更容易一些(即便扩展性差一些),方法直到完成时才会返回。

为了满足上述要求,Protocol buffer编译器也生成了服务的阻塞版本。
Foo.BlockingInterface
等同于
Foo.Interface
,除非每个方法不是使用回调而只是简单地返回结果。那么,举个例子,bar被定义为:

abstract FooResponse bar(RpcController controller, FooRequest request) throws ServiceException;


和非阻塞服务相似,Foo.newReflectiveBlockingService(Foo.BlockingInterface) 返回一个阻塞的服务,它包装了
Foo.BlockingInterface
。最终,Foo.BlockingStub返回一个
Foo.BlockingInterface的
stub实现,这个接口会发送请求到特定的
BlockingRpcChannel
(阻塞通道)。

插件插入点——Plugin Insertion Points

代码生成插件为了继承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


生成的代码不能包含import语句,因为它们有可能和定义在生成代码中的类型名字冲突。当指向一个外部类的时候,你必须总是是用全路径名称。

注意:对于Java代码生成器,决定输出文件名称的逻辑非常复杂。你可能需要查看protoc的源码,特别是java_headers.cc,确保你考虑到了所有的情形。


注意:不要依赖标准代码生成器声明的私有类属性来生成代码,因为这些实现细节可能在未来的Protocol Buffers的版本中会有改动。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  protobuf gRPC