Protocol Buffer文档翻译:Protocol Buffer基本用法:Java,Protocol Buffer Basics: Java
这个教程,用于向Java 程序员介绍协议缓冲区(protocol buffers)的基本用法。通过建立一个简单示例程序的过程,此教程会向妳展示,如何做以下事情:
•. 在 .proto 文件中定义消息格式。
•.使用protocol buffer编译器。
•.使用Java的protocol buffer应用编程接口来写入及读取消息。
此教程并不是 一个 关于如何 在Java 中使用的protocol buffers 综合指南。 欲获取更详尽的参考信息,则阅读 Protocol Buffer语言指南 、 Java应用编程接口参考 、 Java生成 后的代码指南 和 编码参考 。
我们要使用的示例,是一个狠简单的"地址本"应用程序,它能够将联系人的信息写入到文件中,并且在日后读取回来。地址本中的每个人物,都拥有一个名字、编号、邮件地址和联系电话号码。
妳会如何对这样的结构化数据进行序列化及读取?对于这个问题,有多种解决方法:
•.使用Java自带的序列化功能(Serialization)。这是默认手段,因为,语言里自带了这种功能。但是它存在着一堆广为人知的问题(阅读Josh Bloch 的Effective Java,213 页)。并且,当妳需要与C++或Python 写的程序分享数据时,就不好搞了。
•. 妳可以自己发明一种格式,用来将数据内容编码成一个单个的字符串——例如,将4个整数编码为"12:3:-23:67"。这种方法,狠简单也狠灵活,不过,它需要编写一次性的编码及解码代码,并且,解码过程要耗费一些运行时资源。对于非常简单的数据,这种方式最好。
•. 将数据序列化为XML。这种方式,狠有吸引力,因为,XML是(某种程度上)人眼可读的,并且,狠多语言中都提供了对应的处理库。如果妳想与其它程序/项目共享数据的话,这是一个好方法。然而,XML有一个公认的毛病,那就是,太占用空间,并且,对它进行编码/解码也非常显著地影响到程序的性能。另外,对XML DOM 树进行遍历,也比对一般的类中的字段进行遍历要来得复杂。
Protocol buffers 就是专门为解决这种问题而开发的灵活、高效、自动的解决方案。 在使用 protocol buffers 的过程中, 妳编写一个 .proto 文件,用来描述妳想要存储的数据结构。根据那个文件, protocol buffer编译 器会生成一个类, 这个类会自动 以高效的二进制格式来对那个 protocol buffer 数据结构进行编码 及解码。 所生成的类,会提供针对 该protocol buffer 中 各个字段 的取值方法( getters ) 和 设值方法( setters ),并且 会处理好读取和写入过程中的各个细节,以便将该protocol buffer 作为一个单元来处理。 还有一个重要的特性,那就是, protocol buffer格式支持 妳在日后对该格式进行扩展,使得, 妳的代码仍然能够读取以旧格式编码的数据。
示例代码包含 于源代码压缩包中,具体位于"examples"目录 中。 到这里下载。
要开发妳的地址本程序,首先要创建一个 .proto 文件。 .proto 文件 中的定义是狠简单的 :对于 妳想要序列化的每种数据结构,都加入一个消息定义( message ),然后 ,为该消息中的每个字段指定一个名字和类型。 以下便是本教程中使用的 .proto 文件, addressbook.proto 。
package tutorial;
option java_package = "com.example.tutorial";
option java_outer_classname = "AddressBookProtos";
message Person {
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
message AddressBook {
repeated Person person = 1;
}
妳应该感受到了,它的语法与C++或Java类似。让我们来详细研究一下这个文件中的各个部分,看看它们分别起到什么作用。
.proto 文件 的开头是一个包名声明, 它可以用来避免多个项目之间的命名冲突。 在 Java 中,开头声明 的包名会作为 所生成的Java 包的包名,除非 妳显式地指定一 个 java_package , 我们在此教程里就是这么做的。即使 妳显式地提供了一个 java_package , 妳仍然应当定义一个标准的 package ,以避免在Protocol Buffers 和其它非Java 的语言的命名空间中产生冲突。
包名定义之后 ,可以看到两 个 仅适用于Java 的选项: java_package 和 java_outer_classname 。 java_package ,指定的是,要将生成 的类放置到哪个Java 包中。如果 妳不显式地指定这个,那么, 它就会借用 package 声明 中指定的包名,但是 ,那个地方所声明的名字通常并不适合作为 Java包名使用(因为它们通常 不是以域名开头的 ) 。 java_outer_classname 选项 ,定义的是,用来包含 此文件中定义的所有类的类。如果 妳不显式指定 java_outer_classname ,那么 , 它会将文件名转换成驼峰风格,并作为对应的类名。例如 , "my_proto.proto" ,默认情况下,会使用"MyProto"作为外层类名。
接下来,就是消息定义了。消息,实际 上就是一个包含了一组各种类型的字段的聚合体。 有狠多标准的简单数据类型都可以作为字段类型来使用,包括: bool 、 int32 、 float 、 double 和 string 。 妳还可以将其它 的消息类型作为字段类型加入到妳的消息中—— 在上面的示例中, Person 消息 里包含着 PhoneNumber 消息, 而 AddressBook 消息 中又包含着 Person 消息 。 妳甚至还可以将消息类型嵌套地定义于其它消息内部——在本示例中, PhoneNumber 类型 即是定义于 Person 内部。 妳还可以定义枚举 ( enum ) 类型, 以确保某个字段 只取预定义列表中的某个值—— 本示例中, 我们指定了,电话号码 可以是 MOBILE 、 HOME 或 WORK 。
每个元素中" = 1"、" = 2"的标记,用来指定该字段在二进制编码中使用的唯一标识("tag")。1-15的标记值,要比其它的标记值少占用一个字节的编码空间,因此,作为一种优化手段,妳可以将这些标记值用于常用元素或重复元素,而将16及更大数字的标记值用于较少使用的可选元素。重复字段中的每个元素都需要将其标记值重新编码,因此,重复字段尤其适合于采用这种优化手段。
每个字段,必须使用以下某个修饰符来注解:
•. required : 此字段必须提供,否则 , 该消息会被认为是“未初始化的”("uninitialized")。尝试构造 一个未初始化的消息,则会抛出 RuntimeException 异常。 对一个未初始化的消息进行解析,会抛出 IOException 异常。 除此之外,必选(required)字段与可选(optional)字段的行为完全相同。
•. optional :此字段可以存在,也可以不存在。如果 某个可选(optional)字段的值未被设置, 则, 会使用默认值。对于简单 的类型,妳可以指定自己的默认值, 在本示例中,我们就对电话号码(phone number)的 type 字段指定了默认值。如果 妳未指定默认值,则会使用系统默认值:数字类型 的默认值是0;字符串类型的默认值是空字符串;逻辑类型的默认值是假。对于嵌套 的消息,其默认值一定是 其消息的“默认实例”("default instance")或“原型”("prototype"), 即,任何字段都未设置。调用取值函数 去获取一个未显式设置值的可选(或必选)字段的值,会取到该字段的默认值。
•. repeated : 此字段可以重复任意次数 (包括 0次 ) 。那些 被重复的字段值,会在protocol buffer 中保留原有的顺序。 可以将重复字段当作动态改变尺寸的数组来看待。
必选(Required)属性是永久生效的。妳应当谨慎地将字段标记为必选( required )。如果,有朝一日,妳想要停止写入或发送某个必选字段的话,那么,在尝试将该字段变成可选字段的过程中就会遇到问题——旧的代码会将那些不包含该字段的消息当成是不完整的,因而会拒绝或丢弃对应的消息。妳应当考虑写一些与当前应用程序相关的自定义验证代码来验证消息的完整性,而不是依赖必选字段。Google某些的工程师认为,使用required,其带来的害处比好处要大;它们倾向于只使用optional和repeated。不过,这只是部分人的看法。
在 Protocol Buffer语言指南 ,可以找到一个关于如何编写 .proto 文件的完整指南,包括所有可用的字段类型列表。 不要尝试去寻找类似于类继承的功能—— protocol buffers 不支持这种特性。
现在 ,已经写好了 .proto 。那么,下一个要做的事就是,生成对应 的类, 以便用来读取及写入 AddressBook (当然 还包括 Person 和 PhoneNumber )消息 。 为了完成这件事,妳需要针对妳的 .proto 文件来运行 protocol buffer 的编译器 protoc :
2. 现在 ,运行该编译器,指定以下参数: 源代码目录 ( 妳的应用程序的源代码所在的目录——如果妳未指定的话则会使用当前目录 ) ;目标目录 ( 生成的代码要放置到该目录中;通常与 $SRC_DIR 一致 ) ; 妳的 .proto 文件的路径。 在本示例中,即是:
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
因为 妳想要的是 Java 类,所以指定 --java_out 选项——对于其它 被支持的语言,也提供了类似的选项。
这会在妳指定的目标目录中生成 com/example/tutorial/AddressBookProtos.java 。
让我们来研究一下生成的代码,看看编译 器 为妳创建了哪些 类和方法。打开 AddressBookProtos.java , 妳会发现其中定义了一个 类 AddressBookProtos ,其中 又嵌套定义了若干个类,分别对应 着妳在 addressbook.proto 中定义的每种消息。 每个类,都自带了一个 Builder 类,用于创建那个类的实例。 妳可以下文的 构建 器与消息 小节中了解更多关于构建器(builders)的信息。
消息 和构建器,都拥有针对消息中每个字段的自动生成的访问函数;消息 ,只拥有取值函数( getters ),而构建器,同时拥有取值函数和设值函数(setters)。 以下是 Person 类的某些访问函数 (省略 了具体实现代码以保持简洁 ) :
// required string name = 1;
public boolean hasName();
public String getName();
// required int32 id = 2;
public boolean hasId();
public int getId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
同时, Person.Builder 拥有 同样的取值函数,另外还有设值函数:
// required string name = 1;
public boolean hasName();
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName();
// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();
// repeated .tutorial.Person.PhoneNumber phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);
public Builder addPhone(PhoneNumber value);
public Builder addAllPhone(Iterable<PhoneNumber> value);
public Builder clearPhone();
妳可以看到,对于每个字段,所提供的,实际上就是简单的 JavaBeans风格 的设值及取值函数。另外 ,对于每个单值字段,都有对应的 has 取值函数 ,如果该字段已被设置值,则该函数会返回真(true)。最后 ,每个字段都有一个 clear 方法,用于 将该字段恢复到空白状态。
重复字段 还拥有一些额外的方法 : Count 方法 (返回 该列表的尺寸 );取值函数 和设值函数,用于获取及设置列表 中指定下标的特定元素; add 方法 ,用于将一个新元素追加到列表中; addAll 方法 ,用于将某个容器中所有的元素加入到列表中。
注意 看,这些访问方法都采用了驼峰命名风格 ,尽管对应 的 .proto 文件中使用了小写下划线风格也是如此。 这种转换,是由protocol buffer 编译器自动完成的, 以使得生成的类符合标准的Java 命名风 格习惯。 妳应当一直在 .proto 文件中使用小写下划线风格来命名字段; 这能够确保在所有受支持的语言中都保有良好的命名风格。阅读 风格指南 以了解更多关于良好的 .proto 风格的信息。
若想要详细了解编译 器针对特定的字段定义会生成哪些成员,则阅读 Java生成 的代码参考 。
生成 的代码中,包含一个名为 PhoneType 的Java 5 枚举,并且嵌套到 Person 中:
public static enum PhoneType {
MOBILE( 0 , 0 ),
HOME( 1 , 1 ),
WORK( 2 , 2 ),
;
...
}
而嵌套类 Person.PhoneNumber 呢,如妳所料,是被嵌套到 Person 中。
protocol buffer 编译 器生成的消息类,是只读的( immutable )。 一旦某个消息对象被创建出来了,它就不可再被修改了,就像Java String 一样。 要想构造一个消息,妳必须先构造一个构建器, 将妳想设置的任何字段设置成对应的值,然后调用构建器的 build() 方法。
妳应该注意到了,在构建器中,每个能够修改该消息的方法,都返回另一个构建器实例。所返回的对象,实际上就是妳对其调用方法的同一个构建器对象。此处将它返回,是为了便于在同一行代码中串联式调用多个设值函数。
以下代码,展示了如何创建 Person 实例:
Person john =
Person .newBuilder()
.setId( 1234 )
.setName( "John Doe" )
.setEmail( "jdoe@example.com" )
.addPhone(
Person . PhoneNumber .newBuilder()
.setNumber( "555-4321" )
.setType( Person . PhoneType .HOME))
.build();
每个消息和构建器类,都还包含了一些其它方法,可用来检查或操作整个消息。这些方法包括:
•. isInitialized() :检查是否所有必选字段都已被设置。
•. toString() :返回一个人眼可读的字符串,用于描述该消息,对于调试尤其有用。
•. mergeFrom(Message other) : ( 仅适用于构建器 ) 将 other 中的内容合并到本消息中,具体 就是, 对单值字段进行覆盖,对重复字段进行串接。
•. clear() : ( 仅适用于构建器 ) 将所有字段清除,恢复到空白状态。
这些方法,实现了 Message 和 Message.Builder 接口 ( interfaces ),这两个接口是由所有的 Java消息 和构建器共享的。欲知更多信息 ,则阅读 Message 的 完整应用编程接口文档 。
最后 ,每个 protocol buffer 类,都提供了相应的方法,用来按照protocol buffer 二进制格式 对妳所选择的类型进行消息的写入和读取。 这些方法包括:
•. byte[] toByteArray(); : 将消息序列化,并且返回一个字节数组,其中即包含着序列化之后的原始字节流。
•. static Person parseFrom(byte[] data); :从指定的字节数组中解析出一个消息。
•. void writeTo(OutputStream output); :将消息序列化,并且写入到一个 OutputStream 中。
•. static Person parseFrom(InputStream input); :从 InputStream 中读取并解析出一个消息。
这些,仅仅是提供出来的所有解析及序列化方法中的一部分。同样地,欲查看完整列表,则阅读 Message 的 应用编程接口参考 。
Protocol Buffers与面向对象设计 Protocol buffer类,本质上是一些简单的数据容器(类似于C++中的结构体);它们并不是面向对象模型中的一等公民。如果妳想要向生成的类中加入更丰富的行为,那么,最好的方式是使用某个类来将生成的protocol buffer类封装起来。同时,如果妳并无权控制该.proto文件的设计(例如,妳在复用另一个项目中的文件),那么,将protocol buffers 封装起来也是一个好主意。在那种情况下,妳可以利用该封装类来形成一个更适合该应用程序的独特环境的接口:隐藏某些数据和方法,暴露出某些便利函数,等等。妳不应该通过继承那些生成类的方式来向它们添加行为。这会破坏某些内部机制,同时也并不是一个好的面向对象开发方式。
好了,让我们来试着使用一下妳的protocol buffer 类。妳想要利用地址本程序来做的第一件事就是,向地址本文件中写入人物的详细信息。要实现这一点,妳需要创建那些protocol buffer 类的实例,并且填充其中的数据,然后将它们写入到某个输出流中。
以下这个程序,从文件中读入一个 AddressBook ,根据用户 的输入向其中加入一个新的 Person ,然后 将新的 AddressBook 重新写入到文件中。那些直接调用 或引用由编译 器 生成的代码的部分,已经高亮显示。
import com.example.tutorial. AddressBookProtos . AddressBook;
import com.example.tutorial. AddressBookProtos . Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;
class AddPerson {
// 这个函数,根据用户的输入来填充一个Person 消息。
static Person PromptForAddress ( BufferedReader stdin,
PrintStream stdout) throws IOException {
Person . Builder person = Person .newBuilder() ;
stdout. print ( "Enter person ID: " );
person.setId ( Integer .valueOf(stdin.readLine()));
stdout. print ( "Enter name: " );
person.setName (stdin.readLine());
stdout. print ( "Enter email address (blank for none): " );
String email = stdin.readLine();
if (email.length() > 0 ) {
person.setEmail (email);
}
while ( true ) {
stdout. print ( "Enter a phone number (or leave blank to finish): " );
String number = stdin.readLine();
if (number.length() == 0 ) {
break ;
}
Person . PhoneNumber . Builder phoneNumber =
Person . PhoneNumber .newBuilder().setNumber(number) ;
stdout. print ( "Is this a mobile, home, or work phone? " );
String type = stdin.readLine();
if (type.equals( "mobile" )) {
phoneNumber.setType( Person . PhoneType .MOBILE) ;
} else if (type.equals( "home" )) {
phoneNumber.setType( Person . PhoneType .HOME) ;
} else if (type.equals( "work" )) {
phoneNumber.setType( Person . PhoneType .WORK) ;
} else {
stdout.println( "Unknown phone type. Using default." );
}
person.addPhone(phoneNumber) ;
}
return person.build() ;
}
// 主函数:从文件中读入整个地址本,
// 根据用户 的输入来加入一个联系人,然后将地址本重新写入到同一个文件中。
public static void main( String [] args) throws Exception {
if (args.length != 1 ) {
System .err.println( "Usage: AddPerson ADDRESS_BOOK_FILE" );
System . exit (- 1 );
}
AddressBook . Builder addressBook = AddressBook .newBuilder() ;
// 读入已有的地址本。
try {
addressBook.mergeFrom ( new FileInputStream (args[ 0 ]));
} catch ( FileNotFoundException e) {
System . out .println(args[ 0 ] + ": File not found. Creating a new file." );
}
// 加入 一个地址。
addressBook.addPerson (
PromptForAddress ( new BufferedReader ( new InputStreamReader ( System . in )),
System . out ));
// 将新的地址本重新写入到磁盘中。
FileOutputStream output = new FileOutputStream (args[ 0 ]);
addressBook.build().writeTo (output);
output.close();
}
}
显然,一个地址本,如果妳无法读取其中的内容,那么,它就一点卵用也没有!以下示例代码,从上面示例所创建的文件中读取信息,并且输出到终端。
import com.example.tutorial. AddressBookProtos . AddressBook;
import com.example.tutorial. AddressBookProtos . Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
class ListPeople {
// 遍历AddressBook 中的所有联系人,并且输出它们的信息。
static void Print ( AddressBook addressBook) {
for ( Person person: addressBook.getPersonList()) {
System . out .println( "Person ID: " + person.getId() );
System . out .println( " Name: " + person.getName() );
if (person.hasEmail()) {
System . out .println( " E-mail address: " + person.getEmail() );
}
for ( Person . PhoneNumber phoneNumber : person.getPhoneList() ) {
switch ( phoneNumber.getType() ) {
case MOBILE :
System . out . print ( " Mobile phone #: " );
break ;
case HOME :
System . out . print ( " Home phone #: " );
break ;
case WORK :
System . out . print ( " Work phone #: " );
break ;
}
System . out .println( phoneNumber.getNumber() );
}
}
}
// 主函数: 从文件中读取整个地址本,然后输出其中的所有信息。
public static void main( String [] args) throws Exception {
if (args.length != 1 ) {
System .err.println( "Usage: ListPeople ADDRESS_BOOK_FILE" );
System . exit (- 1 );
}
// 读取已有 的地址本。
AddressBook addressBook =
AddressBook .parseFrom ( new FileInputStream (args[ 0 ]));
Print (addressBook);
}
}
在发布了以妳的protocol buffer 为基础的代码之后,迟早妳会发现,妳想要“改善”该protocol buffer 的定义。如果妳希望新的数据格式向后兼容,同时旧的数据格式向前兼容——妳一定想要实现这种效果——的话,那么,妳需要遵守一些规则。在新版本的protocol buffer 中:
•. 妳 不可以 改变任何已有字段 的标记(tag)数字。
•. 妳 不可以 新加入或删除任何必选(required)字段。
•. 妳 可以 删除可选 (optional)或重复(repeated)字段。
•. 妳 可以 添加 新的可选(optional)或重复(repeated)字段,但是 , 妳必须使用新的标记( tag )数字 ( 也就是说,必须使用之前从未在该 protocol buffer 中使用的标记数字,甚至 连已经被删除的字段的标记值也不能使用 ) 。
(对于 这些规则,有 一些例外 ,不过它们极少被使用。 )
如果 妳严格遵守这些规则,那么,旧代码就能够快乐地读取 新格式的消息,并且忽视 掉任何新字段。对于 旧代码来说,那些 被删除的可选字段,会具有其默认值, 而被删除的重复字段则会是空数组。 新代码也能够透明地读取旧格式的消息。然而 , 请注意, 新添加的可选字段不会出现在旧的消息中,因此 , 妳或者要使用 has_ 来显式地检查它们是否已被设置,或者 就 在 .proto 文件中的标记(tag)数字之后使用 [default = value] 来提供一个有意义的默认值。如果 妳没有为某个可选元素指定默认 值,则, 会使用它的类型所对应的默认值:对于字符串 ,其默认值是空字符串。对于逻辑 值,默认值是假(false)。对于数字类型 ,默认值是0。另外 也要注意,如果妳添加了一个重复字段,那么, 在新代码中,妳无法区分它究竟是被(新代码)留空了还是( 旧代码 )根本就没有设置 值 ,因为 ,它没有 has_ 标记。
Protocol buffers 的用法,并不仅仅限于简单的访问函数和序列化。记得 要阅读 Java应用编程接口参考 ,以探索一下妳能够拿它来做什么用。
消息 类提供的其中一个关键特性是,反射( reflection )。 妳可以对一个消息的各个字段进行遍历,操作它们的值,而无需针对任何特定 的消息类型来编写代码。反射 的一个非常有用的用处就是, 在protocol buffers格式与其它格式之间互相转换,例如XML 或JSON。反射 的一个更高端的用法是, 寻找 相同类型的两个消息之间的不同之处,或者 ,开发 出某种“用于protocol buffers消息的正则式”, 以便利用特定的表达式来匹配特定的消息内容。 发挥妳的想象力吧,妳会发现,Protocol Buffers 可以用来解决那些妳之前都没有预期到的问题!
反射 是作为 Message 和 Message.Builder 接口的一部分而提供的。
绿色蔬菜
豆豆
未知美人
美人即将摔倒
未知美人
未知美人
Your opinionsHxLauncher: Launch Android applications by voice commands