本文譯自:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN
ProtocolBuffer基礎:Java
本指南提供了使用ProtocolBuffer工作的Java編程方法。全文通過一個簡單的示例,向你介紹在Java中使用ProtocolBuffer的方法:
1.如何在.proto文件中定義消息格式;
2.如何使用ProtocolBuffer的creates編譯器;
3.如何使用JavaProtocol Buffer的API來讀寫消息。
本文不是在Java中使用ProtocolBuffer的完整指南,更詳細的信息請參照以下資料:
爲什麼使用ProtocolBuffer
我們使用了一個非常簡單的“地址本”應用的例子,這個應用能夠從一個文件中讀寫個人的聯繫方式信息。在地址本中每個人都有以下信息:姓名、ID、郵件地址、電話號碼。
像這樣的結構化數據應該如何系列化和恢復呢?以下幾種方法能夠解決這個問題:
1.使用Java系列化。因爲它是內置在編程語言中的,所以是默認的方法,但是由於衆所周知的主機問題,並且如果需要在使用不同編程語言(如C++或Python)編寫應用程序之間共享數據,這種方式也不會很好的工作。
2.使用特殊的方式把數據項編碼到一個單獨的字符串中,如把4個整數編碼成“12:3:-23:67”。儘管它需要編寫一次性的編碼和解碼代碼,但是這種方法簡單而靈活,而且運行時解析成本很小。這種方法對於簡單數據是最好的。
3.把數據系列化到XML。因爲XML是可人類可讀的,並且很多編程語言都有對應的功能類庫,所以這種方法非常受歡迎。如果你想要跟其他應用程序/項目共享數據,那麼這種方法是一個非常好的選擇。但是,衆所周知,XML是空間密集性的,並且編解碼會嚴重影響應用程序的性能。此外,XML的DOM樹導航也比一般的類中的字段導航要複雜的多。
ProtocolBuffer是完全解決這個問題的靈活、高效的自動化解決方案。使用ProtocolBuffer,要先編寫一個.proto文件,用這個文件來描述你希望保存的數據結構。然後用ProtocolBuffer編譯器創建一個類,這個類用高效的二進制的格式實現了ProtocolBuffer數據的自動編解碼。生成的類提供了組成ProtocolBuffer字段的getter和setter方法,以及提供了負責讀寫一個ProtocolBuffer單位的方法。重要的是,ProtocolBuffer格式支持向後的兼容性,新的代碼依然可以讀取用舊格式編碼的數據。
什麼地方可以找到示例代碼
示例代碼的源代碼包,可以直接從這兒下載。
定義協議格式
要創建你的地址本應用程序,需要從編寫.proto文件開始。.proto文件的定義很簡單:你要在每個想要系列化的數據結構前添加一個message關鍵字,然後指定消息中每個字段的名稱和類型。以下就是你要定義的.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; }
就像你看到的,語法與C++或Java非常類似,接下來讓我們檢查一下文件的每個部分,並看一下它們都做了些什麼。
.proto文件開始是包聲明,它有助於防止不同項目間的命名衝突。除非你明確的指定了java_package
關鍵字,否則,該包名會被用於生成的Java類文件的包名。即使提供了java_package,依然應該定義一個普通的package,以避免跟ProtocolBuffer命名空間以及非Java語言中的命名衝突。
在包聲明之後,有兩個可選的Java規範:java_package和java_outer_classname。java_package指定要生成的Java類的包名。如果沒有明確的指定這個關鍵字,它會簡單的用package關鍵字的聲明來作爲包名,但是這些名稱通常不適合做Java的包名(因爲它們通常不是用域名開頭的)。java_outer_classname可選項定義了這個文件中所包含的所有類的類名。如果沒有明確的給出java_outer_classname定義,它會把文件名轉換成駝峯樣式的類名。如,“my_proto.proto”文件,默認的情況下會使用MyProto作爲外部的類名。
接下來是消息定義,一個消息包含了一組類型字段。很多標準的簡單數據類型都可以作爲有效的字段類型,包括:bool、int32、float、double和string。還可以是其他消息類型作爲字段類型---在上面的示例中,Person消息包含了PhoneNumber消息,而AddressBook消息又包含了Person消息。甚至還可以定嵌套在其他消息內的消息類型---如,PhoneNumber類型就被定義在Person內。如果想要字段有一個預定義的值列表,也可以定enum類型---上例中電話號碼能夠指定MOBILE、HOMEWORK三種類型之一。
每個字段後標記的“=1”、“=2”,是在二進制編碼時使用的每個字段的唯一標識。在編碼時,數字1~15要比大於它們的數字少一個字節,因此,作爲一個優化選項,可以把1~15的數字用於常用的或重複性的元素。大於等於16的數字儘可能的用於那些不常用的可選元素。在重複字段中的每個元素都需要預定義一個標記數字,因此在重複性字段中使用這種優化是良好的選擇。
每個字段必須用以下修飾符之一來進行標註:
1.required:用這個修飾符來標註的字段必須給該字段提供一個值,否則該消息會被認爲未被初始化。嘗試構建一個未被初始化的消息會拋出一個RuntimeException異常。解析未被初始化的消息時,會拋出一個IOException異常。其他方面,該類型字段的行爲與可選類型字段完全一樣;
2.optional:用這個修飾符來標註的字段可以設定值,也可以不設定值。如果可選字段的。值沒有設定,那麼就會使用一個默認的值。對於簡單類型,能夠像上例中指定電話號碼的type那樣,指定一個默認值。否則,系統使用的默認值如下:數字類型是0、字符串類型是空字符串、布爾值是false。對於內嵌的消息,默認值始終是“默認的實例“或”消息的“原型”,其中沒有字段設置。調用沒有明確設置值的字段的獲取值的訪問器的時候,會始終返回字段的默認值。
3.repeated:用這個修飾符來標註的字段可以被重複指定的數字的次數(包括0)。重複值的順序會被保留在ProtocolBuffer中。重複字段跟動態數組很像。
對於標記爲required的字段要始終小心。如果在某些時候,你希望終止寫入或發送一個required類型的字段,那麼在把該字段改變成可選字段時,就會發生問題---舊的版本會認爲沒有這個字段的消息是不完整的,並且會因此而拒絕或刪除它們。因此應該考慮使用編寫應用程序規範來定製Buffer的驗證規則來代替。Google的一些工程師認爲使用required,弊大於利,他們建議只使用optional和repeqted。但實際上是行不通的。
在ProtocolBuffer語言指南中,你會找到完成.proto文件編寫指南---包括所有可能的字段類型。不要尋求類的繼承性,ProtocolBuffer是不支持的。
編譯ProtocolBuffer
現在有一個.proto文件了,接下來要做的就是生成一個讀寫AddressBook(包括Person和PhoneNumber)消息的類。運行ProtocolBuffer編譯器protoc來生成與.proto文件相關的類。
首先,需要下載的關於Protobuf的文件:
2.運行編譯器。在cmd命令下,進入protoc.exe的路徑,然後輸入以下命令:
protoc.exe --proto_path=protocolfile --java_out=javafile protocolfile/*.proto
protocolfile表示當前目錄下的一個文件夾,裏面放着你要編譯的.proto的文件。
javafile 表示該當前目錄下的一個文件夾,裏面放着編譯後的java文件。
ProtocolBuffer API
讓我們來看一下生成的代碼,並看一下編譯器都爲你創建了那些類和方法。如果你在看
AddressBookProtos.java
文件,你能夠看到它定義了一個叫做
AddressBookProtos
的類,在
addressbook.proto
文件中指定的每個消息都嵌套在這個類中。每個類都有它們自己的
Builder
類,你能夠使用這個類來創建對應的類的實例。在下文的
Buildersvs.
Messages
章節中,你會找到更多的有關
Builder
的信息。
Message
和
Builder
會給消息的每個字段都生成訪問方法。
Message
僅有
get
方法,而
Builder
同時擁有
get
和
set
方法。以下是
Person
類的一些訪問方法(爲了簡單,忽略了實現):
(另外一個例子:http://www.cnblogs.com/stephen-liu74/archive/2013/01/06/2842972.html 很詳細。點擊打開鏈接)// 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
有
get
和
set
方法:
// 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
樣式的的
get
和
set
方法。對於每個有
get
方法的字段,如果該字段被設置,那麼對應的
has
方法會返回
ture
。最後,每個字段還有一個
clear
方法,它會清除對應字段的設置,讓它們回退到空的狀態。
重複性字段會有一些額外的方法
---Count
方法(它會返回列表的尺寸)、通過索引指定獲取或設定列表元素的
get
和
set
方法、往列表中添加新元素的
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
編譯器生成的消息類都是不可變的。一旦消息對象被構建了,它就不能被編輯了,就像
Java
的
String
。要構建一個消息對象,首先必須構建一個
Builder
,把你選擇的值設置給對應的字段,然後調用
build()
方法。
你可能已經注意到,每個編輯消息的
builder
方法都會返回另外一個
Builder
對象,返回的
Builder
對象實際上與你調用的那個方法的
Builder
對象相同。這主要是爲了能夠在一行中編寫
set
方法提供方便。
以下是創建
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();標準的消息方法
每個消息和構建器類還包含了一些其他的方法,這些方法會幫助你檢查或維護整個消息,這些方法包括:
1.isInitialized():
檢查所有的
required
字段是否都被設置了。
2.toString():
返回一個可讀的消息描述,對於調試特別有用。
3.mergeFrom(Message other):(
只有構建器有這個方法
)
,它會把
other
參數中的內容,用重寫和串聯
的方式合併到本消息中。
Clear():(
只有構建器纔有這個方法
)
,清除所有字段的值,讓它們返回到空的狀態。
這些方法實現的
Message
和
Message.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的應用範圍會比你的初始期望值還要高。 反射是作爲Message和Message.Builder的接口部分來提供的。