概述
本文译自: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:
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是不支持的。
编译ProtocolBuffer
现在有一个.proto文件了,接下来要做的就是生成一个读写AddressBook(包括Person和PhoneNumber)消息的类。运行ProtocolBuffer编译器protoc来生成与.proto文件相关的类。
1.如果你没有安装编译器,需要下载编译器包,并按着README文件中的指示来做。
2.运行编译器,指定源目录(你的应用程序的源代码所在的目录---如果没有提供这个值,则使用当前目录)、目的目录(生成代码存放的目录,经常使用与环境变量$SRC_DIR
相同的目录),以及
.proto
文件所在的路径,如:
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
因为你想要
Java
类,所以要使用—
java_out
选项,其他支持的语言也提供了类似的选项。
在指定的目标目录中生成
com/example/tutorial/AddressBookProtos.java
文件。
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
;
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 { // 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(); } } 读取一个消息 当然,如果不能够从输出的文件中获取任何信息,那么这个地址本就毫无用处。下面的例子演示了如何从上例创建的文件中读取信息,并把所有的信息都打印出来: 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 { // 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的接口部分来提供的。
最后
以上就是帅气舞蹈为你收集整理的Protocol Buffer介绍(Java)的全部内容,希望文章能够帮你解决Protocol Buffer介绍(Java)所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复