上一章节通过一个例子基本看到了如何使用proto来定义接口,本章就把proto的所有重要内容详细讲解下,内容不多也不难。在某些场景下还是需要一些使用技巧的,在本章的最后笔者会把使用过程中的一些坑分享一下,希望对您有所帮助。PS:proto3和proto2不是完全兼容的,建议用proto3。
开始正文之前先看一下proto的完整定义,整个文件没有必填项,在下面源码的注释中笔者按使用经验标识出了哪些建议一定要写,大体分为三部分:消息头、接口定义、对象定义。完整定义如下:
/*========消息头定义========*/
//可选配置,建议必配,协议,这块与最终的编译相关,现建议采用proto3标准
syntax = "proto3";
//可选配置,建议必配,全名空间,只是proto文件的命名空间,与最终生成的源码无关
package com.zd.baseframework.core.sysrecord;
//可选配置,引入的扩展定义,最常用的就是下面这两个了,如果文中没有使用还是建议不引入
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
//可选配置,建议必配,这三个配置与生成的源码相关
option java_package = "com.zd.baseframework.core.sysrecord.api";
option java_outer_classname = "SysRecordProto";
option java_multiple_files = true;
/*========接口定义========*/
//可选配置,接口定义
service ISysRecordService{
//创建
rpc CreateSysRecord (CreateSysRecordRequest) returns (SysRecordOperatorResponse);
//查询
rpc ListSysRecordByCondition (ListSysRecordRequest) returns (ListSysRecordResponse);
}
/*========对象定义========*/
//可选配置,接口出入参定义
message CreateSysRecordRequest{
google.protobuf.StringValue biz_id = 1;
google.protobuf.Int64Value user_id = 2;
google.protobuf.StringValue track_uid = 3;
google.protobuf.StringValue code = 4;
google.protobuf.StringValue custom_code = 5;
}
message ListSysRecordRequest{
google.protobuf.StringValue biz_id = 1;
google.protobuf.Int64Value user_id = 2;
google.protobuf.Int32Value code = 3;
}
message SysRecordOperatorResponse{
optional int32 status = 1;
optional string message = 2;
}
message ListSysRecordResponse{
repeated SysRecordDto data = 1;
}
//数据传输对象,建议单独定义
message SysRecordDto {
int64 id = 1;
int64 biz_id = 2;
int64 user_id = 3;
string track_uid = 4;
string code = 5;
string custom_code = 6;
int32 state = 7;
google.protobuf.Timestamp code_name = 8;
google.protobuf.Timestamp utime = 9;
}
好,了解完了整体定义,笔者就分几部分详细讲解下各部分的可选择配置有哪些,供读者在实际开发中使用。
以下四个一般是必须要写的:
以个是可选的:
syntax = "proto3"; //指定语法行必须是文件的非空非注释的第一个行,默认是proto2
注意:虽说proto有类似java的extends能力,但不建议这么来做。因为消息的定义是用索引来区分的,后续如果改动了base定义很容易出现问题。所以在冗余代码和继承能力间建议大家选择冗余实现。
//带public的导入可以传导,比如abc,a import b, b import c,这时a不能直接使用c,如果 b import public c则是可以使用的
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
完整定义如下例所示,建议下面的格式做成项目组的一个开发规范:
syntax = "proto3";
package com.zd.baseframework.core.sysrecord;
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
option java_package = "com.zd.baseframework.core.sysrecord.api";
option java_outer_classname = "SysRecordProto";
option java_multiple_files = true;
每个字段由字段限制、字段类型、字段名和编号四部分组成,消息中的每一个字段都有一个独一无二的数值类型的编号。1到15使用一个字节编码,16到2047使用2个字节编码,所以应该将编号1到15留给频繁使用的字段。
普通消息,建议使用
syntax = "proto3";
message SearchRequest {
string query = 1;
}
嵌套类型,不建议使用,原因是可读性差,且源码复杂
//嵌套类型
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
不建议使用,而且在proto3中也不支持默认值设置了。
int32 result_per_page = 3 [default = 10];
因笔者没用过这个设置,如果消息的字段被移除或注释掉,但是使用者可能重复使用字段编码,就有可能导致例如数据损坏、隐私漏洞等问题。一种避免此类问题的方法就是指明这些删除的字段是保留的。如果有用户使用这些字段的编号,protocol buffer编译器会发出告警。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
在初始化时,都会带有默认值。如果没有指定默认值,则会使用系统默认值,对于string默认值为空字符串,对于bool默认值为false,对于数值类型默认值为0,对于enum默认值为定义中的第一个元素,对于repeated默认值为空。
proto Type |
Notes |
Java Type |
Python Type |
Go Type |
double |
double |
float |
float64 |
|
float |
float |
float |
float32 |
|
int32 |
使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 |
int |
int |
int32 |
uint32 |
使用变长编码 |
int |
int/long |
uint32 |
uint64 |
使用变长编码 |
long |
int/long |
uint64 |
sint32 |
使用变长编码,这些编码在负值时比int32高效的多 |
int |
int |
int32 |
sint64 |
使用变长编码,有符号的整型值。编码时比通常的int64高效。 |
long |
int/long |
int64 |
fixed32 |
总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 |
int |
int |
uint32 |
fixed64 |
总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 |
long |
int/long |
uint64 |
sfixed32 |
总是4个字节 |
int |
int |
int32 |
sfixed64 |
总是8个字节 |
long |
int/long |
int64 |
bool |
boolean |
bool |
bool |
|
string |
一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 |
String |
str/unicode |
string |
bytes |
可能包含任意顺序的字节数据。 |
ByteString |
str |
[]byte |
以下的这些头型不一定能用,但可以依照自定义实现:
//以下的这些头型不一定能用,但可以自定义
calendar_period.proto
color.proto
date.proto
datetime.proto
dayofweek.proto
expr.proto
fraction.proto
latlng.proto
money.proto
postal_address.proto
quaternion.proto
timeofday.proto
http_request.proto
以下三种是经常可以用到的:
wrappers.proto //各种基础类型的包装类型
timestamp.proto
annotations.proto//注解
//这处用repeated关键字来实现的
repeated Order ordersList = 3;
key_type可以是除浮点指针或bytes外的其他基本类型,value_type可以是任意类型
map projects = 3;
不太建议使用
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0; #每个枚举类型必须将其第一个类型映射为0
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
设置可选参数allow_alias为true,就可以在枚举结构中使用别名(两个值元素值相同)
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
不太建议使用,因为在程序实现时返参判断特别麻烦。Any可以让你在 proto 文件中使用未定义的类型,具体里面保存什么数据,是在上层业务代码使用的时候决定的,使用 Any 必须导入 import google/protobuf/any.proto
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
不太建议使用,原因同any。Oneof 类似union,如果你的消息中有很多可选字段,而同一个时刻最多仅有其中的一个字段被设置的话,你可以使用oneof来强化这个特性并且节约存储空间。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
流stream接口定义,对应GRPC的流式访问,对于处理大流量数据时非常有用,详细如下:
阻塞型定义 RPC:rpc Search (SearchRequest) returns (SearchResponse);
服务器端流式 RPC:rpc ListFeatures(Rectangle) returns (stream Feature)
客户端流式 RPC:rpc RecordRoute(stream Point) returns (RouteSummary)
双向流式 RPC:rpc RouteChat(stream RouteNote) returns (stream RouteNote)
在java中有时需要把proto对象转为json字符串打印日志,但必须用Google的工具类才可以,代码如下:
String json = JsonFormat.printer().print(dtos);//格式化输出
String json = JsonFormat.printer().omittingInsignificantWhitespace().print(dtos);//去掉换行
需要依赖com.googlecode.protobuf-java-format和protobuf-java-format包。代码如下:
//protobuf对象转换成json:
String jsonFormat = JsonFormat.printToString(SomeProto);
//json转成protobuf对象:
Message.Builder builder =SomeProto.newBuilder();
String jsonFormat = "json字符串";
JsonFormat.merge(jsonFormat, builder);
建议定义接口入参时选择包装类型,这样可以减少由于基本类型默认值问题带来的莫名其妙的错误。因为包装类型的默认值全是null;
建议定义接口出参时选择基础类型,因为结果返回相对来说比较好控制,也更容易维护服务的严谨和稳定性。
proto文件的内容还是比较多的,一行行手写比较慢,复制又容易出错。所以这里可以使用IDEA的Live Template功能定义一个模块,下面是笔者写的一个模块,创建好proto文件后,在空白地方手动输入:proto,再按tab就会自动生成想要的模块了。模板定义如下:
syntax = "proto3";
package universe.core.$fileName$;
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";
option java_package = "net.shukun.universe.core.$fileName$.api";
option java_outer_classname = "$camefileName$Proto";
option java_multiple_files = true;
service I$camefileName$Service{
rpc ReProcess (Request) returns (Response);
}
message Request{
google.protobuf.Int64Value org_id = 1;
google.protobuf.StringValue workflow = 2;
google.protobuf.StringValue case_num = 3;
}
//返回值
message Response{
optional int32 status = 1;
optional string message = 2;
}
上述$$变量地自动替换成真实值。