Protocol Buffer介紹(Java實例)

本文譯自:https://developers.google.com/protocol-buffers/docs/javatutorial?hl=zh-CN

ProtocolBuffer基礎:Java

本指南提供了使用ProtocolBuffer工作的Java編程方法。全文通過一個簡單的示例,向你介紹在Java中使用ProtocolBuffer的方法:

1.如何在.proto文件中定義消息格式;

2.如何使用ProtocolBuffercreates編譯器;

3.如何使用JavaProtocol BufferAPI來讀寫消息。

本文不是在Java中使用ProtocolBuffer的完整指南,更詳細的信息請參照以下資料:

Protocol-buffers語言

JavaAPI參考

生成Java代碼指南

編碼參考

爲什麼使用ProtocolBuffer

我們使用了一個非常簡單的“地址本”應用的例子,這個應用能夠從一個文件中讀寫個人的聯繫方式信息。在地址本中每個人都有以下信息:姓名、ID、郵件地址、電話號碼。

像這樣的結構化數據應該如何系列化和恢復呢?以下幾種方法能夠解決這個問題:

1.使用Java系列化。因爲它是內置在編程語言中的,所以是默認的方法,但是由於衆所周知的主機問題,並且如果需要在使用不同編程語言(如C++Python)編寫應用程序之間共享數據,這種方式也不會很好的工作。

2.使用特殊的方式把數據項編碼到一個單獨的字符串中,如把4個整數編碼成“123-2367”。儘管它需要編寫一次性的編碼和解碼代碼,但是這種方法簡單而靈活,而且運行時解析成本很小。這種方法對於簡單數據是最好的。

3.把數據系列化到XML。因爲XML是可人類可讀的,並且很多編程語言都有對應的功能類庫,所以這種方法非常受歡迎。如果你想要跟其他應用程序/項目共享數據,那麼這種方法是一個非常好的選擇。但是,衆所周知,XML是空間密集性的,並且編解碼會嚴重影響應用程序的性能。此外,XMLDOM樹導航也比一般的類中的字段導航要複雜的多。

ProtocolBuffer是完全解決這個問題的靈活、高效的自動化解決方案。使用ProtocolBuffer,要先編寫一個.proto文件,用這個文件來描述你希望保存的數據結構。然後用ProtocolBuffer編譯器創建一個類,這個類用高效的二進制的格式實現了ProtocolBuffer數據的自動編解碼。生成的類提供了組成ProtocolBuffer字段的gettersetter方法,以及提供了負責讀寫一個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_packagejava_outer_classnamejava_package指定要生成的Java類的包名。如果沒有明確的指定這個關鍵字,它會簡單的用package關鍵字的聲明來作爲包名,但是這些名稱通常不適合做Java的包名(因爲它們通常不是用域名開頭的)。java_outer_classname可選項定義了這個文件中所包含的所有類的類名。如果沒有明確的給出java_outer_classname定義,它會把文件名轉換成駝峯樣式的類名。如,“my_proto.proto”文件,默認的情況下會使用MyProto作爲外部的類名。

接下來是消息定義,一個消息包含了一組類型字段。很多標準的簡單數據類型都可以作爲有效的字段類型,包括:boolint32floatdoublestring。還可以是其他消息類型作爲字段類型---在上面的示例中,Person消息包含了PhoneNumber消息,而AddressBook消息又包含了Person消息。甚至還可以定嵌套在其他消息內的消息類型---如,PhoneNumber類型就被定義在Person內。如果想要字段有一個預定義的值列表,也可以定enum類型---上例中電話號碼能夠指定MOBILEHOMEWORK三種類型之一。

每個字段後標記的“=1”、“=2”,是在二進制編碼時使用的每個字段的唯一標識。在編碼時,數字115要比大於它們的數字少一個字節,因此,作爲一個優化選項,可以把115的數字用於常用的或重複性的元素。大於等於16的數字儘可能的用於那些不常用的可選元素。在重複字段中的每個元素都需要預定義一個標記數字,因此在重複性字段中使用這種優化是良好的選擇。

每個字段必須用以下修飾符之一來進行標註:

1.required:用這個修飾符來標註的字段必須給該字段提供一個值,否則該消息會被認爲未被初始化。嘗試構建一個未被初始化的消息會拋出一個RuntimeException異常。解析未被初始化的消息時,會拋出一個IOException異常。其他方面,該類型字段的行爲與可選類型字段完全一樣;

2.optional:用這個修飾符來標註的字段可以設定值,也可以不設定值。如果可選字段的。值沒有設定,那麼就會使用一個默認的值。對於簡單類型,能夠像上例中指定電話號碼的type那樣,指定一個默認值。否則,系統使用的默認值如下:數字類型是0、字符串類型是空字符串、布爾值是false。對於內嵌的消息,默認值始終是“默認的實例“或”消息的“原型”,其中沒有字段設置。調用沒有明確設置值的字段的獲取值的訪問器的時候,會始終返回字段的默認值。

3.repeated:用這個修飾符來標註的字段可以被重複指定的數字的次數(包括0)。重複值的順序會被保留在ProtocolBuffer中。重複字段跟動態數組很像。

對於標記爲required的字段要始終小心。如果在某些時候,你希望終止寫入或發送一個required類型的字段,那麼在把該字段改變成可選字段時,就會發生問題---舊的版本會認爲沒有這個字段的消息是不完整的,並且會因此而拒絕或刪除它們。因此應該考慮使用編寫應用程序規範來定製Buffer的驗證規則來代替。Google的一些工程師認爲使用required,弊大於利,他們建議只使用optionalrepeqted。但實際上是行不通的。

ProtocolBuffer語言指南中,你會找到完成.proto文件編寫指南---包括所有可能的字段類型。不要尋求類的繼承性,ProtocolBuffer是不支持的。

編譯ProtocolBuffer

現在有一個.proto文件了,接下來要做的就是生成一個讀寫AddressBook(包括PersonPhoneNumber)消息的類。運行ProtocolBuffer編譯器protoc來生成與.proto文件相關的類。

首先,需要下載的關於Protobuf的文件:

1.到http://code.google.com/p/protobuf/downloads/list ,選擇其中的win版本下載,我選擇的是protoc-2.4.1-win32.zip,解壓得到protoc.exe文件。

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的信息。

MessageBuilder會給消息的每個字段都生成訪問方法。Message僅有get方法,而Builder同時擁有getset方法。以下是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.Buildergetset方法:
// 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樣式的的getset方法。對於每個有get方法的字段,如果該字段被設置,那麼對應的has方法會返回ture。最後,每個字段還有一個clear方法,它會清除對應字段的設置,讓它們回退到空的狀態。
重複性字段會有一些額外的方法---Count方法(它會返回列表的尺寸)、通過索引指定獲取或設定列表元素的getset方法、往列表中添加新元素的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編譯器生成的消息類都是不可變的。一旦消息對象被構建了,它就不能被編輯了,就像JavaString。要構建一個消息對象,首先必須構建一個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():(只有構建器纔有這個方法),清除所有字段的值,讓它們返回到空的狀態。
這些方法實現的MessageMessage.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的應用範圍會比你的初始期望值還要高。
反射是作爲MessageMessage.Builder的接口部分來提供的。
(另外一個例子:http://www.cnblogs.com/stephen-liu74/archive/2013/01/06/2842972.html 很詳細。點擊打開鏈接
發佈了15 篇原創文章 · 獲贊 2 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章