為什么使用Protobuf?
本教程翻譯自谷歌開發者官網,原文地址:https://developers.google.com/protocol-buffers/docs/javatutorial。開發學院對其進行了簡單的翻譯和排版,本頁面的內容是根據知識共享屬性3.0許可的,代碼示例是根據Apache 2.0許可的。
Protocol Buffer 基礎
本教程為java程序介紹了Protocol Buffer 的基本知識。通過創建一個簡單的示例程序,它向您展示了如下內容:
在. proto文件中定義消息格式。
使用Protocol Buffer 編譯器。
使用Java protocol buffer API 來寫和讀消息。
這不是在Java中使用Protocol Buffer的全面指南。有關更詳細的參考信息,請參見Protocol Buffer語言指南、Java應用編程接口參考、Java生成代碼指南和編碼參考。
為什么使用Protocol Buffers?
我們將要使用的例子是一個非常簡單的“地址簿”程序,它可以讀寫文件中的聯系人詳細信息。通訊簿中的每個人都有一個姓名、一個身份證、一個電子郵件地址和一個聯系電話號碼。
如何序列化和檢索這樣的結構化數據?有幾種方法可以解決這個問題:
使用Java序列化。這是默認的方法,因為它內置於語言中,但是它有許多眾所周知的問題,如果您需要與用C與C++程序設計學習與實驗系統或Python編寫的應用程序共享數據,這種方法也不能很好地工作。
您可以發明一種特殊的方法將數據項編碼成單個字符串,例如將4個int編碼為“12:3:-23:67”。這是一種簡單而靈活的方法,盡管它確實需要編寫一次性編碼和解析代碼,而且解析會帶來很小的運行時成本。這對於編碼非常簡單的數據最有效。
將數據序列化為XML。這種方法可能非常有吸引力,因為XML(某種程度上)是人類可讀的,並且有許多語言的綁定庫。如果您想與其他應用程序/項目共享數據,這可能是一個不錯的選擇。然而,眾所周知,XML占用大量空間,編碼/解碼它會給應用程序帶來巨大的性能損失。此外,導航一個XML DOM樹比導航一個類中的簡單字段要復雜得多。
Protocol buffers是解決這個問題的靈活、高效、自動化的解決方案。使用Protocol Buffer,您可以編寫一個想要存儲的數據結構的. proto描述。為此,Protocol Buffer編譯器創建了一個類,該類以有效的二進制格式實現Protocol Buffer數據的自動編碼和解析。生成的類為組成Protocol Buffer的字段提供了獲取器和設置器,並作為一個單元處理讀寫Protocol Buffer的細節。重要的是,Protocol Buffer格式支持隨着時間的推移擴展格式的想法,使得代碼仍然可以讀取用舊格式編碼的數據。
第一步:編寫.proto文件
要創建地址簿程序,您需要從. proto文件開始。. proto文件中的定義很簡單:為要序列化的每個數據結構添加一條消息,然后為消息中的每個字段指定一個名稱和類型。下面是定義好的消息文件,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。讓我們檢查一下文件的每個部分,看看它有什么作用。
.proto文件以包聲明開始,這有助於防止不同項目之間的命名沖突。在java中,包名被用作Java包,除非您已經明確指定了一個Java包,就像我們在這里所做的那樣。即使您提供了一個java包,您也應該定義一個普通的包,以避免在Protocol Buffers名字空間和非Java語言中的名字沖突。
在包聲明之后,您可以看到兩個特定於Java的選項:java_package和java_outer_classname。java_package指定生成的類應該以什么樣的java包名存在。如果沒有明確指定,它只是與包聲明中給出的包名相匹配,但是這些名稱通常不是合適的Java包名(因為它們通常不以域名開頭)。java_outer_classname選項定義了應該包含該文件中所有類的類名。如果沒有明確給出java_outer_classname,它將通過將文件名轉換為camel大小寫來生成。例如,默認情況下,“my_proto.proto”將使用“MyProto”作為外部類名。
接下來,您需要定義消息。消息只是包含一組類型化字段的集合。許多標准的簡單數據類型作為字段類型可用,包括bool、int32、float、double和string。您還可以通過使用其他消息類型作為字段類型來為消息添加進一步的結構,在上面的示例中,Person消息包含PhoneNumber消息,而AddressBook消息包含Person消息。您甚至可以定義嵌套在其他消息中的消息類型,如您所見,PhoneNumber類型是在Person中定義的。如果您希望某個字段具有預定義的值列表之一,也可以定義枚舉類型,在這里,您希望指定電話號碼可以是MOBILE, HOME或WORK之一。
每個元件識別上的" = 1", " = 2"標記是字段在二進制編碼中使用的唯一“標簽”。標簽號1-15需要比更高的數字少一個字節來編碼,因此作為一種優化,您可以決定將那些標簽用於常用或重復的元素,而將標簽號16和更高的用於不常用的可選元素。重復字段中的每個元素都需要重新編碼標簽號,因此重復字段是這種優化的特別好的候選者。
每個字段必須用以下修飾符之一進行注釋:
-
required: 必須為字段提供一個值,否則消息將被視為“uninitialized”。試圖構建未初始化的消息將引發RuntimeException。解析未初始化的消息將引發IOException。除此之外,必填字段的行為與可選字段完全一樣。
-
optional: 該字段可以被設置,也可以不被設置。如果未設置可選字段值,則使用默認值。對於簡單類型,您可以指定自己的默認值,就像我們在示例中對電話號碼類型所做的那樣。否則,將使用系統默認值:數字類型為零,字符串為空字符串,布爾值為假。對於嵌入式消息,默認值始終是消息的“default instance”或“prototype”,其中沒有設置任何字段。調用訪問器來獲取未顯式設置的optional (或required)字段的值總是返回該字段的默認值。
-
repeated: 該字段可以重復任何次數(包括零)。重復值的順序將保留在Protocol Buffer中。將重復字段視為動態大小的數組。
Required是永久性的。在根據需要標記字段時,您應該非常小心。如果您希望在某個時候停止寫入或發送Required字段,將該字段更改為optional 將會有問題,舊的接收者會認為沒有該字段的郵件不完整,可能會無意中拒絕或丟棄它們。您應該考慮為您的Protocol Buffer編寫特定於應用程序的自定義驗證例程。谷歌的一些工程師得出結論,使用Required弊大於利;他們更喜歡只使用optional和repeated。然而,這種觀點並不普遍。
你會找到完整的寫作指南。Protocol Buffer 語言指南中的原型文件,包括所有可能的字段類型。但是,別嘗試尋找類似於類繼承的工具,Protocol Buffer 不會這樣做。
第二部:生成Java類
現在我們定義好了.proto,我們需要做的下一件事就是生成需要讀寫AddressBook(以及Person和PhoneNumber)信息的類。為此,您需要在您的.proto上運行Protocol Buffer編譯器:
如果您尚未安裝編譯器,請下載軟件包,並按照自述文件中的說明操作。
現在運行編譯器,指定源目錄(您的應用程序的源代碼所在的位置,如果不提供值則使用當前目錄)、目標目錄(您希望生成的代碼所在的位置;通常與$SRC_DIR相同),以及到您的. proto的路徑,下面是命令:
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
因為我們想要生成Java類,所以使用了 --java_out選項。其他受支持的語言也提供了類似的選項。
上述命令將在指定的目標目錄中生成com/example/tutorial/AddressBookProtos . Java。
讓我們看一下生成的代碼,看看編譯器為您創建了哪些類和方法。打開AddressBookProtos.java,你會發現它定義了一個名為AddressBookProtos的類,嵌套在其中的是你在addressbook.proto中指定的每條消息的一個類。每個類都有自己的Builder類,你可以用它來創建該類的實例。您可以在下面的Builders vs. Messages部分找到更多關於生成器的信息。
消息和生成器都為消息的每個字段自動生成訪問者方法;消息只有獲取者,而構建者既有獲取者又有設置者。下面是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);
與此同時,erson.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。每個特殊字段也有setter,如果該字段已設置,setter、
返回真。最后,每個字段都有一個清晰的方法,可以將字段重新設置為空狀態。
重復字段有一些額外的方法:計數方法(這只是列表大小的簡寫)、通過索引獲取或設置列表中特定元素的getter和setter、向列表中添加新元素的添加方法以及向列表中添加一個裝滿元素的完整容器的添加所有方法。
請注意這些訪問器方法如何使用camel-case命名,即.proto文件使用帶下划線的小寫字母。這種轉換由protocol buffer編譯器自動完成,以便生成的類與標准的Java風格約定相匹配。您應該始終在.proto文件中使用帶下划線的小寫字母作為字段名;這確保了所有生成語言的良好命名實踐。
有關protocol buffer編譯器為任何特定字段定義生成的確切成員的更多信息,請參見Java生成的代碼引用。
枚舉和內部類
生成的代碼包括一個嵌套在Person中的PhoneType枚舉:
public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}
正如您所料,嵌套類型Person.PhoneNumber 是作為Person中的嵌套類生成的。
生成器 vs 消息
protocol buffer編譯器生成的消息類都是不可變的。一旦消息對象被構造,就不能像Java字符串一樣被修改。要構造消息,您必須首先構造一個生成器,將您想要設置的任何字段設置為您選擇的值,然后調用生成器的build()方法。
您可能已經注意到,每個修改消息的生成器方法都會返回另一個生成器。返回的對象實際上是您調用方法的同一生成器。返回它是為了方便,這樣您可以在一行代碼中將幾個setters串在一起。
下面是一個如何創建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();
標准消息方法
每個消息和生成器類還包含許多其他方法,可以讓您檢查或操作整個消息,其中包括:
isInitialized(): 檢查是否所有必填字段都已設置。
toString(): 返回友好的字符串消息,對調試特別有用。
mergeFrom(Message other): (僅生成器) 將其他的內容合並到此消息中,覆蓋單個標量字段,合並復合字段,並連接重復的字段。
clear(): (僅生成器) 將所有字段清除回空狀態。
這些方法實現了消息和消息。所有Java消息和生成器共享的生成器接口。有關更多信息,請參見完整的消息應用編程接口文檔。
解析和序列化
最后,每個協議緩沖區類都有使用協議緩沖區二進制格式寫入和讀取所選類型消息的方法。其中包括:
byte[] toByteArray();: 序列化消息並返回包含原始字節的字節數組。
static Person parseFrom(byte[] data);: 解析給定字節數組中的消息。
void writeTo(OutputStream output);: 序列化消息並將其寫入輸出流。
static Person parseFrom(InputStream input);: 讀取並解析來自輸入流的消息。
這些只是為解析和序列化提供的幾個選項。同樣,有關完整列表,請參見消息應用編程接口參考。
Protocol Buffers和O-O設計Protocol buffer類基本上是啞數據持有人(像C語言中的結構);在實物模型中,他們不是優秀的一等公民。如果您想向生成的類添加更豐富的行為,最好的方法是將生成的Protocol Buffer類包裝在特定於應用程序的類中。如果您不能控制.proto文件(例如,如果您正在重用另一個項目中的一個文件)。在這種情況下,您可以使用包裝類來創建一個更適合您的應用程序的獨特環境的接口:隱藏一些數據和方法,公開方便的函數,等等。永遠不要通過繼承生成的類來給它們添加行為。這將打破內部機制,無論如何都不是好的面向對象實踐。
第三步:使用生成的類創建對象
現在讓我們嘗試使用生成的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);
}
}
示例代碼
示例代碼在https://developers.google.com/protocol-buffers/docs/downloads.html下載,在"examples" 目錄中。
