在Java中使用Protocol Buffers
這份教程為Java開發(fā)者提供了使用 Protocol Buffer 的基本介紹。通過創(chuàng)建一個(gè)簡(jiǎn)單的示例應(yīng)用,它展示了
- 在 .proto 文件中定義消息格式。
- 使用 Protocol Buffer 編譯器。
- 使用Java Protocol Buffer API讀寫消息。
這不是一個(gè)在Java中使用 Protocol Buffer 的全面指南。更多詳細(xì)的信息,請(qǐng)參考Protocol Buffer語(yǔ)言指南, Java API參考,Java Generated Code Guide,和 編碼參考。
為什么使用Protocol Buffers?
我們將使用的例子是一個(gè)非常簡(jiǎn)單的 "address book" 應(yīng)用,它可以從文件讀取和向文件寫入人們的聯(lián)系人詳情。地址簿中的每個(gè)人具有一個(gè)名字 (name),ID,電子郵件地址 (email address),和聯(lián)系人電話號(hào)碼 (contact phone)。
你要如何序列化和提取這樣的結(jié)構(gòu)化數(shù)據(jù)呢?有一些方法可以解決這個(gè)問題:
- 使用Java序列化接口。這是默認(rèn)的方法,因?yàn)樗蔷幊陶Z(yǔ)言內(nèi)建的,但它有一個(gè)廣為人知的問題 (參見Josh Bloch的Effective Java,pp. 213),而且如果你需要與用C++或Python編寫的應(yīng)用共享數(shù)據(jù)時(shí)不能很好的工作。
- 你可以發(fā)明一種特別的方式來將數(shù)據(jù)項(xiàng)編碼為一個(gè)字符串 —— 比如將4個(gè)int值編碼為"12:3:-23:67"。這是一個(gè)簡(jiǎn)單而靈活的方法,盡管它需要編寫一次性的編碼和解析代碼,而且解析消耗一小段運(yùn)行時(shí)代價(jià)。這對(duì)于編碼非常簡(jiǎn)單的數(shù)據(jù)是最好的方式。
- 將數(shù)據(jù)序列化為XML。這種方法可能非常具有吸引力,因?yàn)閄ML是 (有點(diǎn)) 人類可讀的,而且它有大量編程語(yǔ)言的bindings庫(kù)。如果你想要與其它的應(yīng)用/項(xiàng)目共享數(shù)據(jù)的話,這可能是一個(gè)很好的選擇。然而,XML是臭名昭著的空間密集,而且編碼/解碼它需要消耗應(yīng)用大量的性能開銷。而且,瀏覽一個(gè)XML DOM樹也被認(rèn)為比通常瀏覽類中的簡(jiǎn)單字段更復(fù)雜。
Protocol buffers 是解決這個(gè)問題靈活,高效,自動(dòng)化的方案。通過 Protocol buffers ,你可以編寫一個(gè) .proto 描述你想要存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)。通過它, Protocol buffers 編譯器創(chuàng)建一個(gè)類,以一種高效的二進(jìn)制格式實(shí)現(xiàn)自動(dòng)地編碼和解析 Protocol buffers 數(shù)據(jù)。生成的類為構(gòu)成一個(gè) Protocol buffers 的字段提供了getters和setters方法,并處理讀取和寫入 Protocol buffers 的細(xì)節(jié)。重要地是, Protocol buffers 格式通過使代碼依然能夠讀取用老的格式編碼的數(shù)據(jù)來支持隨著時(shí)間對(duì)格式的擴(kuò)展。
在哪里可以找到示例代碼
源碼包中包含的示例代碼,在"examples" 目錄下。在這里下載。
定義你的協(xié)議格式
為了創(chuàng)建你的地址簿應(yīng)用,你需要先創(chuàng)建一個(gè) .proto 文件。 .proto 文件中的定義很簡(jiǎn)單:為每個(gè)你想要序列化的數(shù)據(jù)結(jié)構(gòu)添加一個(gè) 消息(message) ,然后為消息中的每個(gè)字段指定一個(gè)名字和類型。這里是定義你的消息的 .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; }如你所見,語(yǔ)法與C++或Java類似。讓我們看一下這個(gè)文件的每個(gè)部分,并看一下它做了什么。
.proto 文件以一個(gè)包聲明開始,這用于防止不同項(xiàng)目間的命名沖突。在Java中,包名被用作Java包,除非你已經(jīng)顯式地指定了 java_package,如我們這里看到的。即使你不提供 java_package,你依然應(yīng)該定義一個(gè)普通的 package 以避免Protocol Buffers命名空間中的沖突,以及在非Java語(yǔ)言中。
聲明了包之后,你可以看到兩個(gè)Java特有的選項(xiàng): java_package 和 java_outer_classname。java_package 指定生成的類應(yīng)該放在什么Java包名下。如果你沒有顯式地指定這個(gè)值,則它簡(jiǎn)單地匹配由package 聲明給出的Java包名,但這些名字通常都不是合適的Java包名 (由于它們通常不以一個(gè)域名打頭)。 java_outer_classname 選項(xiàng)定義應(yīng)該包含這個(gè)文件中所有類的類名。如果你沒有顯式地給定java_outer_classname ,則將通過把文件名轉(zhuǎn)換為首字母大寫來生成。比如"my_proto.proto",默認(rèn)情況下,將使用 "MyProto" 做為它的外層類的類名。
接下來,定義你的消息。消息只是包含了具有類型的字段的聚合。許多標(biāo)準(zhǔn)的簡(jiǎn)單數(shù)據(jù)類型可用作字段類型,包括bool,int32,float,double,和string。你也可以通過使用消息類型作為字段類型來給你的消息添加更多結(jié)構(gòu) —— 在上面的例子中,Person消息包含了多個(gè)PhoneNumber消息,同時(shí)AddressBook消息包含Person消息。你甚至可以在其它消息中嵌套的定義消息類型 —— 如你所見,PhoneNumber類型是在Person中定義的。如果你想要你的字段值為某個(gè)預(yù)定義的值列表中的某個(gè)值的話,你也可以定義enum類型 —— 這里你想要指定電話號(hào)碼是MOBILE,HOME,或WORK中的一個(gè)。
每個(gè)元素上的 " = 1"," = 2"標(biāo)記標(biāo)識(shí)在二進(jìn)制編碼中使用的該字段唯一的 "tag" 。Tag數(shù)字 1-15 比更大的數(shù)字在編碼上少一個(gè)字節(jié),因而作為一種優(yōu)化,你可以決定將那些數(shù)字用作常用的或重復(fù)的元素的tag,而將16及更大的數(shù)字tag留給更加不常用的可選元素。重復(fù)字段中的每個(gè)元素需要重編碼tag數(shù)字,因而這種優(yōu)化特別適用于重復(fù)字段。
每個(gè)字段必須用下面的修飾符中的一個(gè)來注解:
-
required:字段必須提供,否則消息將被認(rèn)為是 "未初始化的 (uninitialized)"。嘗試構(gòu)建一個(gè)未初始化的消息將拋出一個(gè) RuntimeException。解析一個(gè)未初始化的消息將拋出一個(gè) IOException。此外,required字段的行為與optional字段完全相同。
-
optional:字段可以設(shè)置也可以不設(shè)置。如果可選的字段值沒有設(shè)置,則將使用默認(rèn)值。對(duì)于簡(jiǎn)單的類型,你可以指定你自己的默認(rèn)值,如我們?cè)诶又袨殡娫捥?hào)碼 類型 做的那樣。否則,將使用系統(tǒng)默認(rèn)值:數(shù)字類型為0,字符串類型為空字符串,bools值為false。對(duì)于內(nèi)嵌的消息,默認(rèn)值總是消息的 "默認(rèn)實(shí)例 (default instance)" 或 "原型(prototype)",它們沒有自己的字段集。調(diào)用accessor獲取還沒有顯式地設(shè)置的 optional (或required) 字段的值總是返回字段的默認(rèn)值。
-
repeated:字段可以重復(fù)任意多次 (包括0)。在 protocol buffer 中,重復(fù)值的順序?qū)⒈槐A簟⒅貜?fù)字段想象為動(dòng)態(tài)大小的數(shù)組。
你將找到一個(gè)編寫 .proto 文件的完整指南 —— 包括所有可能的字段類型 —— 在Protocol Buffer Language Guide 一文中。不要尋找與類繼承類似的設(shè)施 —— protocol buffer 不那樣做。
編譯你的Protocol Buffers
現(xiàn)在你有了一個(gè).proto,接下來你需要做的事情是生成讀寫 AddressBook (及Person 和 PhoneNumber) 消息所需的類。要做到這一點(diǎn),你需要在你的 .proto 上運(yùn)行 Protocol Buffers 編譯器protoc:
如果你還沒有安裝編譯器,則下載包,并按照README的指示進(jìn)行。
現(xiàn)在運(yùn)行編譯器,指定源目錄 (放置你的應(yīng)用程序源代碼的地方 —— 如果你沒有提供則使用當(dāng)前目錄),目的目錄 (你希望放置生成的代碼的位置;通常與$SRC_DIR相同),你的.proto的路徑。在這個(gè)例子中,你... :
由于你想要Java類,所以使用 --java_out 選項(xiàng) —— 也為其它支持的語(yǔ)言提供了類似的選項(xiàng)。
這將在你指定的目的目錄下生成com/example/tutorial/AddressBookProtos.java。
Protocol Buffer API
讓我們看一下生成的代碼,并看一下編譯器都為你創(chuàng)建了什么類和函數(shù)。如果查看 AddressBookProtos.java,你可以看到它定義了一個(gè)稱為 AddressBookProtos 的類,其中嵌套了為你在 addressbook.proto 中描述的每個(gè)消息的類。每個(gè)類都有它自己的 Builder 類,你可以用來創(chuàng)建那個(gè)類的實(shí)例。你可以在下面的 Builders vs. Messages 小節(jié)中找到更多關(guān)于builders的信息。
消息和builders具有為消息的每個(gè)字段自動(dòng)生成的accessor方法;消息只有g(shù)etters,而builders則同時(shí)具有g(shù)etters和setters。這里是 Person 類的一些accessors (省略實(shí)現(xiàn)以便于簡(jiǎn)潔):
// 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);同時(shí) Person.Person 類具有相同的getters外加setters:
// 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();如你所見,每個(gè)自動(dòng)都有簡(jiǎn)單的JavaBeans風(fēng)格的getters和setters。每個(gè)單數(shù)的 (required 或 optional) 字段還有 has 方法,如果那個(gè)字段已經(jīng)被設(shè)置了則它們放回true。最后,每個(gè)字段具有一個(gè) clear 方法,用于將字段設(shè)置回它的空狀態(tài)。
重復(fù)的字段還有一些額外的方法 —— 一個(gè) Count 方法(是列表大小的速記),通過索引獲取和設(shè)置列表的特定元素的getters和setters,一個(gè) add 方法,將新元素添加到列表的末尾,及一個(gè) addAll 方法,它將一個(gè)裝滿元素的整個(gè)容器添加到列表中。
注意這些accessor方法是如何以駝峰形式命名的,即使 .proto 文件使用了小寫字母加下劃線。這種轉(zhuǎn)換是由protocol buffer編譯器自動(dòng)地完成的,以產(chǎn)生與標(biāo)準(zhǔn)Java風(fēng)格規(guī)范匹配的類。你應(yīng)該總是在你的 .proto 文件中為字段使用小寫字母加下劃線;這確保了在所有生成的語(yǔ)言中良好的命名實(shí)踐。參考 風(fēng)格指南 來了解更多好的 .proto 風(fēng)格。
關(guān)于protocol編譯器為任何特定的字段定義產(chǎn)生什么成員的更多信息,請(qǐng)參考 Java 生成代碼參考。
枚舉和嵌套類
生成的代碼包含一個(gè)PhoneType Java 5枚舉,嵌套在 Person 中:
public static enum PhoneType {MOBILE(0, 0),HOME(1, 1),WORK(2, 2),;... }生成的嵌套類型 Person.PhoneNumber,如你期待的那樣,是 Person 的嵌套類。
Builders和Messages
由protocol buffer編譯器生成的所有消息類都是不可變的。一旦某個(gè)消息對(duì)象構(gòu)造完成 ,則它不能被修改,如同Java的 String 一樣。要構(gòu)造一個(gè)消息,你必須首先構(gòu)造一個(gè)builder,設(shè)置你想要設(shè)置的字段為你選擇的值,然后調(diào)用builder的 build() 方法。
你可能已經(jīng)注意到了builder的每個(gè)方法都修改消息并返回另一個(gè)builder。返回的對(duì)象實(shí)際上與調(diào)用方法的那個(gè)builder是同一個(gè)。它被返回以使你可以將多個(gè)setters串在一起放在單獨(dú)的一行代碼上。
這里是如何創(chuàng)建你想要的 "Person" 實(shí)例一個(gè)例子:
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();標(biāo)準(zhǔn)的消息方法
每個(gè)消息和builder類還包含大量的其它方法,來讓你檢查或管理整個(gè)消息,包括:
- isInitialized() : 檢查是否所有的required字段都已經(jīng)被設(shè)置了。
- toString() : 返回一個(gè)人類可讀的消息表示,對(duì)調(diào)試特別有用。
- mergeFrom(Message other): (只有builder可用) 將 other 的內(nèi)容合并到這個(gè)消息中,覆寫單數(shù)的字段,附接重復(fù)的。
- clear(): (只有builder可用) 清空所有的元素為空狀態(tài)。
這些方法實(shí)現(xiàn)由所有的Java消息和builders所共享的 Message 和 Message.Builder 接口。更多信息,請(qǐng)參考 Message的完整API文檔。
解析和序列化
最后,每個(gè)protocol buffer類都有使用protocol buffer 二進(jìn)制格式寫和讀你所選擇類型的消息的方法。這些方法包括:
- byte[] toByteArray();: 序列化消息并返回一個(gè)包含它的原始字節(jié)的字節(jié)數(shù)組。
- static Person parseFrom(byte[] data);: 從給定的字節(jié)數(shù)組解析一個(gè)消息。
- void writeTo(OutputStream output);: 序列化消息并將消息寫入 OutputStream。
- static Person parseFrom(InputStream input);: 從一個(gè) InputStream 讀取并解析消息。
這些只是解析和序列化提供的一些選項(xiàng)。再次,請(qǐng)參考 Message API 參考 來獲得完整的列表。
寫消息
現(xiàn)在讓我們?cè)囍褂胮rotocol buffer類。你想要你的地址簿應(yīng)用能夠做的第一件事情是將個(gè)人詳情寫入地址簿文件。要做到這一點(diǎn),你需要?jiǎng)?chuàng)建并放置你的protocol buffer類的實(shí)例,然后將它們寫入一個(gè)輸出流。
這里是一個(gè)程序,它從一個(gè)文件讀取一個(gè)AddressBook,基于用戶輸入給它添加一個(gè)新Person,并再次將新的AddressBook寫回文件。直接調(diào)用或引用由protocol編譯器生成的代碼的部分都被高亮了。
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();} }讀消息
當(dāng)然,如果你不能從地址簿中獲取信息的話,那它就沒什么用了。這個(gè)例子讀取上面例子創(chuàng)建的文件并打印它的所有信息。
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);} }擴(kuò)展一個(gè)Protocol Buffer
在你發(fā)布使用你的protocol buffer的代碼之后或早或完,你都將毫無疑問的想要 "提升" protocol buffer的定義。如果你想要你的新buffers向后兼容,你的老buffers向前兼容 —— 你當(dāng)然幾乎總是想要這樣 —— 然后你有一些規(guī)則要遵守。在新版本的protocol buffer中:
- 你 一定不能 修改任何已有字段的tag數(shù)字。
- 你 一定不能 添加或刪除required字段。
- 你 可以 刪除可選的或重復(fù)的字段。
- 你 可以 添加可選或重復(fù)的字段,但你必須使用新的tag數(shù)字 (比如,從未在這個(gè)protocol buffer中使用過的tag數(shù)字,甚至是在刪除的字段中也是)。
(這些規(guī)則有 一些例外 ,但它們幾乎從未用到)
如果你按照這些規(guī)則,老代碼將開心地讀取新消息并簡(jiǎn)單地忽略新字段。對(duì)于老代碼來說,刪除的可選字段將簡(jiǎn)單的具有它們的默認(rèn)值,刪除的重復(fù)字段將是空的。新代碼將透明地讀取老消息。然而,請(qǐng)記住新的可選字段將不會(huì)出現(xiàn)在老的消息中,因此你將需要通過has_顯式地檢查它們是否設(shè)置了,或通過 [default = value] 在你的 .proto 文件中的tag數(shù)字后面提供一個(gè)合理的默認(rèn)值。如果沒有為可選元素指定默認(rèn)值,則會(huì)使用特定于類型的默認(rèn)值代替:對(duì)于字符串,默認(rèn)值是空字符串。對(duì)于booleans,默認(rèn)值是false。對(duì)于數(shù)字類型,默認(rèn)值是0。還要注意如果你添加了一個(gè)新的重復(fù)字段,你的新代碼將不能區(qū)別他是空的 (通過新代碼) 還是從來沒有設(shè)置 (通過老代碼) ,因?yàn)樗鼪]有 has_ 標(biāo)記。
高級(jí)用法
Protocol buffers的使用場(chǎng)景不僅僅是簡(jiǎn)單的存取器和序列化。務(wù)必瀏覽 Java API 參考 來了解你還可以用它做什么。
由protocol消息類提供的一個(gè)重要功能是 反射 。你可以迭代一個(gè)消息的字段,并在不針對(duì)特定的消息類型編寫你的代碼的情況下,管理它們的值。使用反射的一個(gè)非常有用的方式是將protocol消息轉(zhuǎn)換為其它編碼方式,或從其它編碼方式轉(zhuǎn)換,比如XML或JSON。反射的一個(gè)更高級(jí)的使用可能是查找相同類型的兩個(gè)消息之間的差異,或者開發(fā)某種"protocol消息正則表達(dá)式",你可以編寫表達(dá)式用它匹配某一消息內(nèi)容。如果使用你想象力,則將Protocol Buffers用到比你最初期望的更加廣泛的問題的解決中是有可能的!
反射是作為Message 和 Message.Builder 接口的一部分提供的。
原文
總結(jié)
以上是生活随笔為你收集整理的在Java中使用Protocol Buffers的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在C++中使用Protocol Buff
- 下一篇: 在Android中使用Protocol