Protocol Buffer 语法指南
Java API
Java Generated Code Guide
Encoding Reference
下面一个例子是一个简单的通讯录,可以读写人物信息到文件。每个人都有姓名,ID,email和电话号码。
你会如何序列化和检索这样的结构化数据?下面有几种方案:
对于解决这个问题,Protocol Buffer是灵活,高效,自动化的解决方案。使用协议缓冲区,你可以编写.proto描述存储的数据结构。由此,Protocol Buffer编译器创建一个类,该类使用有效的二进制格式实现Protocol Buffer数据的自动编码和解析。生成的类为构成Protocol Buffer的字段提供getter和setter,并负责将Protocol Buffer作为一个单元进行读写的详细信息。重要的是,Protocol Buffer格式支持随着时间的推移扩展格式的想法,使得代码仍然可以读取用旧格式编码的数据。
要创建你的地址簿,你需要首先创建一个 .proto
文件。这个文件里的定义很简单:为每一个你需要序列化的数据结构添加一个消息,然后为消息中的每一个域指定一个name和type。
addressbook.proto
:
syntax = "proto2"; 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 phones = 4; } message AddressBook { repeated Person people = 1; }
文件语法类似于C++或Java,下面具体了解一下文件结构。
文件由包声明开始,这有助于防止不同项目之间的命名冲突。在Java语言中,包名用作Java包,除非明确指定了一个 java_package
(如代码所做)。即使你定义了 java_package
,你也仍然要定义一个普通的 package
来避免在Protocol Buffer命名空间的名称冲突,尤其是在非Java语言中。
包声明之后是两行Java的可选项: java_package
和 java_outer_classname
。
java_package
,指定生成的类应该以什么Java包名称存在。如果没有明确指定,它简单地匹配由 package
声明的包名,但这些名称通常不是合适的Java包名称(因为它们通常不以域名开头)。 java_outer_classname
,定义应包含此文件中所有类的类名。如果没有显示指定 java_outer_classname
,它会把文件名转换为驼峰命名来生成。比如,”my_proto.proto”会转化成”MyProto”作为外部类名。
下一行就是 message
的定义。消息只是包含一组类型字段的聚合。许多标准的简单数据类型都可用作字段类型,包括bool,int32,float,double和string。你也可以使用其他 message
类型作为域类型为当前 message
增加深层次的结构。上面例子中,Person message就包含了PhoneNumber message,同时,AddressBook message也包含了Person message。你甚至可以在其他message内定义内联的message类型,PhoneNumber就是这样的。如果你希望其中一个字段具有预定义的值列表之一,你可以使用enum实现。代码中PhoneNumber就是 MOBILE
, HOME
或者 WORK
三种。
每个元素的”=1”,”=2”标记标识该字段在二进制编码中使用的唯一“标记”。标记1-15比高的数字需要的编码少一个字节,因此,作为优化,您可以决定将这些标记用于常用或重复元素,将标记16和更高的数字留给不太常用的可选元素。重复字段中的每个元素都需要重新编码标记号,因此重复字段特别适合此优化。
必须使用以下修饰符之一注释每个字段:
required
:必须提供该字段的值,否则该消息将被视为未初始化(uninitialized)。如果编译一个未初始化的消息会抛出 RuntimeException
的异常。解析未初始化的消息会抛出 IOException
的异常。除此之外, required
的字段和 optional
表现一致。
optional
:该字段可以不赋予初始值。如果一个 optional
字段值没有设置,会赋予一个默认值。对于简单的类型,你可以指定你自己的默认值,就像前面对PhoneNumber内type做的一样:
optional PhoneType type = 2 [default = HOME];
否则,系统会给与一个默认值:数字是0,字符串是空字串,bool值是false。
对于嵌入的message,默认值总是该message的默认实例或者 prototype
,该消息没有设置任何字段。调用accessor以获取尚未显式设置的 optional
(或 required
)字段的值始终返回该字段的默认值。
repeated
:该字段可以重复任意次数(包括零)。重复值的顺序将保留在协议缓冲区中。将重复字段视为动态大小的数组。
现在有了一个 .proto
文件,你需要做的下一件事是生成读取和写入AddressBook(以及Person和PhoneNumber)消息所需的类。要完成它,你需要运行Protocol Buffer编译器 protoc
编译 .proto
文件:
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
比如我的环境下,
{18-07-20 15:08}wOw-RMBP:~/Projects/Protobuf wow% ls addressbook.proto {18-07-20 15:08}wOw-RMBP:~/Projects/Protobuf wow% protoc -I=./ --java_out=./ ./addressbook.proto {18-07-20 15:09}wOw-RMBP:~/Projects/Protobuf wow% ls addressbook.proto com
生成的文件结构为
|____addressbook.proto |____com | |____example | | |____tutorial | | | |____AddressBookProtos.java
生成的java文件有2816行,就不贴出来了。下面会对java文件做一些解释。
从 AddressBookProtos.java
看到定义了一个 AddressBookProtos
类,其中嵌套了你在 addressbook.proto
中指定的每条消息的类。每个类都有自己的 Builder
类,你可以通过其创建实例。
public final class AddressBookProtos { public interface PersonOrBuilder extends // @@protoc_insertion_point(interface_extends:tutorial.Person) com.google.protobuf.MessageOrBuilder { } public static final class Person extends com.google.protobuf.GeneratedMessageV3 implements // @@protoc_insertion_point(message_implements:tutorial.Person) PersonOrBuilder { } public interface PhoneNumberOrBuilder extends // @@protoc_insertion_point(interface_extends:tutorial.Person.PhoneNumber) com.google.protobuf.MessageOrBuilder { } public static final class PhoneNumber extends com.google.protobuf.GeneratedMessageV3 implements // @@protoc_insertion_point(message_implements:tutorial.Person.PhoneNumber) PhoneNumberOrBuilder { } public interface AddressBookOrBuilder extends // @@protoc_insertion_point(interface_extends:tutorial.AddressBook) com.google.protobuf.MessageOrBuilder { } public static final class AddressBook extends com.google.protobuf.GeneratedMessageV3 implements // @@protoc_insertion_point(message_implements:tutorial.AddressBook) AddressBookOrBuilder { } }
messages和builders都为消息的每个字段都有自动生成的访问器方法;消息只有getter,而构建器有getter和setter。以下是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也有相同的getter和setter
// 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和setter。每个单独的字段都有getter,如果已设置该字段,则返回true。最后,每个字段都有一个清除方法,将字段取消设置回其空状态。
Repeated字段有一些额外的方法:一个 Count
方法(它只是列表大小的简写),getter和setter通过索引get或set列表的特定元素, add
方法将新元素附加到列表,以及一个 addAll
方法,它将整个容器中的元素添加到列表中。
注意这些accessor方法如何使用驼峰式命名,即使 .proto
文件使用带下划线的小写。此转换由Protocol Buffer编译器自动完成,以便生成的类与标准Java样式约定匹配。您应该始终在 .proto
文件中使用带有下划线的小写字母作为字段名称,这确保了所有生成语言的良好命名实践。有关良好的.proto样式的更多信息,请参阅 样式指南
。
生成的代码包含 PhoneType
集合,内嵌于 Person
类中。
public enum PhoneType implements com.google.protobuf.ProtocolMessageEnum { /** * <code>MOBILE = 0;</code> */ MOBILE(0), /** * <code>HOME = 1;</code> */ HOME(1), /** * <code>WORK = 2;</code> */ WORK(2), ; /** * <code>MOBILE = 0;</code> */ public static final int MOBILE_VALUE = 0; /** * <code>HOME = 1;</code> */ public static final int HOME_VALUE = 1; /** * <code>WORK = 2;</code> */ public static final int WORK_VALUE = 2; ... }
内嵌类 Person.PhoneNumber
也一并生成,代码结构在前面API可以看到。
Protocol Buffer编译器生成的消息类都是不可变的。一旦构造了消息对象,就不能像Java String一样修改它。要构造消息,必须首先构造构建器,将要设置的任何字段设置为所选值,然后调用构建器的 build
方法。
你可能已经注意到每个修改消息的构建器的方法都返回另一个构建器。返回的对象实际上是您调用该方法的同一个构建器。为方便起见,它会返回,以便您可以在一行代码中将多个setter串在一起。(链式编程)
下面是个创建 Person
对象的例子:
Person john = Person.newBuilder() .setId(1234) .setName("John Doe") .setEmail("jdoe@example.com") .addPhones( Person.PhoneNumber.newBuilder() .setNumber("555-4321") .setType(Person.PhoneType.HOME)) .build();
每个message和builder类还包含许多其他方法,可用于检查或操作整个消息,包括:
isInitialized() toString() mergeFrom(Message other) clear()
这些方法实现了所有Java消息和构建器共享的 Message
和 Message.Builder
接口。有关更多信息,请参阅 Message的完整API文档
。
最后,每个Protocol Buffer类都有使用Protocol Buffer二进制格式编写和读取所选类型消息的方法。这些包括:
byte [] toByteArray(); static Person parseFrom(byte[] data); void writeTo(OutputStream output); static Person parseFrom(InputStream input);
这些只是解析和序列化提供的几个选项。
现在让我们尝试使用生成的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 { // 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.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); } * 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.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(); } }
当然,如果你无法从中获取任何信息,那么地址簿就不会有多大用处!此示例读取上面示例创建的文件并打印其中的所有信息。
* 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.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 (* 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. * AddressBook addressBook = * AddressBook.parseFrom(new FileInputStream(args[0])); Print(addressBook); } }
在释放使用Protocol Buffer的代码之后,你无疑会想要“改进”Protocol Buffer的定义。如果你希望你的新缓冲区向后兼容,并且你的旧缓冲区是向前兼容的 - 而且你几乎肯定想要这个 - 那么你需要遵循一些规则。在新版本的协议缓冲区中:
如果你遵循这些规则,旧代码将可以阅读新消息并简单地忽略任何新字段。对于旧代码,已删除的可选字段将只具有其默认值,删除的重复字段将为空。新代码也将透明地读取旧消息。但是,请记住旧的消息中不会出现新的可选字段,因此您需要明确检查它们是否设置为 has_
,或者使用 [default = value]
在 .proto
文件中提供合理的默认值。标签号后面。如果未为可选元素指定默认值,则使用特定于类型的默认值:对于string,默认值为空字符串。对于boolean,默认值为false。对于numeric,默认值为零。另请注意,如果添加了新的重复字段,则新代码将无法判断它是否为空(通过新代码)或从未设置(通过旧代码),因为它没有 has_
标志。
Protocol Buffer 的用途不仅仅是简单的访问器和序列化。请务必浏览 Java API参考 ,以了解你可以使用它们做些什么。
Protocol Buffer类提供的一个关键特性是反射。你可以迭代消息的字段并操纵它们的值,而无需针对任何特定的消息类型编写代码。使用反射的一种非常有用的方法是将Protocol Buffer 转换为其他编码,例如XML或JSON。更高级的反射用法可能是找到两个相同类型的消息之间的差异,或者开发一种“Protocol Buffer的正则表达式”,你可以在其中编写与某些消息内容匹配的表达式。如果你运用自己的想象力,可以将Protocol Buffer应用于比你最初预期更广泛的问题!
反射是作为 Message
和 Message.Builder
接口的一部分提供的。