原文地址:
https://developers.google.com/protocol-buffers/docs/javatutorial
這個教程為Java開發(fā)者使用protocol buffers工作提供一個基本的介紹。通過創(chuàng)建一個簡單的示例程序刽辙,向你展示如何:
- 在一個.proto文件中定義message格式
- 使用protocol buffer編譯器
- 使用Java protocol buffer的API來讀寫message
這不是一個深入介紹如何通過Java使用protocol buffers的教程铃慷。你可以通過Protocol Buffer Language Guide, the Java API Reference, the Java Generated Code Guide, and the Encoding Reference.來獲取更多的參考信息纸肉。
為什么要使用Protocol Buffers端逼?
我們將要使用的例子是一個非常簡單的“地址簿”應用颜矿,這個應用可以從一個文件里讀寫人們的聯(lián)系方式洗贰。每個在地址簿中的人都有一個名字找岖, 一個ID, 一個email地址敛滋,和一個聯(lián)系電話號碼许布。
你是如何序列化并尋回像這樣的結構化數(shù)據(jù)的呢?有這樣一些方法可以解決這個問題:
- 使用Java序列化绎晃。這是默認的方式蜜唾,因為它是內建于語言的帖旨,但是它有諸多的眾所周知的問題(參見Effective Java,by Josh Bloch pp. 213),并且如果你需要與C++或者Python寫成的應用共享數(shù)據(jù)的時候灵妨,這種方式并不能很好的工作解阅。
- 你可以發(fā)明一種ad-hoc的方式來將數(shù)據(jù)項編碼成一個單獨的串--比如編碼 4 ints為“12:3-23:67”。這是一種簡單并且靈活的方式泌霍,雖然它需要寫編碼和轉碼的代碼货抄,并且轉碼的過程會消耗一些運行時間。這種方式對于編碼一些簡單的數(shù)據(jù)是最好的朱转。
- 將數(shù)據(jù)序列化為XML蟹地。這種方式是非常吸引人的,因為XML幾乎是人類可讀的藤为,并且許多語言都有支持XML的庫可以使用怪与。如果你想同其它應用/項目共享數(shù)據(jù),這將是一個很好的選擇缅疟。然而分别,XML的臭名昭著的空格密集型,使得編碼和解碼的過程給應用帶來巨大的性能問題存淫。與此同時耘斩,導航一個XML DOM樹比起導航一個類中簡單的域屬性來說會帶來更大的復雜性。
Protocol buffers是靈活桅咆,高效括授,自動化的解決方案,來解決這個問題岩饼。通過protocol buffers荚虚,你寫一個.proto的描述來描述你想要存儲的數(shù)據(jù)結構。proto buffer編譯器會通過這個.proto創(chuàng)建一個類籍茧,這個類實現(xiàn)了自動化的編碼和將proto buffer數(shù)據(jù)的轉化為高效的二進制格式版述。生成的類為域屬性提供了getters和setters,并實現(xiàn)將proto buffer作為一個單元來進行讀寫的功能硕糊。更重要的是院水,proto buffer格式支持擴展格式,因此简十,使用這種方式的代碼依舊能夠讀取通過舊格式編碼的數(shù)據(jù)檬某。
哪里可以找到示例代碼?
示例代碼是被包含在源碼包中的螟蝙,在名為“example”目錄下面恢恼。點擊下載
定義你的Protocol格式
為了創(chuàng)建你的地址簿應用,你將需要從一個.proto
文件開始胰默。在.proto
文件中定義很簡單:你為每一個想要序列化的數(shù)據(jù)結構添加一個message场斑,然后在message中為每一個屬性域指定一個name和一個type漓踢。下面是定義了你的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 name
被用來作為Java的package脖隶,除非你詳細指定了一個java_package
扁耐,正如我們在這里是這樣做的。即使你卻是提供了一個java_package
产阱,你也應該仍舊定義一個正常的package
以便在Protocol Buffers命名空間和非Java語言中避免命名沖突婉称。
在包聲明以后,你會看到兩個Java特有的配置項:java_package
和java_outer_classname
构蹬。java_package
指定你生成的類要存在于什么包下王暗。如果你沒有具體的指定,它會簡單的與package
給出的package name
相匹配怎燥,但是這些名字通常不適合做為Java包的名字(因為它們通常不以域名開頭)瘫筐。java_outer_classname
配置項定義了那些在這個文件中包含所有類的名稱。如果你沒有具體指定java_outer_classname
铐姚,它將會轉換文件名為駝峰式來產生。例如肛捍,“my_proto.proto”將默認使用“MyProto”做為outer class name
(類的文件名)隐绵。
接下來是你的message定義。一個message只是一系列具有類型的域的集合拙毫。許多標準的簡單類型可以做為可用的域類型依许,包括bool
, int32
, float
, double
, 和string
。你同樣可以向你的message中添加更多的結構缀蹄,通過把其他的message類型當做域類型使用--在上面的例子中峭跳,Person message包含PhoneNumber message,而AddressBook message包含Person message缺前。你甚至可以通過內部嵌套其它message的方式定義message蛀醉,你可以看到,PhoneNumber類型被定義在Person里面衅码。你同樣可以定義enum
類型拯刁,如果你希望其中的一個你的域擁有預定義列表中的一個值--在這里,你希望指定一個phone number的值是MOBILE
, HOME
, 或者WORK
中的一個逝段。
這里在每個元素上面的“=1”垛玻,“=2”標記割捅,辨識唯一的“tag”,這些“tag”是域在二進制編碼的時候使用的帚桩。Tag編號從1到15亿驾,需要比更大的數(shù)字少一個字節(jié),因此账嚎,出于優(yōu)化考慮你可以決定使用常用的或者重復的元素颊乘。而從16到更高的編號留給不常用的可選元素。在重復域中的每一個元素都需要重新編碼tag number醉锄,所以乏悄,使用重復域對優(yōu)化來說是特別好的選擇。
每一個域都必須被下面之一的modifer注解:
-
required
:域的值必須被提供恳不,否則message會被認為是“未初始化的”檩小。試圖創(chuàng)建一個未初始化的message將會拋出一個RuntimeException
。轉化一個未初始化的message將會拋出一個IOException
烟勋。除了這些规求,required
域的行為和optional
域表現(xiàn)相同。 -
optional
:該域可能被設置卵惦,也可能未被設置阻肿。如果一個optional
域未被設置,默認的值將會被使用沮尿。對于簡單類型來說丛塌,你可以指定你自己的默認值,就像我們在例子中對phone number類型所做的那樣畜疾。否則赴邻,一個系統(tǒng)默認的值將被使用:數(shù)值類型是0,字符串類型是""啡捶,布爾類型是false姥敛。對于嵌入式的message來說,默認值總是這個message的沒有任何域被設置過的“默認實例”或“原型”瞎暑。調用訪問器去獲取一個optional
或者required
域的值彤敛,這些域如果沒有被具體設置,那么總是返回域的默認值了赌。 -
repeated
:域將會被重復任意次數(shù)(包括0次)墨榄。重復的值的順序將會被保存在protocol buffer中∽岵穑可以把重復的域想成變長的數(shù)組渠概。
Required是永久的。你在將域標記成required的時候需要很小心。如果在某些時候你希望停止寫入或發(fā)送一個required域播揪,它將會易出問題地將域轉為一個optional域--以前的讀取器將認為沒有這個域的message是不完整的贮喧,并且可能無意中將它們拒絕或者丟棄。取而代之地猪狈,你應該考慮為你的buffer寫應用特有的自定義校驗規(guī)則箱沦。在Google的一些工程師得出了結論就是:與帶來的益處相比,使用
required
帶來的壞處會更多雇庙。他們傾向于只使用optional
和repeated
谓形。然而,這種情景并不是普遍的疆前。
你將找到一個完整的教程關于寫.proto
文件--包括所有可能的域類型--在Protocol Buffer Language Guide寒跳。不要試圖尋找類似于類繼承機制的組件,因為protocol buffer不做這些竹椒。
編譯你的Protocol Buffers
現(xiàn)在你有一個.proto
童太,接下來你需要生成類,這些類是你需要讀寫AddressBook
(當然也包括Person
和PhoneNumber
) message用的胸完。為了做到這點书释,你需要運行proto buffer的編譯器protoc
在你的.proto
上:
- 如果你還沒有安裝編譯器,下載這個包并按照README中介紹的步驟操作赊窥。
- 現(xiàn)在爆惧,運行編譯器,指定源目錄(你應用的源代碼所在的地方--如果你不提供這個值锨能,將使用當前目錄)扯再,目標目錄(你希望生成的代碼所在的地方,通常與
$SRC_DIR
相同)腹侣,并你的.proto
的路徑叔收。在這個例子中,你運行:
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
因為你想要Java類傲隶,所以使用--java_out
選項--其它被支持的語言的該選項相似。
這將生成com/example/tutorial/AddressBookProtos.java
在你指定的目標目錄中窃页。
Protocol Buffer API
讓我們來看一些生成的代碼跺株,并看看編譯器都為你生成了什么類和方法。如果你看看AddressBookProtos.java
文件脖卖,你能看到它定義了一個叫做AddressBookProtos
的類乒省,嵌套在其中的是你在addressbook.proto
中指定的每一個類。每一個類都有自己的Builder
類畦木,通過這個類你可以創(chuàng)建那個類的實例袖扛。你可以在下面的Builders vs. Messages
找到關于builder的更多信息。
message和builder都有自動生成的為message中每個域準備的訪問器方法。message只有getter方法蛆封,而builder同時有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 phone = 4;
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
public PhoneNumber getPhone(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 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();
你可以看到惨篱,每一個域都有簡單的JavaBeans風格的getters和setters方法盏筐。如果一個域被設置了值,同樣會有getters為每一個單獨的域砸讳。最后琢融,每一個域都有一個clear
方法,用來將域設置回原來的空狀態(tài)簿寂。
Repeated
域有一些額外的方法--一個Count
方法(用來速記列表的大醒А),getters和setters用來通過下標常遂,get或者set一個具體的元素纳令。一個add
方法用來向列表中追加一個新元素。一個addAll
方法用來追加整個容器中的元素到列表中烈钞。
請注意這些訪問器方法是如何使用駝峰式的命名泊碑,即使.proto
文件使用了小寫字母+下劃線的方式。這種格式的轉換是由protocol buffer的編譯器自動完成的毯欣,以便于生成的類可以符合標準的Java風格規(guī)范馒过。你應該總是為.proto
文件中的域名稱使用“小寫字母+下劃線”的方式;這確保了好的命名實踐在所有的生成的語言里酗钞。參考style guide以了解更多好的.proto
風格腹忽。
了解更多關于編譯器對于任何具體的域定義會生成什么樣的成員,請參見Java generated code reference砚作。
枚舉和內嵌類
生成的代碼中包含一個PhoneType
Java 5 enum, 內嵌于Person
:
public static enum PhoneType {
MOBILE(0, 0),
HOME(1, 1),
WORK(2, 2),
;
...
}
內嵌的類型Person.PhoneNumber
被生成窘奏,正如你所期望的那樣,作為一個內嵌類在Person
中葫录。
Builders vs. Messages
protocol buffer生成的所有message類都是不可變的(immutable)着裹。一旦一個message對象被構建,它就不能被修改米同,就像Java的String類型一樣蜕煌。為了構建一個message练般,你必須首先構建一個builder潮尝,給任何域設置你想要設置的值邑退,然后調用builder的build()
方法。
你或許已經注意到builder的每一個修改message的方法都會返回一個新的builder熬苍。返回的對象和你調用方法時使用的其實是同一個builder稍走。它被返回是為了方便,使你能夠將若干setters組成一串,在代碼中書寫為一行(譯者注:鏈式編程)婿脸。
這里是一個你如何創(chuàng)建一個Person
實例的例子:
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();
標準的Message方法
每一個message和builder類也包含一些其它的方法粱胜,這些方法使你可以檢查或操作整個message,其中包括:
-
isInitialized()
:檢查是否所有的required域都已經被設置過盖淡。 -
toString()
:返回一個便于人眼閱讀的message的展示年柠,在debug的時候特別有用。 -
mergeFrom(Message other)
:(builder特有)合并other
的內容到這個message中褪迟,如果是單數(shù)域則覆蓋冗恨,如果是重復域則追加連接。 -
clear()
:(builder特有)清空所有的域味赃,回到空值狀態(tài)掀抹。
這些方法實現(xiàn)了Message
和Message.Builder
接口,這些接口被所有的Java message和builder共享心俗。更多的信息傲武,請參見complete API documentation for Message。
轉化和序列化
最后城榛,每一個protocol buffer類都有一些方法用來讀寫你使用protocol buffer二進制格式選擇的message揪利。這包括:
-
byte[] toByteArray();
:序列化message并返回一個包含它原始字節(jié)的byte數(shù)組。 -
static Person parseFrom(byte[] data);
:從給出的byte數(shù)組轉化一個message狠持。 -
void writeTo(OutputStream output);
:序列化message疟位,并將其寫入一個OutputStream
。 -
static Person parseFrom(InputStream input);
:從一個InputStream
中讀取并轉化一個message喘垂。
這些只是一些提供的選項來轉化和序列化甜刻。再次參見Message API reference的完整列表。
Protocol Buffers和面向對象Protocol buffer類是基本的不發(fā)揮作用的數(shù)據(jù)持有者(像是C++中的結構體)正勒;它們不創(chuàng)建第一個類成員在一個對象模型中得院。如果你想要向一個生成的類中添加豐富的行為,最好的方式是用一個應用特有的類包含生成的protocol buffer類章贞。這樣做同樣是一個好的方式祥绞,如果你對
.proto
文件的設計沒有控制權的時候(這是說,如果你在從另一個項目中重用一個.proto
文件)鸭限。在這種情況下就谜,你可以用包含的類去精巧地設計一個接口使它更適合你應用特有的環(huán)境:隱藏一些數(shù)據(jù)和方法,暴露便于使用的功能里覆,等等。你絕不應該去通過繼承它們來向生成的類中添加行為缆瓣。這將會破壞內部機制喧枷,并且畢竟不是面向對象的做法。
寫一個Message
現(xiàn)在,讓我們試著使用你的protocol buffer類隧甚。第一件你想讓你的地址簿應用能夠做的事情是向你的地址簿文件寫入個人詳情车荔。為了做這個,你需要創(chuàng)建并安置你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.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();
}
}
讀取一個Message
當然,如果你無法從一個地址簿中獲得任何信息砍艾,那么這個地址簿是沒有多大用處的蒂教。這個例子讀取上個例子中創(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);
}
}
擴展一個Protocol Buffer
當你發(fā)布了使用protocol buffer的代碼后凝垛,毫無疑問你遲早會想改進protocol buffer的定義。如果你想要你新的buffer能夠向后兼容蜓谋,并且你的舊buffer向前兼容(你一定會想這么做)梦皮,那么你就需要遵守下面的一些約定。在新版本的protocol buffer中:
- 你一定不能改變任何已存在的域的tag number桃焕。
- 你一定不能添加或刪除任何required域剑肯。
- 你可以刪除optional域或repeated域。
- 你可以添加新的optional域或repeated域覆旭,但是你必須使用新的tag number(比如:你在這個protocol buffer中從沒使用過的tag number退子,甚至不能是已刪除的域的tag number)。
(關于這些約定有一些例外的情況型将,但它們極少被用到寂祥。)
如果你遵守這些規(guī)則,舊的代碼將會很好地讀取新的message七兜,并且輕易地忽略任何新的域丸凭。對于舊的代碼來說,被刪除的optional域將有它們的默認值腕铸,并且被刪除的repeated域將會為空惜犀。新的代碼將會透明地讀取舊的message。然而狠裹,需要牢記的是新的optional域將不會在舊的message中出現(xiàn)虽界,所以你將需要具體檢查它們是否被has_
設置,或者提供一個可靠的默認值在你的.proto
文件中涛菠,即在tag number后面寫[default = value]
莉御。如果一個optional元素未被具體指定撇吞,那么取而代之地,一個具體類型的默認值將被使用:對于string礁叔,默認值是空串牍颈。對于boolean,默認值是false琅关。對于numeric類型煮岁,默認值是0。同樣記得涣易,如果你添加了一個新的repeated域画机,你的新代碼將不能識別它是被置空(通過新的代碼)還是從來沒有被設置過(通過舊的代碼)。因為都毒,沒有為它提供has_
標記色罚。
高級用法
Protocol buffers有一些超過訪問器和序列化的用法。請確保查看過Java API reference來看看你還能用它做些什么账劲。
Protocol message類提供的一個關鍵特性是反射戳护。你可以迭代一個message中的域,并且不用寫任何與message中類型抵觸的代碼瀑焦,來操作它們的值它們的值腌且。一個非常有用的方法是使用反射來將protocol message轉為或轉自其它的編碼方式,比如:XML或JSON榛瓮。一個更高級的反射用法是查找相同類型的兩個message的不同之處铺董,或者開發(fā)一套“protocol message的正則表達式”,使得你可以通過寫表達式來匹配確定的message內容禀晓。如果發(fā)揮你的想象力精续,通過應用Protocol Buffer去解決一些的問題會遠超出你的預期!
反射被作為Message
和Message.Builder
接口的一部分提供出來粹懒。