参考链接
https://developers.google.com/protocol-buffers/docs/javatutorial
Demo地址
https://github.com/Shoothzj/maven-demo/tree/master/demo-protobuf
为什么使用ProtoBuffers
样例我们将使用简单地使用"地址簿"应用,该应用可以从文件中读写人们的联系方式。地址簿中的每个人都拥有一个名字,一个ID,一个邮件地址,和一个联系电话号。
简单地编解码方式有如下几种:
- 使用Java的序列化方式。这是java语言内置的方式,但是它的缺点非常出名,并且无法跨语言操作
- 你可以发明一个对口的方式,把数据编码成一个字符串,比方说编码成4个整型值=> "12:3:-23:67"。尽管确实需要编写一次性编码和解析代码,但是这是一种简单且灵活的方法,而且解析带来的运行时成本很小。这对于编码非常简单的数据最有效
- 把数据序列化成XML/Json。这个方式可以说很有吸引力,因为xml和json这两个都是阅读友好的,几乎所有的编程语言都有内置的或者是三方库来支持。但是,这两个方式的性能稍差一些,也更浪费空间。
Protocol buffers 是灵活,高效,自动化的解决方案,可以准确地解决此问题。使用Protocol buffer, 你可以编写要存储的数据结构的.proto描述。 由此,protocol buffer 编译器创建了一个类,该类以高效的二进制格式实现Protocol buffer数据的自动编码和解析。并且生成类给每个字段都提供了set,get方法,并以protocol buffer为单元来处理读写操作。重要的是,protocol buffer支持对旧格式进行扩展,使得代码可以兼容旧的数据。
定义你的协议格式
创建地址簿应用之前,你将从.proto文件开始。.proto文件中的定义十分简单:每有一个数据结构,你就添加一个message,然后指定其中的每一个字段的名字和类型。这是addressbook.proto的样例
syntax = "proto2";
package tutorial;
option java_package = "com.github.shoothzj.demo.protobuf.module";
option java_outer_classname = "AddressBookProtos";
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
包声明
.proto文件以程序包声明开头,这有助于防止不同项目之间的命名冲突。在Java中,除非您已明确指定java_package,否则将包名称用作Java包,如此处所示。即使您确实提供了java_package,也仍然应该定义一个普通的包,以避免协议缓冲区名称空间和非Java语言中的名称冲突。
Java Option
程序包声明后,您可以看到两个特定于Java的选项:java_package和java_outer_classname。 java_package指定您所生成的类应使用哪个Java包名称。如果未明确指定,则它仅与程序包声明中给出的程序包名称匹配,但这些名称通常不是适当的Java程序包名称(因为它们通常不以域名开头)。 java_outer_classname选项定义了类名称,该名称应包含此文件中的所有类。如果您没有明确给出java_outer_classname,它将通过将文件名转换为大写驼峰来生成。例如,默认情况下,“ my_proto.proto”将使用“ MyProto”作为外部类名称。
消息定义
接下来是消息定义。消息只是一系列有类型的字段的聚合。许多标准的简单数据可以用做字段类型,包括bool,int32,float,double,string。您还可以通过使用其他消息类型作为字段类型来为消息添加更多结构-在上面的示例中,“个人”消息包含PhoneNumber消息,而“地址簿”消息包含“个人”消息。 您甚至可以定义嵌套在其他消息中的消息类型-如您所见,PhoneNumber类型是在Person内部定义的。 如果希望您的字段之一具有预定义的值列表之一,也可以定义枚举类型-在这里您要指定电话号码可以是MOBILE,HOME或WORK之一。
每个元素上"=1", "=2"的标志标志着字段用来二进制编码时独一无二的标识。标签编号1至15与较高的编号相比,编码所需的字节减少了一个字节,因此,为了进行优化,您可以决定将这些标签用于常用或重复的元素,而将标签16和更高的标签用于较少使用的可选元素。 重复字段中的每个元素都需要重新编码标签号,因此重复字段是此优化的最佳候选者。
修饰符
每个字段都必须使用以下修饰符之一进行注释
- Optional:可能会或可能不会设置该字段。如果未设置可选字段值,则使用默认值。对于简单类型,您可以指定自己的默认值,就像在示例中为电话号码类型所做的那样。否则,将使用系统默认值:数字类型为零,字符串为空字符串,布尔值为false。对于嵌入式消息,默认值始终是消息的“默认实例”或“原型”,没有设置任何字段。调用访问器以获取未显式设置的可选(或必填)字段的值始终会返回该字段的默认值。
- Repeated:该字段可以重复任意次(包括零次)。重复值的顺序将保留在协议缓冲区中。将重复字段视为动态大小的数组。
- Required:必须提供该字段的值,否则该消息将被视为“未初始化”。尝试生成未初始化的消息将引发RuntimeException。解析未初始化的消息将引发IOException。除此之外,必填字段的行为与可选字段完全相同。
在Protocol Buffer 指南中,你会找到编写.proto文件的完整指南--包括所有可能的字段类型。但是,不要去寻找类似于类继承的工具--协议缓冲区不能做这件事
编译Protocol Buffer
现在你拥有一个.proto,接下来我们要生成java类, 首先我们需要安装protoc
安装Protoc
OSX
brew install protobuf
[图片上传失败...(image-b2dfc4-1605274346421)]
然后在src/main 路径下执行
protoc -I=proto --java_out=java proto/Message.proto
就会发现生成的对应的Java类
[图片上传失败...(image-b2f213-1605274346421)]
Protocol Buffer API
查看AddressBookProtocs.java类,可以看到其中有内部类AddressBookProtos。每个类都有自己的Builder类。
消息和构建器都为消息的每个字段都自动生成了访问器方法。 消息只有get,而建造者既有set又有get。 以下是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 phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
Person.Builder的set和get方法:
// 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 phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
public Builder setPhones(int index, PhoneNumber value);
public Builder addPhones(PhoneNumber value);
public Builder addAllPhones(Iterable<PhoneNumber> value);
public Builder clearPhones();
如您所见,每个字段都有简单的JavaBeans风格的getter和setters。每个非基本类型字段也都有has方法,如果已设置该字段,则返回true。最后,每个字段都有一个clear方法,可以将字段取消设置为空状态。
重复的字段有一些额外的方法-Count方法(这只是列表大小的简写),getter和setter(通过索引获取或设置列表的特定元素),add方法(将新元素附加到列表中)和 一个addAll方法,它将一个充满元素的整个容器添加到列表中。
请注意,即使.proto文件使用带下划线的小写字母,这些访问器方法也如何使用驼峰式命名。 此转换由协议缓冲区编译器自动完成,以使生成的类与标准Java样式约定匹配。 对于.proto文件中的字段名称,应始终使用带下划线的小写字母; 这样可以确保所有生成的语言都具有良好的命名习惯。 有关良好的.proto样式,请参见样式指南。
Builder 和 Message 的区别
协议缓冲区编译器生成的消息类都是不可变的。 一旦构造了消息对象,就不能像Java String一样对其进行修改。 要构造消息,必须首先构造一个构建器,将要设置的任何字段设置为所选值,然后调用该构建器的build()方法。
package com.github.shoothzj.demo.protobuf.module;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
/**
* @author hezhangjian
*/
@Slf4j
public class PersonTest {
@Test
public void testPersonBuild() {
AddressBookProtos.Person john = AddressBookProtos.Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhones(AddressBookProtos.Person.PhoneNumber.newBuilder()
.setNumber("555-4321").setType(AddressBookProtos.Person.PhoneType.HOME))
.build();
System.out.println(john);
}
}
写入Message
现在,让我们尝试使用ProtoBuf类。 您希望地址簿应用程序能够做的第一件事是将个人详细信息写入地址簿文件。 为此,您需要创建并填充协议缓冲区类的实例,然后将它们写入输出流。
这是一个程序,它从文件中读取地址簿,根据用户输入向其中添加一个新的Person,然后将新的地址簿再次写回到文件中。
package com.github.shoothzj.demo.protobuf;
import com.github.shoothzj.demo.protobuf.module.AddressBookProtos;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
class AddPerson {
/**
* This function fills in a Person message based on user input.
*
* @param stdin
* @param stdout
* @return
* @throws IOException
*/
static AddressBookProtos.Person promptForAddress(BufferedReader stdin,
PrintStream stdout) throws IOException {
AddressBookProtos.Person.Builder person = AddressBookProtos.Person.newBuilder();
stdout.print("Enter person ID: ");
person.setId(Integer.parseInt(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;
}
AddressBookProtos.Person.PhoneNumber.Builder phoneNumber =
AddressBookProtos.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(AddressBookProtos.Person.PhoneType.MOBILE);
} else if (type.equals("home")) {
phoneNumber.setType(AddressBookProtos.Person.PhoneType.HOME);
} else if (type.equals("work")) {
phoneNumber.setType(AddressBookProtos.Person.PhoneType.WORK);
} else {
stdout.println("Unknown phone type. Using default.");
}
person.addPhones(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);
}
AddressBookProtos.AddressBook.Builder addressBook = AddressBookProtos.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.addPeople(
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();
}
}
读取Message
当然,如果您无法从中获取任何信息,通讯录也不会有用! 本示例读取由以上示例创建的文件,并打印其中的所有信息。
package com.github.shoothzj.demo.protobuf;
import com.github.shoothzj.demo.protobuf.module.AddressBookProtos;
import java.io.FileInputStream;
class ListPeople {
// Iterates though all people in the AddressBook and prints info about them.
static void print(AddressBookProtos.AddressBook addressBook) {
for (AddressBookProtos.Person person : addressBook.getPeopleList()) {
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 (AddressBookProtos.Person.PhoneNumber phoneNumber : person.getPhonesList()) {
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.
AddressBookProtos.AddressBook addressBook =
AddressBookProtos.AddressBook.parseFrom(new FileInputStream(args[0]));
print(addressBook);
}
}
保持兼容性
- 不得更改任何现有字段的标签号。
- 不得添加或删除任何必填字段。
- 可以删除可选字段或重复字段。
- 可以添加新的可选或重复字段,但必须使用新的标签号(即,该协议缓冲区中从未使用过的标签号,即使删除的字段也从未使用过)。
(这些规则有一些例外,但很少使用。)
如果遵循这些规则,旧代码将还可以很好地接收新消息,而忽略任何新字段。对于旧代码,已删除的可选字段将仅具有其默认值,而删除的重复字段将为空。新代码还将透明地读取旧消息。但是,请记住,新的可选字段将不会出现在旧消息中,因此您将需要明确检查是否已使用has_设置它们,或使用[default = value]在.proto文件中提供合理的默认值。标签编号之后。如果未为可选元素指定默认值,则使用特定于类型的默认值:对于字符串,默认值为空字符串。对于布尔值,默认值为false。对于数字类型,默认值为零。还要注意,如果添加了一个新的重复字段,则新代码将无法告诉它是空的(由新代码)还是根本没有设置(由旧代码),因为没有has_标志。
网友评论