Java的ProtoBuf

安裝

protocolbuffer是google 的一種數據交換的格式,它獨立於語言,獨立於平臺。google 提供了三種語言的實現:javac++ 和 python,每一種實現都包含了相應語言的編譯器以及庫文件。由於它是一種二進制的格式,比使用 xml 進行數據交換快許多。可以把它用於分佈式應用之間的數據通信或者異構環境下的數據交換。作爲一種效率和兼容性都很優秀的二進制數據傳輸格式,可以用於諸如網絡傳輸、配置文件、數據存儲等諸多領域

本篇博客主要教大家如何在windows7下安裝Java的protocol buffer(具體使用及注意事項將會在下一篇博客當中進行詳細介紹) 

首先,要使用protocol buffer得保證maven安裝成功,maven的下載地址:http://maven.apache.org/download.cgi 。

1.解壓完之後請將maven的bin目錄配置到你的環境變量當中。

2.請確保你的JAVA_HOME的變量是指向你的JDK的主目錄,如果你的系統變量中沒有JAVA_HOME這一項,請點擊新建添加。

3.打開命令行,輸入“mvn --version”如果輸出正確則表示安裝成功

安裝完maven之後就要進行protocol buffer的安裝了,下載地址: http://code.google.com/p/protobuf/downloads/list 。下載protobuf-2.4.1.zip 和 protoc-2.4.1-win32.zip 兩個包。

1. 解壓完成之後有兩種選擇,第一:將protoc-2.4.1-win32中的protoc.exe所在的目錄配置到環境變量當中,第二:將protoc.exe拷貝到c:\windows\system32目錄下,這裏推薦第二種做法。

2. 將proto.exe文件拷貝到解壓後的protobuf-2.4.1\src目錄中.

3. 進入protobuf-2.4.1\java 目錄  執行mvn package命令編輯該包,系統將會在target目錄中生成protobuf-java-2.4.1.jar文件(注意運行時需要聯網,首次安裝可能需要一定的時間)。

4. 假設你的數據文件目錄在XXX\data目錄,把上一步生成的jar拷貝到該目錄中即可。

5. 進入XXX\protobuf-2.4.1\examples目錄,可以看到addressbook.proto文件,在命令行中執行 protoc --java_out=. addressbook.proto 命令(特別注意. Addressbook.proto中間的空格,我第一次安裝就因爲沒注意而反覆失敗),如果生成com文件夾並且最終生成AddressBookProtos類則說明安裝成功。

6. 打開eclipse,選擇windows-->preferences-->java-->Installed JREs編輯你默認的java源碼包,並將上面所提到的protobuf-java-2.4.1.jar文件添加進去。

使用

創建一個.proto文件並在裏面定義消息格式把你需要序列化的數據在裏面聲明類型和名稱,並將這個文件命名爲addressbook.proto。具體內容如下:

Proto代碼  收藏代碼
  1. package tutorial;  
  2.   
  3. option java_package = "com.example.tutorial";  
  4. option java_outer_classname = "AddressBookProtos";  
  5.   
  6. message Person{  
  7.     required string name = 1;  
  8.     required int32  id =2;  
  9.     optional string email = 3;  
  10.       
  11.     enum PhoneType{  
  12.         MOBILE = 0;  
  13.         HOME = 1;  
  14.         WORK = 2;  
  15.     }  
  16.       
  17.     message PhoneNumber{  
  18.         required string number = 1;  
  19.         optional PhoneType type  = 2 [default = HOME];  
  20.     }  
  21.       
  22.     repeated PhoneNumber phone = 4;  
  23. }  
  24.   
  25. message AddressBook{  
  26.     repeated Person person = 1;  
  27. }  

正如你所看到的,它的語法類似C++或者Java,讓我們通過該文件的每一個部分來分析一下吧。

首先,這個文件以一個package的定義開頭,以防在不同工程中的命名衝突,在Java裏面,package就是用來當做Java package(除非你有明確的定義一個java package)。 不過,在這裏,即使你已經提供了一個java_package,你仍然需要定義一個package以防Protocol Buffer使用在其他沒有Java語言的環境中。

在package的定義之後,你可以看到兩個option(選項):java_package以及java_outer_classname。java_package是用來定義java類應該在哪個包裏面生成,如果你沒有寫這一項的話,那麼他默認的會以你的package的定義來生成。Java_outer_classname這一項是用來定義在這個文件中的哪一個類需要包含所有的這些類的信息,如果你不定義這一項的話,那麼它會以你的文件名來作爲刻板,比如說如果你寫的是“my_proto.proto”的話,那麼它就會默認的使用“MyProto”來作爲類名。

接下來就是你消息的類型定義了。一個message就是一系列類型領域的集合,許多基礎的數據類型在這裏面都是可用的,包括bool,int32,float,double,以及string等。你同樣可以添加更多的結構化的數據在裏面,比如上面的Person message就包含了PhoneNumber message,而AddressBook message包含了Person message。你同樣在一中message類型裏面定義另外一種類型,例如上面所舉到的,Person裏面所定義的enum(枚舉)類型,以區分PhoneNumber的不同類型。

在每一個元素之後的“=1”,“=2”是用來區分它們的獨特的“標籤”。標籤數字1-15編碼所需的字節數比更高的數字所需的字節數要少一個,所以爲了使程序達到最佳的狀態,你可以使用這些標籤進行反覆標記(在同一個域中不能重複)。每一個元素在重複的領域都需要重新編碼這些“標籤”,所以重複的領域應該考慮到更多可能的方案來達到最佳狀態。

每一個域的前面都必須使用下面這些修飾符來修飾:

·required(必需的): 這說明這個域的值不能爲空,否則這條message將會被當做“不知情的”。如果你嘗試的創建一條“不知情的”message,那麼系統將會拋出一個RuntimeException(運行時異常)。而分析一條“不知情的”message則會拋出IOException。除此之外,確切的來說一個被required修飾的域其行爲則更接近optional修飾的域。 

·optional(可選擇的): 被這個修飾符修飾的域可以爲空。如果一個被optional修飾的域沒有設值的話,那麼系統將會使用默認值。對於一些基本類型來說,你可以定義你自己的默認值(就像我前面在定義PhoneNumber的PhoneType時一樣)。 否則,對於數值類型來說,系統的默認值是0,string的默認值是empty string,bool的默認值是false。對於植入的message來說(比如AddressBook裏面的Person),默認值則經常是該消息默認的實例或者標準。調用存取器去獲取那些被optional(或者required)修飾的但還沒有被初始化的域將會返回它的默認值。

·repeated(反覆的): 某一個域可能會被使用多次,而那些反覆使用的值將會被保留在protocol buffer裏面。 你可以把用repeated修飾的域想象成動態數組。

 

Required是“永久”的。

當你使用required來修飾域的時候你必須非常的小心。如果某些時候你想要停止發送一個用required修飾的域並將它修改爲optional修飾時,之前的readers會把你的message考慮爲不完整的並且無意識的丟棄它。事實上,人們已經開始注意到了使用required的所帶來的危害,所以我們應該更多的使用optional和repeated。


使用Protocol buffer的編譯器


    現在,你已經有一個.proto文件了,接下來就需要生成class來發送和讀取AddressBook消息(包含Person以及PhoneNumber),爲了完成這件工作,你需要調用protocol buffer的編譯器的proto指令來編譯你的.proto文件。語法如下:
                         <span style="white-space:pre">		</span>proto -I=$SRC --java_out=$DIR File
$SRC表示資源文件夾,$DIR表示目標文件夾,File是你要編譯的.proto文件,當然,你也可以定位到這個資源目錄中然後只調用proto --java_out=$DIR File即可。(需要注意的是生成的class文件會存在於你所填的目標文件夾下的java_package所指向的目錄中,如果你想把目標文件夾指向當前目錄,你可以使用“.”來代替,例如對於該例,我們先定位到這個目錄下,然後運行:
<span style="white-space:pre">					</span>proto --java_out=. addressbook.proto)


使用protocol buffer的API


首先,讓我們看看編譯器給我們生成了那些代碼。首先你可以發現它的類名與我們定義的java_outer_classname的名字相同,同樣裏面還包含了你在addressbook.proto裏面定義的各種message的類,每一個類都有它自身的Builder用來實例化這個類對象。所有的messagesbuilders都自動生成了存取器,但是messages只有getterbuilders既有getters又有setters。以下就是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 也有這樣的存取器:

// 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-stylegetterssetters,但是用repeated修飾的域有一些特殊的方法,Count方法(用來統計這個消息list(列表)的長度)。通過add 方法可以在這個list中追加一個元素,而addAll 方法可以把一個Container(容器)裏面的所有元素都添加在list當中。

注意到這些方法都是使用的駝峯式命名法,儘管在.proto文件裏面我們都是寫的小寫,這也恰恰展示了protocol buffer的強大之處。

另外,還有一個需要注意的就是enum(枚舉)類型所生成的類,它自動生成了如下代碼:

public static enum PhoneType {
  MOBILE(0, 0),
  HOME(1, 1),
  WORK(2, 2),  ;  ...}

 

Builders vs. Messages

Message類裏面由protocol buffer編譯器自動生成的代碼都是不可變的,一旦一個message對象被實例化之後,它就不能再被修改了,就像Java中的String一樣。而如果想要實例化一個message類,你必須首先實例化一個builder類,然後設置好所有你想要設置的屬性,然後再調用builder類的build()方法。你或許已經注意到了builder的每一個用來改變message的屬性的方法都返回了另外一個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();

標準的Message方法

每一個message以及builder類都包含了一些其他的方法用來檢測和操作所有的message,這些方法包括:

·isInitialized()檢測所有用required修飾的域是否都設置了初始值。

·toString()返回一個可讀的message,特別是用來測試的時候 

·mergeFrom(Message other): (builder獨有的把另一個message合併到這個message當中,重寫單獨域並且連接反覆的域。

·clear(): (builder獨有的)清空所有的域並且返回空的狀態

這些方法都實現了MessageMessage.Builder 接口並且接口被所有的Java messages以及builders共享。

 

解析和序列化

最後,所有的protocol buffer類都有writingreading你所選擇的protocol buffer (二進制)格式數據的方法。他們包括:

·byte[] toByteArray();序列化這個 message 並且返回一個字節數組。

·static Person parseFrom(byte[] data);從給定的字節數組中解析一條message

·void writeTo(OutputStream output);序列化這個message並且將它寫入 OutputStream.

·static Person parseFrom(InputStream input);讀取並且解析一條InputStream中的message

這裏只是其中的幾個解析和序列化的方法而已,如果想要知道其中所有的方法可以將該類生成doc文檔然後查看。

 

Protocol BuffersO-O Design

 Protocol buffer的類基本上是無聲的數據持有者(就像 C++裏面的結構體一樣);他們最開始在一個對象模型中表現的並不友好,如果你想要爲生成的類添加一個更加友好的方法的話,最好的方式是將protocol buffer生成的類包裝在一個特定的應用程序裏面。對於不太懂.proto文件設計的人來說包裝protocol buffer也是一個不錯的主意。你也可以使用包裝類生成接口以便更好地適應特定的程序環境:隱藏一些數據和方法,並且顯示一些便捷的功能,等等。特別需要注意的是,你最好不要寫一些行爲去繼承這些生成的類。那會打破它內部的機制況且它對於你來說也不是一次很好的面向對象的練習機會。

 

Writing A Message

OK,說了這麼多,那就來使用一下protocol buffer生成的類吧。首先,你肯定希望你的這個“addressbook”的應用程序可以write一個你定義的message。爲了完成這項工作,你需要創建一個新的類來調用protocol buffer類裏面的方法並將message寫入OutputStream.

下面就是一個將用戶在控制檯輸入的AddressBook 的相關信息寫入文件的一個類,當然,你首先得創建一個文件(當然你也可以在文件不存在的情況下使用File類的createNewFile()方法來創建一個新的文件),爲了個性化你的程序,不妨以.book作爲你的後綴名,具體代碼如下:

 

Java代碼  收藏代碼
  1. import java.io.BufferedReader;  
  2. import java.io.FileInputStream;  
  3. import java.io.FileNotFoundException;  
  4. import java.io.FileOutputStream;  
  5. import java.io.IOException;  
  6. import java.io.InputStreamReader;  
  7. import java.io.PrintStream;  
  8.   
  9. import com.example.tutorial.AddressBookProtos.AddressBook;  
  10. import com.example.tutorial.AddressBookProtos.Person;  
  11. class AddPerson {  
  12.     /** 
  13.      * 將用戶輸入的Person message寫入輸出流中   
  14.      * @param stdin 輸入流 
  15.      * @param stdout 打印輸出流 
  16.      * @return Person類 
  17.      * @throws IOException 
  18.      */  
  19.     static Person PromptForAddress(BufferedReader stdin,PrintStream stdout)  
  20.             throws IOException {  
  21.           
  22.         Person.Builder person = Person.newBuilder();  
  23.         stdout.print("Enter person ID: ");  
  24.         person.setId(Integer.valueOf(stdin.readLine()));  
  25.   
  26.         stdout.print("Enter name: ");  
  27.         person.setName(stdin.readLine());  
  28.   
  29.         //空白表示沒有  
  30.         stdout.print("Enter email address (blank for none): ");  
  31.         String email = stdin.readLine();  
  32.         if (email.length() > 0){  
  33.             person.setEmail(email);  
  34.         }  
  35.         while (true) {  
  36.             //按下Enter鍵結束輸入  
  37.             stdout.print("Enter a phone number (or leave blank to finish): ");  
  38.             String number = stdin.readLine();  
  39.             if (number.length() == 0) {  
  40.                 break;  
  41.             }  
  42.               
  43.             Person.PhoneNumber.Builder phoneNumber = Person.PhoneNumber.newBuilder().setNumber(number);  
  44.               
  45.             //輸入完成之後需要確定你輸入的是手機號、家庭電話還是工作電話  
  46.             stdout.print("Is this a mobile, home, or work phone? ");  
  47.             String type = stdin.readLine();  
  48.             if (type.equals("mobile")) {  
  49.                 phoneNumber.setType(Person.PhoneType.MOBILE);  
  50.             } else if(type.equals("home")){  
  51.                     phoneNumber.setType(Person.PhoneType.HOME);  
  52.             } else if (type.equals("work")) {  
  53.                 phoneNumber.setType(Person.PhoneType.WORK);  
  54.             } else {  
  55.                 stdout.println("Unknown phone type.Using default.");  
  56.             }  
  57.             person.addPhone(phoneNumber);  
  58.             }  
  59.             return person.build();  
  60.         }  
  61.       
  62.     //Main function:  Reads the entire address book from a file,  
  63.     //adds one person based on user input, then writes it back out to the same  
  64.     //file.    
  65.     public static void main(String[] args)  
  66.             throws Exception {  
  67.           
  68. //      if (args.length != 1) {  
  69. //          System.err.println("Usage:  AddPerson ADDRESS_BOOK_FILE");  
  70. //          System.exit(-1);  
  71. //          }  
  72.         AddressBook.Builder addressBook = AddressBook.newBuilder();  
  73.           
  74.         // 檢驗是否存在這個文件  
  75.         try {  
  76.             addressBook.mergeFrom(new FileInputStream("src/Book/TestPerson.book"));  
  77.             } catch (FileNotFoundException e) {  
  78.                 System.out.println("src/Book/TestPerson.book" + ": File not found.Creating a new file.");  
  79.             }      
  80.           
  81.         //將這條Person message添加到AddressBook中  
  82.         addressBook.addPerson(PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),System.out));  
  83.           
  84.         //將新建的AddressBook寫入文件當中  
  85.         FileOutputStream output = new FileOutputStream("src/Book/TestPerson.book");  
  86.         addressBook.build().writeTo(output);  
  87.         output.close();    
  88.     }  
  89. }  

Reading A Message

當然,這個程序肯定不止一個寫入消息的類,還要能把存在文件中的數據讀出來。如下:

 

Java代碼  收藏代碼
  1. import com.example.tutorial.AddressBookProtos.AddressBook;  
  2. import com.example.tutorial.AddressBookProtos.Person;  
  3. import java.io.FileInputStream;  
  4. class ListPeople {  
  5.     /** 
  6.      * 迭代遍歷並且打印文件中所包含的信息 
  7.      * @param addressBook AddressBook對象 
  8.      */  
  9.     static void Print(AddressBook addressBook) {  
  10.         for (Person person: addressBook.getPersonList()){  
  11.             System.out.println("Person ID: " + person.getId());  
  12.             System.out.println("Name: " + person.getName());  
  13.             if (person.hasEmail()) {  
  14.                 System.out.println("E-mail address:"+ person.getEmail());  
  15.                 }  
  16.             for (Person.PhoneNumber phoneNumber : person.getPhoneList()) {  
  17.                 switch (phoneNumber.getType()) {  
  18.                 case MOBILE:  
  19.                     System.out.print("Mobile phone #: ");  
  20.                     break;      
  21.                 case HOME:  
  22.                     System.out.print("Home phone #: ");  
  23.                     break;  
  24.                 case WORK:  
  25.                     System.out.print("Work phone #: ");  
  26.                     break;  
  27.                     }        
  28.                     System.out.println(phoneNumber.getNumber());  
  29.                 }  
  30.             }  
  31.         }  
  32.       
  33.     public static void main(String[] args) throws Exception {  
  34. //      if (args.length != 1) {  
  35. //          System.err.println("Usage:  ListPeople ADDRESS_BOOK_FILE");   
  36. //          System.exit(-1);  
  37. //      }   
  38.         // 讀取已經存在.book文件  
  39.         AddressBook addressBook = AddressBook.parseFrom(new FileInputStream("src/Book/TestPerson.book"));  
  40.         Print(addressBook);  
  41.     }  
  42.  }  

 

擴展一個Protocol Buffer

當你發佈了一段由你的protocol buffer編寫的代碼之後,你或許迫不及待的想要擴展它的功能。如果你想要使你的新buffers是反向兼容的或者你的舊buffers是正向兼容的話,那麼下面有幾條規則是你需要遵守的,在新的protocol buffer的版本中:· 你最好不要改變已經存在的域的標籤(Tag)

   ·你最好不要添加或者刪除任何用required修飾的域

      ·你可以刪除optional或者repeated修飾的域

   ·你可以添加新的用optional或者repeated修飾的域但你必須使用Tag數字(從未被這個protocol所使用過的tag,即使是被刪除了的也不行).

如果你遵循這些規則,舊的代碼也會非常“高興”的讀取新的消息。對於舊的代碼來說,那些被刪除了的用optional修飾的域會有他們的默認值並且被刪除了用repeated修飾的域會爲空,新的代碼讀取舊的消息也會很輕鬆。然而,請記住,新的optional域不會存在於舊的message當中,所以你應該明確的知道它們是否被設置爲has_或者在你的.proto 文件提供了一個合理的默認值[default = value]。如果默認值沒有明確是一個optional元素,而是按默認類型定義的話對於string來說默認值就是empty string,其他類型也類型,在本文的上面已經提到過了,這裏就不在累贅。特別聲明,如果你添加了一個新的用repeated修飾的域而沒有has_標誌的話,你的新代碼將無法識別它是否爲空,而你的舊代碼則完全無法設置它。

 

高級用法

Protocol buffers還有一些用法是一般的存取器和序列化所無法辦到的,如果你需要了解更多的信息可以上Google的官方文檔上面去查看。

Protocolmessage類提供的一個關鍵特徵就是映射 在一個message裏面你可以反覆聲明不同的域並且操作他們的值而不需要在任何類型的message之前寫代碼。使用映射來從其他的編碼中轉換一條message無疑是一個非常有效的方法,即使面對XML 或者JSON也一樣。關於映射的一個更加高級的用法恐怕就是找出兩條相同類型的message類之間的不同點了,或者是爲Protocol buffer生成一系列的“規則映像”,使用這些映像你可以匹配確切的消息內容。發揮你的想象,Protocol Buffers可以應用到更多的領域當中去。





發佈了28 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章