Protocol Buffer介绍(Java实例)
2013-12-13 11:59
309 查看
本文译自:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN
ProtocolBuffer基础:Java
本指南提供了使用ProtocolBuffer工作的Java编程方法。全文通过一个简单的示例,向你介绍在Java中使用ProtocolBuffer的方法:
1.如何在.proto文件中定义消息格式;
2.如何使用ProtocolBuffer的creates编译器;
3.如何使用JavaProtocol Buffer的API来读写消息。
本文不是在Java中使用ProtocolBuffer的完整指南,更详细的信息请参照以下资料:
Protocol-buffers语言
JavaAPI参考
生成Java代码指南
编码参考
为什么使用ProtocolBuffer
我们使用了一个非常简单的“地址本”应用的例子,这个应用能够从一个文件中读写个人的联系方式信息。在地址本中每个人都有以下信息:姓名、ID、邮件地址、电话号码。
像这样的结构化数据应该如何系列化和恢复呢?以下几种方法能够解决这个问题:
1.使用Java系列化。因为它是内置在编程语言中的,所以是默认的方法,但是由于众所周知的主机问题,并且如果需要在使用不同编程语言(如C++或Python)编写应用程序之间共享数据,这种方式也不会很好的工作。
2.使用特殊的方式把数据项编码到一个单独的字符串中,如把4个整数编码成“12:3:-23:67”。尽管它需要编写一次性的编码和解码代码,但是这种方法简单而灵活,而且运行时解析成本很小。这种方法对于简单数据是最好的。
3.把数据系列化到XML。因为XML是可人类可读的,并且很多编程语言都有对应的功能类库,所以这种方法非常受欢迎。如果你想要跟其他应用程序/项目共享数据,那么这种方法是一个非常好的选择。但是,众所周知,XML是空间密集性的,并且编解码会严重影响应用程序的性能。此外,XML的DOM树导航也比一般的类中的字段导航要复杂的多。
ProtocolBuffer是完全解决这个问题的灵活、高效的自动化解决方案。使用ProtocolBuffer,要先编写一个.proto文件,用这个文件来描述你希望保存的数据结构。然后用ProtocolBuffer编译器创建一个类,这个类用高效的二进制的格式实现了ProtocolBuffer数据的自动编解码。生成的类提供了组成ProtocolBuffer字段的getter和setter方法,以及提供了负责读写一个ProtocolBuffer单位的方法。重要的是,ProtocolBuffer格式支持向后的兼容性,新的代码依然可以读取用旧格式编码的数据。
什么地方可以找到示例代码
示例代码的源代码包,可以直接从这儿下载。
定义协议格式
要创建你的地址本应用程序,需要从编写.proto文件开始。.proto文件的定义很简单:你要在每个想要系列化的数据结构前添加一个message关键字,然后指定消息中每个字段的名称和类型。以下就是你要定义的.proto文件,addressbook.proto:
就像你看到的,语法与C++或Java非常类似,接下来让我们检查一下文件的每个部分,并看一下它们都做了些什么。
.proto文件开始是包声明,它有助于防止不同项目间的命名冲突。除非你明确的指定了java_package
关键字,否则,该包名会被用于生成的Java类文件的包名。即使提供了java_package,依然应该定义一个普通的package,以避免跟ProtocolBuffer命名空间以及非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、HOMEWORK三种类型之一。
每个字段后标记的“=1”、“=2”,是在二进制编码时使用的每个字段的唯一标识。在编码时,数字1~15要比大于它们的数字少一个字节,因此,作为一个优化选项,可以把1~15的数字用于常用的或重复性的元素。大于等于16的数字尽可能的用于那些不常用的可选元素。在重复字段中的每个元素都需要预定义一个标记数字,因此在重复性字段中使用这种优化是良好的选择。
每个字段必须用以下修饰符之一来进行标注:
1.required:用这个修饰符来标注的字段必须给该字段提供一个值,否则该消息会被认为未被初始化。尝试构建一个未被初始化的消息会抛出一个RuntimeException异常。解析未被初始化的消息时,会抛出一个IOException异常。其他方面,该类型字段的行为与可选类型字段完全一样;
2.optional:用这个修饰符来标注的字段可以设定值,也可以不设定值。如果可选字段的。值没有设定,那么就会使用一个默认的值。对于简单类型,能够像上例中指定电话号码的type那样,指定一个默认值。否则,系统使用的默认值如下:数字类型是0、字符串类型是空字符串、布尔值是false。对于内嵌的消息,默认值始终是“默认的实例“或”消息的“原型”,其中没有字段设置。调用没有明确设置值的字段的获取值的访问器的时候,会始终返回字段的默认值。
3.repeated:用这个修饰符来标注的字段可以被重复指定的数字的次数(包括0)。重复值的顺序会被保留在ProtocolBuffer中。重复字段跟动态数组很像。
对于标记为required的字段要始终小心。如果在某些时候,你希望终止写入或发送一个required类型的字段,那么在把该字段改变成可选字段时,就会发生问题---旧的版本会认为没有这个字段的消息是不完整的,并且会因此而拒绝或删除它们。因此应该考虑使用编写应用程序规范来定制Buffer的验证规则来代替。Google的一些工程师认为使用required,弊大于利,他们建议只使用optional和repeqted。但实际上是行不通的。
在ProtocolBuffer语言指南中,你会找到完成.proto文件编写指南---包括所有可能的字段类型。不要寻求类的继承性,ProtocolBuffer是不支持的
156b4
。
编译ProtocolBuffer
现在有一个.proto文件了,接下来要做的就是生成一个读写AddressBook(包括Person和PhoneNumber)消息的类。运行ProtocolBuffer编译器protoc来生成与.proto文件相关的类。
首先,需要下载的关于Protobuf的文件:
1.到http://code.google.com/p/protobuf/downloads/list ,选择其中的win版本下载,我选择的是protoc-2.4.1-win32.zip,解压得到protoc.exe文件。
2.运行编译器。在cmd命令下,进入protoc.exe的路径,
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);
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();
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhone(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.HOME))
.build();
importcom.example.tutorial.AddressBookProtos.Person;
importjava.io.BufferedReader;
importjava.io.FileInputStream;
importjava.io.FileNotFoundException;
importjava.io.FileOutputStream;
importjava.io.InputStreamReader;
importjava.io.IOException;
importjava.io.PrintStream;
class AddPerson {
// This function fills in a Person message based on user input.
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();
}
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
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();
// Read the existing address book.
try {
addressBook.mergeFrom(new FileInputStream(args[0]));
} catch (FileNotFoundException e) {
System.out.println(args[0] + ": File not found. Creating a new file.");
}
// Add an address.
addressBook.addPerson(
PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
System.out));
// Write the new address book back to disk.
FileOutputStream output = new FileOutputStream(args[0]);
addressBook.build().writeTo(output);
output.close();
}
}
读取一个消息
当然,如果不能够从输出的文件中获取任何信息,那么这个地址本就毫无用处。下面的例子演示了如何从上例创建的文件中读取信息,并把所有的信息都打印出来:
importcom.example.tutorial.AddressBookProtos.AddressBook;
importcom.example.tutorial.AddressBookProtos.Person;
importjava.io.FileInputStream;
importjava.io.IOException;
importjava.io.PrintStream;
class ListPeople {
// Iterates though all people in the AddressBook and prints info about them.
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());
}
}
}
// Main function: Reads the entire address book from a file and prints all
// the information inside.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
System.exit(-1);
}
// Read the existing address book.
AddressBook addressBook =
AddressBook.parseFrom(new FileInputStream(args[0]));
Print(addressBook);
}
}
扩展Protocol Buffer
使用Protocol Buffer的代码发布以后,不可避免的,你希望要改善Protocol Buffer的定义。如果想要新的Buffer类保持向后的兼容性,旧的Buffer保持向前的兼容性---几乎可以确定你是希望这样的。以下是你的新的Protocol Buffer版本要遵循的一些规则:
1.一定不要改变既存的标记数字;
2.不要添加或删除任何required类型的字段;
3.可以删除可选的或重复类型的字段;
4.可以添加新的可选的或重复类型的字段,必须使用新的标记数字(即,在该Protocol Buffer中没有被使用过的(即使是被删除的字段也不曾使用过)标记数字)。
(除了这些规则之外,还有一些其他的规则,但是它们很少使用)
如果你遵循了这些规则,旧的代码将会很好的读取新的消息,并且只是简单忽略了新的字段。对于旧代码,被删除的可选字段会简单的使用它们的默认值,被删除的重复性字段会被设置为空。新的代码也会透明的读取旧的消息。但是,要记住,新的可选字段不会出现在旧的消息里,因此你既可以明确的使用has_方法来检查它们是否被设置,也可以在.proto文件中在标记数字之后,用[default = value]来提供一个合理的默认值。对于没有指定默认值的可选元素,以下是特定类型使用的默认值:字符串类型,默认值是空字符串;布尔类型,默认值是false;数字类型,默认值是0。还要注意的是,如果你添加了一个新的重复性字段,因为没有给它has_标记,所以你的新代码不能被告知该字段是否是空的还是没有被设置。
高级用法
Protocol Buffer消息提供的一个关键特征就是反射。你能够迭代消息的字段,不用编写任何代码就可以维护任何指定的消息类型的值。使用反射的一个非常有用的方法就是把其他的编码格式转换成Protocol Buffer消息,如XML消息或JSON消息。反射的更高级的用途是查找两个相同类型消息直接的差异,或者是开发一种针对Protocol Buffer消息的正则表达式,在这个表达式中,你能够编写跟确定消息内容匹配的表达式。如果发挥你的想象力,Protocol Buffer的应用范围会比你的初始期望值还要高。
反射是作为Message和Message.Builder的接口部分来提供的。[/code](另外一个例子:http://www.cnblogs.com/stephen-liu74/archive/2013/01/06/2842972.html 很详细。点击打开链接)
ProtocolBuffer基础:Java
本指南提供了使用ProtocolBuffer工作的Java编程方法。全文通过一个简单的示例,向你介绍在Java中使用ProtocolBuffer的方法:
1.如何在.proto文件中定义消息格式;
2.如何使用ProtocolBuffer的creates编译器;
3.如何使用JavaProtocol Buffer的API来读写消息。
本文不是在Java中使用ProtocolBuffer的完整指南,更详细的信息请参照以下资料:
Protocol-buffers语言
JavaAPI参考
生成Java代码指南
编码参考
为什么使用ProtocolBuffer
我们使用了一个非常简单的“地址本”应用的例子,这个应用能够从一个文件中读写个人的联系方式信息。在地址本中每个人都有以下信息:姓名、ID、邮件地址、电话号码。
像这样的结构化数据应该如何系列化和恢复呢?以下几种方法能够解决这个问题:
1.使用Java系列化。因为它是内置在编程语言中的,所以是默认的方法,但是由于众所周知的主机问题,并且如果需要在使用不同编程语言(如C++或Python)编写应用程序之间共享数据,这种方式也不会很好的工作。
2.使用特殊的方式把数据项编码到一个单独的字符串中,如把4个整数编码成“12:3:-23:67”。尽管它需要编写一次性的编码和解码代码,但是这种方法简单而灵活,而且运行时解析成本很小。这种方法对于简单数据是最好的。
3.把数据系列化到XML。因为XML是可人类可读的,并且很多编程语言都有对应的功能类库,所以这种方法非常受欢迎。如果你想要跟其他应用程序/项目共享数据,那么这种方法是一个非常好的选择。但是,众所周知,XML是空间密集性的,并且编解码会严重影响应用程序的性能。此外,XML的DOM树导航也比一般的类中的字段导航要复杂的多。
ProtocolBuffer是完全解决这个问题的灵活、高效的自动化解决方案。使用ProtocolBuffer,要先编写一个.proto文件,用这个文件来描述你希望保存的数据结构。然后用ProtocolBuffer编译器创建一个类,这个类用高效的二进制的格式实现了ProtocolBuffer数据的自动编解码。生成的类提供了组成ProtocolBuffer字段的getter和setter方法,以及提供了负责读写一个ProtocolBuffer单位的方法。重要的是,ProtocolBuffer格式支持向后的兼容性,新的代码依然可以读取用旧格式编码的数据。
什么地方可以找到示例代码
示例代码的源代码包,可以直接从这儿下载。
定义协议格式
要创建你的地址本应用程序,需要从编写.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_package
关键字,否则,该包名会被用于生成的Java类文件的包名。即使提供了java_package,依然应该定义一个普通的package,以避免跟ProtocolBuffer命名空间以及非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、HOMEWORK三种类型之一。
每个字段后标记的“=1”、“=2”,是在二进制编码时使用的每个字段的唯一标识。在编码时,数字1~15要比大于它们的数字少一个字节,因此,作为一个优化选项,可以把1~15的数字用于常用的或重复性的元素。大于等于16的数字尽可能的用于那些不常用的可选元素。在重复字段中的每个元素都需要预定义一个标记数字,因此在重复性字段中使用这种优化是良好的选择。
每个字段必须用以下修饰符之一来进行标注:
1.required:用这个修饰符来标注的字段必须给该字段提供一个值,否则该消息会被认为未被初始化。尝试构建一个未被初始化的消息会抛出一个RuntimeException异常。解析未被初始化的消息时,会抛出一个IOException异常。其他方面,该类型字段的行为与可选类型字段完全一样;
2.optional:用这个修饰符来标注的字段可以设定值,也可以不设定值。如果可选字段的。值没有设定,那么就会使用一个默认的值。对于简单类型,能够像上例中指定电话号码的type那样,指定一个默认值。否则,系统使用的默认值如下:数字类型是0、字符串类型是空字符串、布尔值是false。对于内嵌的消息,默认值始终是“默认的实例“或”消息的“原型”,其中没有字段设置。调用没有明确设置值的字段的获取值的访问器的时候,会始终返回字段的默认值。
3.repeated:用这个修饰符来标注的字段可以被重复指定的数字的次数(包括0)。重复值的顺序会被保留在ProtocolBuffer中。重复字段跟动态数组很像。
对于标记为required的字段要始终小心。如果在某些时候,你希望终止写入或发送一个required类型的字段,那么在把该字段改变成可选字段时,就会发生问题---旧的版本会认为没有这个字段的消息是不完整的,并且会因此而拒绝或删除它们。因此应该考虑使用编写应用程序规范来定制Buffer的验证规则来代替。Google的一些工程师认为使用required,弊大于利,他们建议只使用optional和repeqted。但实际上是行不通的。
在ProtocolBuffer语言指南中,你会找到完成.proto文件编写指南---包括所有可能的字段类型。不要寻求类的继承性,ProtocolBuffer是不支持的
156b4
。
编译ProtocolBuffer
现在有一个.proto文件了,接下来要做的就是生成一个读写AddressBook(包括Person和PhoneNumber)消息的类。运行ProtocolBuffer编译器protoc来生成与.proto文件相关的类。
首先,需要下载的关于Protobuf的文件:
1.到http://code.google.com/p/protobuf/downloads/list ,选择其中的win版本下载,我选择的是protoc-2.4.1-win32.zip,解压得到protoc.exe文件。
2.运行编译器。在cmd命令下,进入protoc.exe的路径,
然后输入以下命令:
protoc.exe --proto_path=protocolfile --java_out=javafile protocolfile/*.proto
protocolfile表示当前目录下的一个文件夹,里面放着你要编译的.proto的文件。 javafile 表示该当前目录下的一个文件夹,里面放着编译后的java文件。[/code]
ProtocolBuffer API
让我们来看一下生成的代码,并看一下编译器都为你创建了那些类和方法。如果你在看
AddressBookProtos.java
文件,你能够看到它定义了一个叫做
AddressBookProtos
的类,在
addressbook.proto
文件中指定的每个消息都嵌套在这个类中。每个类都有它们自己的
Builder
类,你能够使用这个类来创建对应的类的实例。在下文的
Buildersvs. Messages
章节中,你会找到更多的有关
Builder
的信息。
Message
和
Builder
会给消息的每个字段都生成访问方法。
Message
仅有
get
方法,而
Builder
同时拥有
get
和
set
方法。以下是
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
有
get
和
set
方法:
// 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();
正如你看到的,每个字段都有简单的
JavaBean
样式的的
get
和
set
方法。对于每个有
get
方法的字段,如果该字段被设置,那么对应的
has
方法会返回
ture
。最后,每个字段还有一个
clear
方法,它会清除对应字段的设置,让它们回退到空的状态。
重复性字段会有一些额外的方法
---Count
方法(它会返回列表的尺寸)、通过索引指定获取或设定列表元素的
get
和
set
方法、往列表中添加新元素的
add
方法、以及把装有完整元素的容器放入列表中。
注意,这些访问方法都使用驼峰式命名,即使是使用小写字母和下划线的
.proto
文件名。这些变换都是由
Protocol Buffer
编译器自动完成的,因此生成的类也符合标准的
Java
样式协议。在你的
.proto
文件中,应该始终使用小写字母和下划线的字段命名,这样就会在所有的生成的编程语言中具有良好的命名实践。更多的良好的
.proto
样式,请看样式指南
。
对于那些特殊的字段定义,
Protocol
编译器生成的成员相关的更多更准确的信息,请看“
Java
生成代码参照
”。
枚举和嵌套类
在嵌套的
Person
类的生成代码中包含了
Java5
中的枚举类型
PhoneType
:
public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}
正如你所期待的,作为
Person
的嵌套类,生成了
Person.PhoneNumber
类型。
Builders vs. Messages
这些有
Protocol Buffer
编译器生成的消息类都是不可变的。一旦消息对象被构建了,它就不能被编辑了,就像
Java
的
String
。要构建一个消息对象,首先必须构建一个
Builder
,把你选择的值设置给对应的字段,然后调用
build()
方法。
你可能已经注意到,每个编辑消息的
builder
方法都会返回另外一个
Builder
对象,返回的
Builder
对象实际上与你调用的那个方法的
Builder
对象相同。这主要是为了能够在一行中编写
set
方法提供方便。
以下是创建
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();
标准的消息方法
每个消息和构建器类还包含了一些其他的方法,这些方法会帮助你检查或维护整个消息,这些方法包括:
1.isInitialized():
检查所有的
required
字段是否都被设置了。
2.toString():
返回一个可读的消息描述,对于调试特别有用。
3.mergeFrom(Message other):(
只有构建器有这个方法
)
,它会把
other
参数中的内容,用重写和串联
的方式合并到本消息中。
Clear():(
只有构建器才有这个方法
)
,清除所有字段的值,让它们返回到空的状态。
这些方法实现的
Message
和
Message.Builder
接口,会被所有的
Java
消息和构建器共享。更多信息,请看
Message
的完成
API
文档
。
解析和系列化
最后,每个
Protocol Buffer
类都有一些使用二进制来读写你选择的类型的方法,包括:
1.byte[] toByteArray()
:系列化消息,并返回包含原始字节的字节数组。
2.static Person parseFrom(byte[] data):
从给定的字节数组中解析消息。
3.void writeTo(OutputStream output):
系列化一个消息,并把该消息写入一个
OutputStream
对象中。
4.static Person parseFrom(InputStream input):
从
InputStream
对象中读取和解析一个消息。
对于解析和系列化,这些方法是成对使用的。完整的
API
列表请看“
Message API
参考
”
Protocol Buffer
和面向对象的设计:
Protocol Buffer
类是基本的数据持有者(有点类似
C++
中的结构体);在对象模型中,它们不是良好的一等类公民。如果你想要给生成的类添加丰富的行为,最好的做法是在特定的应用程序类中封装生成的
Protocol Buffer
类。如果在
.proto
文件的设计上没有控制,那么封装
Protocol Buffer
类也是个不错的主意(比方说,你要重用另一个项目中一个
Protocol Buffer
类)。在这种情况下,你能够包装类来构建一个适应你的应用程序环境的更好的接口:如隐藏一些数据和方法、暴露一些方便的功能,等等。你不应该通过继承给这些生成的类添加行为方法,这样做会终端内部机制,而且也不是良好的面向对象的实践。
编写一个消息
现在,让我们来尝试使用这些
Protocol Buffer
类。首先,你希望你的地址本应用程序能够把个人详细信息写入地址本文件。要完成这件事情,你需要创建并初始化
Protocol Buffer
类的实例,然后把它们写入一个输出流中。
以下是一段从文件中读取
AddressBook
的程序,它会基于用户的输入把一个新的
Person
对象添加到
AddressBook
对象中,并这个新的
AddressBook
对象在写回该文件中。
import
com.example.tutorial.AddressBookProtos.AddressBook
;
importcom.example.tutorial.AddressBookProtos.Person;
importjava.io.BufferedReader;
importjava.io.FileInputStream;
importjava.io.FileNotFoundException;
importjava.io.FileOutputStream;
importjava.io.InputStreamReader;
importjava.io.IOException;
importjava.io.PrintStream;
class AddPerson {
// This function fills in a Person message based on user input.
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();
}
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
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();
// Read the existing address book.
try {
addressBook.mergeFrom(new FileInputStream(args[0]));
} catch (FileNotFoundException e) {
System.out.println(args[0] + ": File not found. Creating a new file.");
}
// Add an address.
addressBook.addPerson(
PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
System.out));
// Write the new address book back to disk.
FileOutputStream output = new FileOutputStream(args[0]);
addressBook.build().writeTo(output);
output.close();
}
}
读取一个消息
当然,如果不能够从输出的文件中获取任何信息,那么这个地址本就毫无用处。下面的例子演示了如何从上例创建的文件中读取信息,并把所有的信息都打印出来:
importcom.example.tutorial.AddressBookProtos.AddressBook;
importcom.example.tutorial.AddressBookProtos.Person;
importjava.io.FileInputStream;
importjava.io.IOException;
importjava.io.PrintStream;
class ListPeople {
// Iterates though all people in the AddressBook and prints info about them.
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());
}
}
}
// Main function: Reads the entire address book from a file and prints all
// the information inside.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
System.exit(-1);
}
// Read the existing address book.
AddressBook addressBook =
AddressBook.parseFrom(new FileInputStream(args[0]));
Print(addressBook);
}
}
扩展Protocol Buffer
使用Protocol Buffer的代码发布以后,不可避免的,你希望要改善Protocol Buffer的定义。如果想要新的Buffer类保持向后的兼容性,旧的Buffer保持向前的兼容性---几乎可以确定你是希望这样的。以下是你的新的Protocol Buffer版本要遵循的一些规则:
1.一定不要改变既存的标记数字;
2.不要添加或删除任何required类型的字段;
3.可以删除可选的或重复类型的字段;
4.可以添加新的可选的或重复类型的字段,必须使用新的标记数字(即,在该Protocol Buffer中没有被使用过的(即使是被删除的字段也不曾使用过)标记数字)。
(除了这些规则之外,还有一些其他的规则,但是它们很少使用)
如果你遵循了这些规则,旧的代码将会很好的读取新的消息,并且只是简单忽略了新的字段。对于旧代码,被删除的可选字段会简单的使用它们的默认值,被删除的重复性字段会被设置为空。新的代码也会透明的读取旧的消息。但是,要记住,新的可选字段不会出现在旧的消息里,因此你既可以明确的使用has_方法来检查它们是否被设置,也可以在.proto文件中在标记数字之后,用[default = value]来提供一个合理的默认值。对于没有指定默认值的可选元素,以下是特定类型使用的默认值:字符串类型,默认值是空字符串;布尔类型,默认值是false;数字类型,默认值是0。还要注意的是,如果你添加了一个新的重复性字段,因为没有给它has_标记,所以你的新代码不能被告知该字段是否是空的还是没有被设置。
高级用法
Protocol Buffer消息提供的一个关键特征就是反射。你能够迭代消息的字段,不用编写任何代码就可以维护任何指定的消息类型的值。使用反射的一个非常有用的方法就是把其他的编码格式转换成Protocol Buffer消息,如XML消息或JSON消息。反射的更高级的用途是查找两个相同类型消息直接的差异,或者是开发一种针对Protocol Buffer消息的正则表达式,在这个表达式中,你能够编写跟确定消息内容匹配的表达式。如果发挥你的想象力,Protocol Buffer的应用范围会比你的初始期望值还要高。
反射是作为Message和Message.Builder的接口部分来提供的。[/code](另外一个例子:http://www.cnblogs.com/stephen-liu74/archive/2013/01/06/2842972.html 很详细。点击打开链接)
相关文章推荐
- SSL介绍与Java实例
- 【编程语言-Java】ThreadPoolExecutor介绍与实例
- java 线程锁详细介绍及实例代码
- Protocol Buffer技术详解(Java实例)
- Java中三种简单注解介绍和代码实例
- Java 集合之HashSet常用方法实例介绍
- java图像用户界面的组件与面板介绍及其实例
- java中静态变量和实例变量的区别详细介绍
- java异常处理详细介绍及实例
- 【Java】Collection,set,List,Map介绍(附实例)
- Java 时间日期详细介绍及实例
- Java内存数据库-H2介绍及实例(SpringBoot)
- Java XML解析工具 JDOM介绍及使用实例
- Protocol Buffer技术详解(Java实例)
- Java 代码优化过程的实例介绍
- java注解介绍和自定义注解实例
- java调用python方法的库jython介绍及使用实例
- FTP客户端Java类库 ftp4j介绍及其实例(键人岐)
- log4j介绍以及在java项目和javaweb项目中使用log4j的实例
- Java 正则表达式的详细介绍以及实例演示