標籤:
本文檔為java編程人員使用protocol buffer提供了一個基本的介紹,通過一個簡單的常式進行介紹。通過本文,你可以瞭解到如下資訊:
1、在一個.proto檔案中定義一個資訊格式.
2、使用protoc命令進行編譯,產生java代碼.
3、使用Java protocol buffer API進行讀寫操作.
l 定義proto檔案
以一個地址薄為例,從建立一個.proto檔案開始,為需要序列化的資料介面加入一個message屬性,在message裡面,為每一個欄位指定名稱和類型,如下所示:
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中message定義的文法類似,下面我們來看看每個部分的意義:
為了避免命名衝突,.proto檔案以包聲明開始,在java中除了特別指定一個java_package屬性,否則包名一般為Java的包。正像上面的例子,雖然提供了java_package屬性,你通常還是應該定義package屬性以避免在ProtocolBuffers中命名衝突。包聲明以後,有兩個Java屬性:java_package和java_outer_classname。java_package表示產生的Java代碼的包,如果沒有指定,編譯器會根據package屬性確定包名。java_outer_classname屬性定義組建檔案的類名。如果沒有指定,會根據檔案名稱進行轉換,如:"my_proto.proto"預設會使用MyProto作為外部類名。
接下來是定義message屬性,一個message是包含了各種類型欄位的聚集。有很多標準的變數類型可以使用,包括:bool,int32,float,double和string。你也可以使用其他的message作為欄位類型。正像例子中的Person包含了 PhoneNumber,而AddressBook包含了Persion。甚至可以在message內部定義message,例如:PhoneNumber就是在Persion裡面定義的。你還可以定義enum類型,正像指定電話號碼類型的MOBILE、HOME、WORK。
其中“=1”,“=2”表示每個元素的標識號,它會用在二進位編碼中對域的標識。標識號1-15由於使用時會比那些高的標識號少一個位元組,從最佳化角度考慮,可以將其使用在一些較常用的或repeated元素上,對於16以上的則使用在不常用的或optional的元素上。對於repeated的每個元素都需要重複編碼該標識號,所以repeated的域進行最佳化來說是最顯示的。
每個欄位必須提供一個修飾詞:
Ø required:表示欄位必須提供,不可為空。否則message會被認為是未初始化的,試圖build未初始化的message會拋出 RuntimeException。解析未初始化的message會拋出IOException。除此之外,一個required欄位與optional 欄位完全相同。
Ø optional:可選欄位,可以設定也可以不設定。如果沒有設定,會設定一個預設值。可以指定一個預設值,正像電話號碼的type欄位。否則,使用系統的預設值:數字類型預設為0;字元類型預設為空白串;邏輯類型預設為false;對於嵌入的message,預設值通常是message的執行個體或原型。
Ø repeated:欄位可以被重複(包括0),可等同於動態數組或列表。其中儲存的值列表的順序是被保留的。
Required修飾的欄位是永久性的,在使用該修飾符時一定要特別小心。如果在以後想要修改required域為optional域時會出現問題。對於訪問舊介面的使用者來說沒有該欄位時,將會認為是不合法的訪問,將會被拒絕或丟棄。其中google的一些工程師給出的建議是如果不是必須,就盡量少用required修飾符。
l 編譯Protocol Buffers檔案
既然現在已經有了.proto檔案,接下來就需要利用編譯器protoc對.proto檔案進行編譯,產生具體的java類。就可以讀取及寫入AddressBook、Person及PersonNumber訊息了。
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto |
$SRC_DIR :表示.proto檔案所在目錄;$DST_DIR:產生的java代碼的檔案夾。
編譯成功後,會在指定的目錄下產生Java代碼檔案,包含了對屬性的操作,下一步就可以通過API進行資料的讀寫了。
l Protocol Buffer API使用
接下來具體看一下所產生的java代碼及其中的方法。在AddressBookProtos.java中可以看出,其中的內部類對應的是addressbook.proto中定義的格式。每個類都有它自己的Builder類,通過它即可以建立該類的執行個體。你可以在http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/javatutorial.html#builders中查閱到更多關於builder的資訊。
Messages和Builders都會為每個域建立自動的存取方法,其中messages只有getters,而builders有getters和setters。下面是Person類message的存取方法:
// 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的存取方法(Person.Builder):
// 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風格的getters和setters。對於具有單一值的類型,有has方法用來表示該值是否有設定。當然也可以通過clear方法來將該欄位的值清空。
重複域也有額外的方法,如count方法用來統計當前重複域的大小,getters和setters用於根據索引來擷取或設定值。add方法用於將一個新元素添加到重複域中,addAll方法則將一組元素添加到重複域中。
上述樣本中存取方法的名稱採用了駝峰式命名,對應在.proto檔案中採用的是小寫字母+底線的命名。這種轉換是由protoc編譯器自動完成的,我們只需要按照這種規約定義.proto檔案即可。
l 枚舉和內部類
產生的程式碼包含了一個枚舉類型PhoneType,它屬於Person的內部類:
public static enum PhoneType { MOBILE(0, 0), HOME(1, 1), WORK(2, 2), ; ... } |
PhoneNumber也是作為Person的一個內部類而產生的。
l Builders 對Messages
由編譯器自動產生的message類是不可變的,一旦一個message對象構建以後,就象java中的String類一樣是不可變的。建立一個message時,必須首先建立一個builder,設定必須的一些值後,再調用builder的build()方法。
也許你已經注意到了,builder的每個方法在訊息修改後又會返回builder,這個返回對象又可以調用其它方法。這種方式對於在同一行操作不同的方法提供了便利。如下的程式碼範例,建立一個Person執行個體。
Person john = Person.newBuilder() .setId(1234) .setName("John Doe") .setEmail("[email protected]") .addPhone( Person.PhoneNumber.newBuilder() .setNumber("555-4321") .setType(Person.PhoneType.HOME)) .build(); |
l 標準的Message方法
對於每個message或builder類也包含一些方法用於檢查或操作整個訊息,如:
· isInitialized():檢查是否所有的required欄位已經設定了值;
· toString():返回一個易於閱讀的訊息結果,對於調試來說非常有用;
· mergeFrom(Message other): 將其它內部merger到當前的訊息中,重寫單一範圍或者新增repeated域,僅用於builder。
· clear():將所有域清空設定,僅用於builder。
l 解析及序列化
最終,protocol buffer類就可以通過一些方法來完成訊息的讀寫入及讀取。如:
· byte[] toByteArray():訊息序列化並返回一個位元組數組;
· static Person parseFrom(byte[] data):從一個特定的位元組數組解析成訊息;
· void writeTo(OutputStream output):序列化訊息並將其寫入到OutputStream中;
· static Person parseFrom(InputStreaminput):從InputStream流中讀取並解析訊息。
上述提供的僅僅是解析及序列化的一組介面,可以在http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/java/com/google/protobuf/Message.html中查閱更全面的的介面。
l 寫入訊息
接下來先看如何來用protocol buffer類,對於地址薄應用首先需要將設定檔寫入地址薄中。為了做到這些,需要建立protocol buffer類並將資訊寫入。程式設計如下,會先從一個檔案讀取AddressBook資訊,通過使用者手工輸入一個Person的資訊,交將其回寫至 AddressBook檔案中。程式碼範例如下,其中高亮部分是protobuf自動產生的程式碼。
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(); } } |
l 讀取訊息
當然了,如果只有地址薄不能讀取也是一件悲劇的事情,下面的程式碼範例就是從檔案中讀取該地址薄中的個人詳細資訊。
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); } } |
l 對Protocol Buffer進行擴充
有時會發現在發布完protocolbuffer代碼後,需要對其進行擴充升級。如果想讓新代碼向後相容,而且老代碼能夠向前相容,此時需要遵循以下的規則。
· 不能改變已存在域的標識號;
· 不要任意添加或刪除required修飾的域;
· 可以刪除optional或repeated修飾的域;
· 可以新增optional或repeated修飾的域,但是必須使用新的標識號。
如果按照上述規約進行了升級,舊的代碼將可以讀取新的訊息並將一些新的欄位忽略掉。對於舊代碼,被刪除的optional域將會使用其預設值,刪除的repeated域將會被置空。新代碼中也將能夠透明地讀取舊的訊息,但是有一點需要明確,那就是新的optional域不能出現在舊訊息中,可以通過 has方法進行明確檢查,或者在.proto檔案中為該欄位提供一個預設值。如果一個optional元素沒有明確的聲明預設值的話,則會根據其類型取預設值,如:字串類型,取空串為預設值;布爾類型取false為其預設值;數字類型取0為其預設值。如果新增了一個repeated域,新代碼將不能判斷其是否是空,老代碼也不會設定其值,且它並沒有has方法。
l 進階用法
Protocol Buffers目前已經能夠提供的功能遠超過了上述介紹的簡單訪問及序列化,可以在http://code.google.com/intl/zh-CN/apis/protocolbuffers/docs/reference/java/index.html中發掘更進階的特性。
Protocol訊息類提供的一個主要特性是反射,對於任何具體的訊息類型在不需要寫代碼的情況下就可以迭代其中的域並操控其中的值。其有效應用情境即可將其它編碼(XML、JSON)的訊息轉換成protocol訊息。一個更進階的反射應用即可以發現同一類型訊息的差異,或者是採用一系列Regex來匹配一定的訊息內容。充分發揮想象力,protocol buffer將能夠解決更廣範圍的問題。其中反射是作為Message及Message.Builder的介面的一部分而提供的。
protobuf for java