Protobuf java基礎
本文檔爲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; |
Person類builder的訪問方法(Person.Builder):
// required string name = 1; |
正如你所見,對於每個域都有簡單的javabean風格的getters和setters。對於具有單一值的類型,有has方法用來表示該值是否有設置。當然也可以通過clear方法來將該字段的值清空。
重複域也有額外的方法,如count方法用來統計當前重複域的大小,getters和setters用於根據索引來獲取或設置值。add方法用於將一個新元素添加到重複域中,addAll方法則將一組元素添加到重複域中。
上述示例中訪問方法的名稱採用了駝峯式命名,對應在.proto文件中採用的是小寫字母+下劃線的命名。這種轉換是由protoc編譯器自動完成的,我們只需要按照這種規約定義.proto文件即可。
l 枚舉和內部類
生成的代碼包含了一個枚舉類型PhoneType,它屬於Person的內部類:
public static enum PhoneType { |
PhoneNumber也是作爲Person的一個內部類而產生的。
l Builders 對Messages
由編譯器自動生成的message類是不可變的,一旦一個message對象構建以後,就象java中的String類一樣是不可變的。創建一個message時,必須首先創建一個builder,設置必須的一些值後,再調用builder的build()方法。
也許你已經注意到了,builder的每個方法在消息修改後又會返回builder,這個返回對象又可以調用其它方法。這種方式對於在同一行操作不同的方法提供了便利。如下的代碼示例,創建一個Person實例。
Person john = |
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; |
l 讀取消息
當然了,如果只有地址薄不能讀取也是一件悲劇的事情,下面的代碼示例就是從文件中讀取該地址薄中的個人詳細信息。
import com.example.tutorial.AddressBookProtos.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消息。一個更高級的反射應用即可以發現同一類型消息的差異,或者是採用一系列正則表達式來匹配一定的消息內容。充分發揮想象力,protocol buffer將能夠解決更廣範圍的問題。其中反射是作爲Message及Message.Builder的接口的一部分而提供的。