我是靠谱客的博主 帅气舞蹈,这篇文章主要介绍Protocol Buffer介绍(Java),现在分享给大家,希望可以做个参考。

本文译自:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN

ProtocolBuffer基础:Java

本指南提供了使用ProtocolBuffer工作的Java编程方法。全文通过一个简单的示例,向你介绍在Java中使用ProtocolBuffer的方法:

1.如何在.proto文件中定义消息格式;

2.如何使用ProtocolBuffercreates编译器;

3.如何使用JavaProtocol BufferAPI来读写消息。

本文不是在Java中使用ProtocolBuffer的完整指南,更详细的信息请参照以下资料:

Protocol-buffers语言

JavaAPI参考

生成Java代码指南

编码参考

为什么使用ProtocolBuffer

我们使用了一个非常简单的“地址本”应用的例子,这个应用能够从一个文件中读写个人的联系方式信息。在地址本中每个人都有以下信息:姓名、ID、邮件地址、电话号码。

像这样的结构化数据应该如何系列化和恢复呢?以下几种方法能够解决这个问题:

1.使用Java系列化。因为它是内置在编程语言中的,所以是默认的方法,但是由于众所周知的主机问题,并且如果需要在使用不同编程语言(如C++Python)编写应用程序之间共享数据,这种方式也不会很好的工作。

2.使用特殊的方式把数据项编码到一个单独的字符串中,如把4个整数编码成“123-2367”。尽管它需要编写一次性的编码和解码代码,但是这种方法简单而灵活,而且运行时解析成本很小。这种方法对于简单数据是最好的。

3.把数据系列化到XML。因为XML是可人类可读的,并且很多编程语言都有对应的功能类库,所以这种方法非常受欢迎。如果你想要跟其他应用程序/项目共享数据,那么这种方法是一个非常好的选择。但是,众所周知,XML是空间密集性的,并且编解码会严重影响应用程序的性能。此外,XMLDOM树导航也比一般的类中的字段导航要复杂的多。

ProtocolBuffer是完全解决这个问题的灵活、高效的自动化解决方案。使用ProtocolBuffer,要先编写一个.proto文件,用这个文件来描述你希望保存的数据结构。然后用ProtocolBuffer编译器创建一个类,这个类用高效的二进制的格式实现了ProtocolBuffer数据的自动编解码。生成的类提供了组成ProtocolBuffer字段的gettersetter方法,以及提供了负责读写一个ProtocolBuffer单位的方法。重要的是,ProtocolBuffer格式支持向后的兼容性,新的代码依然可以读取用旧格式编码的数据。


什么地方可以找到示例代码

示例代码的源代码包,可以直接从这儿下载。

定义协议格式

要创建你的地址本应用程序,需要从编写.proto文件开始。.proto文件的定义很简单:你要在每个想要系列化的数据结构前添加一个message关键字,然后指定消息中每个字段的名称和类型。以下就是你要定义的.proto文件,addressbook.proto:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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_packagejava_outer_classnamejava_package指定要生成的Java类的包名。如果没有明确的指定这个关键字,它会简单的用package关键字的声明来作为包名,但是这些名称通常不适合做Java的包名(因为它们通常不是用域名开头的)。java_outer_classname可选项定义了这个文件中所包含的所有类的类名。如果没有明确的给出java_outer_classname定义,它会把文件名转换成驼峰样式的类名。如,“my_proto.proto”文件,默认的情况下会使用MyProto作为外部的类名。

接下来是消息定义,一个消息包含了一组类型字段。很多标准的简单数据类型都可以作为有效的字段类型,包括:boolint32floatdoublestring。还可以是其他消息类型作为字段类型---在上面的示例中,Person消息包含了PhoneNumber消息,而AddressBook消息又包含了Person消息。甚至还可以定嵌套在其他消息内的消息类型---如,PhoneNumber类型就被定义在Person内。如果想要字段有一个预定义的值列表,也可以定enum类型---上例中电话号码能够指定MOBILEHOMEWORK三种类型之一。

每个字段后标记的“=1”、“=2”,是在二进制编码时使用的每个字段的唯一标识。在编码时,数字115要比大于它们的数字少一个字节,因此,作为一个优化选项,可以把115的数字用于常用的或重复性的元素。大于等于16的数字尽可能的用于那些不常用的可选元素。在重复字段中的每个元素都需要预定义一个标记数字,因此在重复性字段中使用这种优化是良好的选择。

每个字段必须用以下修饰符之一来进行标注:

1.required:用这个修饰符来标注的字段必须给该字段提供一个值,否则该消息会被认为未被初始化。尝试构建一个未被初始化的消息会抛出一个RuntimeException异常。解析未被初始化的消息时,会抛出一个IOException异常。其他方面,该类型字段的行为与可选类型字段完全一样;

2.optional:用这个修饰符来标注的字段可以设定值,也可以不设定值。如果可选字段的。值没有设定,那么就会使用一个默认的值。对于简单类型,能够像上例中指定电话号码的type那样,指定一个默认值。否则,系统使用的默认值如下:数字类型是0、字符串类型是空字符串、布尔值是false。对于内嵌的消息,默认值始终是“默认的实例“或”消息的“原型”,其中没有字段设置。调用没有明确设置值的字段的获取值的访问器的时候,会始终返回字段的默认值。

3.repeated:用这个修饰符来标注的字段可以被重复指定的数字的次数(包括0)。重复值的顺序会被保留在ProtocolBuffer中。重复字段跟动态数组很像。

对于标记为required的字段要始终小心。如果在某些时候,你希望终止写入或发送一个required类型的字段,那么在把该字段改变成可选字段时,就会发生问题---旧的版本会认为没有这个字段的消息是不完整的,并且会因此而拒绝或删除它们。因此应该考虑使用编写应用程序规范来定制Buffer的验证规则来代替。Google的一些工程师认为使用required,弊大于利,他们建议只使用optionalrepeqted。但实际上是行不通的。

ProtocolBuffer语言指南中,你会找到完成.proto文件编写指南---包括所有可能的字段类型。不要寻求类的继承性,ProtocolBuffer是不支持的。

编译ProtocolBuffer

现在有一个.proto文件了,接下来要做的就是生成一个读写AddressBook(包括PersonPhoneNumber)消息的类。运行ProtocolBuffer编译器protoc来生成与.proto文件相关的类。

1.如果你没有安装编译器,需要下载编译器包,并按着README文件中的指示来做。

2.运行编译器,指定源目录(你的应用程序的源代码所在的目录---如果没有提供这个值,则使用当前目录)、目的目录(生成代码存放的目录,经常使用与环境变量$SRC_DIR相同的目录),以及.proto文件所在的路径,如:

复制代码
1
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的信息。

MessageBuilder会给消息的每个字段都生成访问方法。Message仅有get方法,而Builder同时拥有getset方法。以下是Person类的一些访问方法(为了简单,忽略了实现):

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
// 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.Buildergetset方法: // 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样式的的getset方法。对于每个有get方法的字段,如果该字段被设置,那么对应的has方法会返回ture。最后,每个字段还有一个clear方法,它会清除对应字段的设置,让它们回退到空的状态。 重复性字段会有一些额外的方法---Count方法(它会返回列表的尺寸)、通过索引指定获取或设定列表元素的getset方法、往列表中添加新元素的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编译器生成的消息类都是不可变的。一旦消息对象被构建了,它就不能被编辑了,就像JavaString。要构建一个消息对象,首先必须构建一个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():(只有构建器才有这个方法),清除所有字段的值,让它们返回到空的状态。 这些方法实现的MessageMessage.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的应用范围会比你的初始期望值还要高。 反射是作为MessageMessage.Builder的接口部分来提供的。

最后

以上就是帅气舞蹈最近收集整理的关于Protocol Buffer介绍(Java)的全部内容,更多相关Protocol内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(88)

评论列表共有 0 条评论

立即
投稿
返回
顶部