Grundlagen von Protocol Buffers: Java
Dieses Tutorial bietet eine grundlegende Einführung für Java-Programmierer in die Arbeit mit Protocol Buffers. Anhand einer einfachen Beispielanwendung wird gezeigt, wie Sie
- Nachrichtenformate in einer
.proto-Datei definieren. - Den Protocol Buffer-Compiler verwenden.
- Die Java-Protokollpuffer-API zum Schreiben und Lesen von Nachrichten verwenden.
Dies ist keine umfassende Anleitung zur Verwendung von Protocol Buffers in Java. Ausführlichere Referenzinformationen finden Sie im Protocol Buffer Language Guide (proto2), im Protocol Buffer Language Guide (proto3), im Java API Reference, im Java Generated Code Guide und im Encoding Reference.
Das Problemfeld
Das Beispiel, das wir verwenden werden, ist eine sehr einfache "Adressbuch"-Anwendung, die Kontaktdaten von Personen lesen und in eine Datei schreiben kann. Jede Person im Adressbuch hat einen Namen, eine ID, eine E-Mail-Adresse und eine Telefonnummer.
Wie serialisiert und ruft man strukturierte Daten wie diese ab? Es gibt mehrere Möglichkeiten, dieses Problem zu lösen
- Java-Serialisierung verwenden. Dies ist der Standardansatz, da er in die Sprache integriert ist, aber er hat eine Reihe bekannter Probleme (siehe Effective Java, von Josh Bloch S. 213) und funktioniert auch nicht sehr gut, wenn Sie Daten mit Anwendungen austauschen müssen, die in C++ oder Python geschrieben sind.
- Man kann sich einen Ad-hoc-Weg ausdenken, um die Datenpunkte in einen einzigen String zu kodieren – z. B. 4 ganze Zahlen als „12:3:-23:67“ kodieren. Dies ist ein einfacher und flexibler Ansatz, obwohl er das Schreiben von einmaligen Kodierungs- und Parsing-Codes erfordert und das Parsen einen geringen Laufzeitaufwand verursacht. Dies funktioniert am besten für die Kodierung sehr einfacher Daten.
- Serialisieren Sie die Daten in XML. Dieser Ansatz kann sehr attraktiv sein, da XML (sozusagen) menschenlesbar ist und es Bindungsbibliotheken für viele Sprachen gibt. Dies kann eine gute Wahl sein, wenn Sie Daten mit anderen Anwendungen/Projekten teilen möchten. XML ist jedoch bekanntermaßen speicherintensiv, und die Kodierung/Dekodierung kann Anwendungen eine enorme Leistungseinbuße bescheren. Außerdem ist die Navigation durch einen XML-DOM-Baum erheblich komplizierter als die normale Navigation durch einfache Felder in einer Klasse.
Anstelle dieser Optionen können Sie Protocol Buffers verwenden. Protocol Buffers sind die flexible, effiziente und automatisierte Lösung für genau dieses Problem. Mit Protocol Buffers schreiben Sie eine .proto-Beschreibung der Datenstruktur, die Sie speichern möchten. Daraus erstellt der Protocol Buffer-Compiler eine Klasse, die eine automatische Kodierung und ein Parsing der Protocol Buffer-Daten mit einem effizienten Binärformat implementiert. Die generierte Klasse bietet Getter und Setter für die Felder, aus denen ein Protocol Buffer besteht, und kümmert sich um die Details des Lesens und Schreibens des Protocol Buffers als Einheit. Wichtig ist, dass das Protocol Buffer-Format die Idee unterstützt, das Format im Laufe der Zeit so zu erweitern, dass der Code Daten, die mit dem alten Format kodiert wurden, weiterhin lesen kann.
Wo man den Beispielcode findet
Der Beispielcode ist im Quellcode-Paket im Verzeichnis „examples“ enthalten. Laden Sie ihn hier herunter.
Definieren Ihres Protokollformats
Um Ihre Adressbuchanwendung zu erstellen, müssen Sie mit einer .proto-Datei beginnen. Die Definitionen in einer .proto-Datei sind einfach: Sie fügen eine Nachricht für jede Datenstruktur hinzu, die Sie serialisieren möchten, und geben dann einen Namen und einen Typ für jedes Feld in der Nachricht an. Hier ist die .proto-Datei, die Ihre Nachrichten definiert, addressbook.proto.
syntax = "proto2";
package tutorial;
option java_multiple_files = true;
option java_package = "com.example.tutorial.protos";
option java_outer_classname = "AddressBookProtos";
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = PHONE_TYPE_HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
Wie Sie sehen, ist die Syntax ähnlich wie bei C++ oder Java. Lassen Sie uns jeden Teil der Datei durchgehen und sehen, was er tut.
Die Datei .proto beginnt mit einer Paketdeklaration, die dazu dient, Namenskonflikte zwischen verschiedenen Projekten zu vermeiden. In Java wird der Paketname als Java-Paket verwendet, es sei denn, Sie haben explizit ein java_package angegeben, wie wir es hier getan haben. Selbst wenn Sie ein java_package angeben, sollten Sie dennoch ein normales package definieren, um Namenskollisionen im Protocol Buffers-Namensraum sowie in Nicht-Java-Sprachen zu vermeiden.
Nach der Paketdeklaration sehen Sie drei Java-spezifische Optionen: java_multiple_files, java_package und java_outer_classname. java_package gibt an, in welchem Java-Paketnamen Ihre generierten Klassen leben sollen. Wenn Sie dies nicht explizit angeben, entspricht es einfach dem Paketnamen, der von der package-Deklaration angegeben wird, aber diese Namen sind normalerweise keine geeigneten Java-Paketnamen (da sie normalerweise nicht mit einem Domainnamen beginnen). Die Option java_outer_classname definiert den Klassennamen der Wrapper-Klasse, die diese Datei repräsentieren wird. Wenn Sie keinen java_outer_classname explizit angeben, wird er generiert, indem der Dateiname in Upper Camel Case konvertiert wird. Zum Beispiel würde „my_proto.proto“ standardmäßig „MyProto“ als Wrapper-Klassennamen verwenden. Die Option java_multiple_files = true ermöglicht die Generierung einer separaten .java-Datei für jede generierte Klasse (anstelle des älteren Verhaltens, bei dem eine einzelne .java-Datei für die Wrapper-Klasse generiert wurde, die Wrapper-Klasse als äußere Klasse verwendet und alle anderen Klassen innerhalb der Wrapper-Klasse verschachtelt wurden).
Als Nächstes haben Sie Ihre NachrichtenDefinitionen. Eine Nachricht ist einfach eine Aggregation, die eine Reihe von typisierten Feldern enthält. Viele Standard-Datentypen sind als Feldtypen verfügbar, darunter bool, int32, float, double und string. Sie können Ihren Nachrichten auch weitere Struktur verleihen, indem Sie andere Nachrichtentypen als Feldtypen verwenden – im obigen Beispiel enthält die Nachricht Person PhoneNumber-Nachrichten, während die Nachricht AddressBook Person-Nachrichten enthält. Sie können sogar Nachrichtentypen definieren, die innerhalb anderer Nachrichten verschachtelt sind – wie Sie sehen, ist der Typ PhoneNumber innerhalb von Person definiert. Sie können auch enum-Typen definieren, wenn Sie möchten, dass eines Ihrer Felder einen Wert aus einer vordefinierten Liste hat – hier möchten Sie angeben, dass eine Telefonnummer einer der folgenden Telefontypen sein kann: PHONE_TYPE_MOBILE, PHONE_TYPE_HOME oder PHONE_TYPE_WORK.
Die Markierungen " = 1", " = 2" bei jedem Element identifizieren den eindeutigen "Tag", den dieses Feld in der Binärkodierung verwendet. Tag-Nummern von 1-15 erfordern ein Byte weniger zur Kodierung als höhere Nummern. Daher können Sie diese Tags als Optimierung für häufig verwendete oder wiederholte Elemente verwenden und die Tags 16 und höher für weniger häufig verwendete optionale Elemente übrig lassen. Jedes Element in einem wiederholten Feld erfordert eine erneute Kodierung der Tag-Nummer. Wiederholte Felder sind daher besonders gute Kandidaten für diese Optimierung.
Jedes Feld muss mit einem der folgenden Modifikatoren annotiert werden
optional: Das Feld kann gesetzt sein oder nicht. Wenn ein optionales Feld nicht gesetzt ist, wird ein Standardwert verwendet. Für einfache Typen können Sie Ihren eigenen Standardwert angeben, wie wir es für den Telefontyptypeim Beispiel getan haben. Andernfalls wird ein Systemstandard verwendet: Null für numerische Typen, ein leerer String für Strings, false für boolesche Werte. Für eingebettete Nachrichten ist der Standardwert immer die „Standardinstanz“ oder der „Prototyp“ der Nachricht, bei der keine ihrer Felder gesetzt ist. Der Aufruf des Accessors zum Abrufen des Werts eines optionalen (oder erforderlichen) Feldes, das nicht explizit gesetzt wurde, gibt immer den Standardwert dieses Feldes zurück.repeated: Das Feld kann beliebig oft wiederholt werden (einschließlich null). Die Reihenfolge der wiederholten Werte wird im Protokollpuffer beibehalten. Betrachten Sie wiederholte Felder als dynamisch dimensionierte Arrays.required: Ein Wert für das Feld muss angegeben werden, andernfalls wird die Nachricht als „nicht initialisiert“ betrachtet. Der Versuch, eine nicht initialisierte Nachricht zu erstellen, löst eineRuntimeExceptionaus. Das Parsen einer nicht initialisierten Nachricht löst eineIOExceptionaus. Ansonsten verhält sich ein erforderliches Feld genau wie ein optionales Feld.
Wichtig
Required Is Forever Seien Sie sehr vorsichtig beim Kennzeichnen von Feldern alsrequired. Wenn Sie zu einem späteren Zeitpunkt aufhören möchten, ein erforderliches Feld zu schreiben oder zu senden, wird es problematisch sein, das Feld in ein optionales Feld zu ändern – alte Leser werden Nachrichten ohne dieses Feld als unvollständig betrachten und sie möglicherweise unbeabsichtigt ablehnen oder verwerfen. Sie sollten stattdessen anwendungsspezifische benutzerdefinierte Validierungsroutinen für Ihre Puffer in Betracht ziehen. Innerhalb von Google werden required-Felder stark abgelehnt; die meisten in der proto2-Syntax definierten Nachrichten verwenden nur optional und repeated. (Proto3 unterstützt überhaupt keine required-Felder.)Eine vollständige Anleitung zum Schreiben von .proto-Dateien – einschließlich aller möglichen Feldtypen – finden Sie im Protocol Buffer Language Guide. Suchen Sie jedoch nicht nach Funktionen, die der Klassenvererbung ähneln – Protocol Buffers tun dies nicht.
Kompilieren Ihrer Protocol Buffers
Nachdem Sie nun eine .proto-Datei haben, müssen Sie als Nächstes die Klassen generieren, die Sie zum Lesen und Schreiben von AddressBook (und damit Person und PhoneNumber) Nachrichten benötigen. Dazu müssen Sie den Protocol Buffer-Compiler protoc auf Ihrer .proto-Datei ausführen.
Wenn Sie den Compiler noch nicht installiert haben, laden Sie das Paket herunter und befolgen Sie die Anweisungen in der README-Datei.
Führen Sie nun den Compiler aus und geben Sie das Quellverzeichnis an (wo sich der Quellcode Ihrer Anwendung befindet – das aktuelle Verzeichnis wird verwendet, wenn Sie keinen Wert angeben), das Zielverzeichnis (wo der generierte Code hingehen soll; oft dasselbe wie
$SRC_DIR) und den Pfad zu Ihrer.proto-Datei. In diesem Fall tun Sie…protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.protoDa Sie Java-Klassen wünschen, verwenden Sie die Option
--java_out– ähnliche Optionen werden für andere unterstützte Sprachen bereitgestellt.
Dies generiert ein Unterverzeichnis com/example/tutorial/protos/ in Ihrem angegebenen Zielverzeichnis, das einige generierte .java-Dateien enthält.
Die Protocol Buffer API
Schauen wir uns einen Teil des generierten Codes an und sehen wir, welche Klassen und Methoden der Compiler für Sie erstellt hat. Wenn Sie in com/example/tutorial/protos/ nachsehen, sehen Sie, dass es .java-Dateien gibt, die eine Klasse für jede Nachricht definieren, die Sie in addressbook.proto angegeben haben. Jede Klasse hat ihre eigene Builder-Klasse, die Sie zum Erstellen von Instanzen dieser Klasse verwenden. Mehr über Builder erfahren Sie im Abschnitt Builder vs. Nachrichten unten.
Sowohl Nachrichten als auch Builder haben automatisch generierte Accessor-Methoden für jedes Feld der Nachricht; Nachrichten haben nur Getter, während Builder sowohl Getter als auch Setter haben. Hier sind einige der Accessoren für die Person-Klasse (Implementierungen der Kürze halber weggelassen)
// 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);
In der Zwischenzeit hat Person.Builder dieselben Getter plus Setter
// required string name = 1;
public boolean hasName();
public 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();
Wie Sie sehen können, gibt es einfache JavaBeans-ähnliche Getter und Setter für jedes Feld. Es gibt auch has-Getter für jedes einzelne Feld, die true zurückgeben, wenn dieses Feld gesetzt wurde. Schließlich hat jedes Feld eine clear-Methode, die das Feld in seinen leeren Zustand zurücksetzt.
Wiederholte Felder haben einige zusätzliche Methoden – eine Count-Methode (die nur eine Kurzform für die Größe der Liste ist), Getter und Setter, die ein bestimmtes Element der Liste anhand des Index abrufen oder setzen, eine add-Methode, die ein neues Element an die Liste anhängt, und eine addAll-Methode, die einen gesamten Container voller Elemente zur Liste hinzufügt.
Beachten Sie, wie diese Accessor-Methoden die CamelCase-Schreibweise verwenden, obwohl die .proto-Datei Kleinbuchstaben mit Unterstrichen verwendet. Diese Transformation wird automatisch vom Protocol Buffer-Compiler durchgeführt, damit die generierten Klassen den Standard-Java-Konventionen entsprechen. Sie sollten immer Kleinbuchstaben mit Unterstrichen für Feldnamen in Ihren .proto-Dateien verwenden; dies gewährleistet gute Namenskonventionen in allen generierten Sprachen. Weitere Informationen zu guter .proto-Formatierung finden Sie in der Style-Anleitung.
Weitere Informationen darüber, welche Member der Protokollcompiler für eine bestimmte Felddefinition genau generiert, finden Sie in der Java Generated Code Reference.
Enums und verschachtelte Klassen
Der generierte Code enthält ein Java 5-Enum PhoneType, das in Person verschachtelt ist
public static enum PhoneType {
PHONE_TYPE_UNSPECIFIED(0, 0),
PHONE_TYPE_MOBILE(1, 1),
PHONE_TYPE_HOME(2, 2),
PHONE_TYPE_WORK(3, 3),
;
...
}
Der verschachtelte Typ Person.PhoneNumber wird, wie erwartet, als verschachtelte Klasse innerhalb von Person generiert.
Builder vs. Nachrichten
Die vom Protocol Buffer-Compiler generierten Nachrichtenklassen sind alle unveränderlich. Sobald ein Nachrichtenobjekt konstruiert ist, kann es nicht mehr geändert werden, genau wie ein Java String. Um eine Nachricht zu konstruieren, müssen Sie zuerst einen Builder konstruieren, alle Felder, die Sie setzen möchten, auf Ihre gewählten Werte setzen und dann die build()-Methode des Builders aufrufen.
Möglicherweise haben Sie bemerkt, dass jede Methode des Builders, die die Nachricht ändert, einen anderen Builder zurückgibt. Das zurückgegebene Objekt ist tatsächlich derselbe Builder, auf dem Sie die Methode aufgerufen haben. Er wird zur Vereinfachung zurückgegeben, damit Sie mehrere Setter in einer einzigen Codezeile verketten können.
Hier ist ein Beispiel, wie Sie eine Instanz von Person erstellen würden
Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhones(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.PHONE_TYPE_HOME)
.build());
.build();
Standard-Nachrichtenmethoden
Jede Nachrichten- und Builder-Klasse enthält auch eine Reihe anderer Methoden, mit denen Sie die gesamte Nachricht überprüfen oder manipulieren können, darunter
isInitialized(): prüft, ob alle erforderlichen Felder gesetzt wurden.toString(): gibt eine menschenlesbare Darstellung der Nachricht zurück, besonders nützlich für die Fehlersuche.mergeFrom(Message other): (nur Builder) fügt den Inhalt vonotherin diese Nachricht ein und überschreibt einzelne Skalarfelder, fügt zusammengesetzte Felder zusammen und hängt wiederholte Felder aneinander.clear(): (nur Builder) löscht alle Felder zurück in den leeren Zustand.
Diese Methoden implementieren die Schnittstellen Message und Message.Builder, die von allen Java-Nachrichten und -Buildern gemeinsam genutzt werden. Weitere Informationen finden Sie in der vollständigen API-Dokumentation für Message.
Parsen und Serialisieren
Schließlich verfügt jede Protocol Buffer-Klasse über Methoden zum Schreiben und Lesen von Nachrichten Ihres gewählten Typs unter Verwendung des Binärformats von Protocol Buffers. Dazu gehören:
byte[] toByteArray();: serialisiert die Nachricht und gibt ein Byte-Array mit ihren Roh-Bytes zurück.static Person parseFrom(byte[] data);: parst eine Nachricht aus dem gegebenen Byte-Array.void writeTo(OutputStream output);: serialisiert die Nachricht und schreibt sie in einenOutputStream.static Person parseFrom(InputStream input);: liest und parst eine Nachricht aus einemInputStream.
Dies sind nur einige der Optionen, die für das Parsen und die Serialisierung angeboten werden. Auch hier finden Sie in der Message API-Referenz eine vollständige Liste.
Wichtig
Protocol Buffers und objektorientiertes Design Protokollpufferklassen sind im Grunde Datenhalter (wie Strukturen in C), die keine zusätzliche Funktionalität bieten; sie eignen sich nicht gut als First-Class Citizens in einem Objektmodell. Wenn Sie einer generierten Klasse reichhaltigeres Verhalten hinzufügen möchten, ist die beste Methode, die generierte Protokollpufferklasse in eine anwendungsspezifische Klasse zu wrappen. Das Wrappen von Protokollpuffern ist auch eine gute Idee, wenn Sie keinen Einfluss auf das Design der.proto-Datei haben (wenn Sie beispielsweise eine aus einem anderen Projekt wiederverwenden). In diesem Fall können Sie die Wrapper-Klasse verwenden, um eine Schnittstelle zu gestalten, die besser an die einzigartige Umgebung Ihrer Anwendung angepasst ist: Verbergen einiger Daten und Methoden, Bereitstellen von Komfortfunktionen usw. Sie sollten niemals Verhalten zu den generierten Klassen hinzufügen, indem Sie von ihnen erben. Dies wird interne Mechanismen brechen und ist ohnehin keine gute objektorientierte Praxis.Eine Nachricht schreiben
Lassen Sie uns nun versuchen, Ihre Protocol Buffer-Klassen zu verwenden. Das Erste, was Ihre Adressbuchanwendung tun können sollte, ist das Schreiben von persönlichen Details in Ihre Adressbuchdatei. Dazu müssen Sie Instanzen Ihrer Protocol Buffer-Klassen erstellen und befüllen und sie dann in einen Ausgabestrom schreiben.
Hier ist ein Programm, das ein AddressBook aus einer Datei liest, basierend auf Benutzereingaben eine neue Person hinzufügt und das neue AddressBook wieder in die Datei schreibt. Die Teile, die direkt Code aufrufen oder darauf verweisen, der vom Protocol Compiler generiert wurde, sind hervorgehoben.
import com.example.tutorial.protos.AddressBook;
import com.example.tutorial.protos.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.PHONE_TYPE_MOBILE);
} else if (type.equals("home")) {
phoneNumber.setType(Person.PhoneType.PHONE_TYPE_HOME);
} else if (type.equals("work")) {
phoneNumber.setType(Person.PhoneType.PHONE_TYPE_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.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();
}
}
Eine Nachricht lesen
Natürlich wäre ein Adressbuch nicht sehr nützlich, wenn man keine Informationen daraus abrufen könnte! Dieses Beispiel liest die Datei, die vom obigen Beispiel erstellt wurde, und gibt alle darin enthaltenen Informationen aus.
import com.example.tutorial.protos.AddressBook;
import com.example.tutorial.protos.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 PHONE_TYPE_MOBILE:
System.out.print(" Mobile phone #: ");
break;
case PHONE_TYPE_HOME:
System.out.print(" Home phone #: ");
break;
case PHONE_TYPE_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);
}
}
Erweitern eines Protocol Buffers
Früher oder später, nachdem Sie den Code veröffentlicht haben, der Ihre Protocol Buffer verwendet, werden Sie zweifellos die Definition des Protocol Buffers „verbessern“ wollen. Wenn Sie möchten, dass Ihre neuen Buffer abwärtskompatibel sind und Ihre alten Buffer vorwärtskompatibel sind – und das wollen Sie fast sicherlich –, dann gibt es einige Regeln, die Sie befolgen müssen. In der neuen Version des Protocol Buffers
- Sie dürfen die Tag-Nummern vorhandener Felder nicht ändern.
- Sie dürfen keine erforderlichen Felder hinzufügen oder löschen.
- Sie dürfen optionale oder wiederholte Felder löschen.
- Sie dürfen neue optionale oder wiederholte Felder hinzufügen, müssen aber neue Tag-Nummern verwenden (d. h. Tag-Nummern, die in diesem Protokollpuffer noch nie verwendet wurden, auch nicht von gelöschten Feldern).
(Es gibt einige Ausnahmen zu diesen Regeln, aber sie werden selten verwendet.)
Wenn Sie diese Regeln befolgen, liest der alte Code neue Nachrichten problemlos und ignoriert einfach alle neuen Felder. Für den alten Code sind optional gelöschte Felder einfach ihr Standardwert, und gelöschte wiederholte Felder sind leer. Neuer Code liest auch alte Nachrichten transparent. Beachten Sie jedoch, dass neue optionale Felder in alten Nachrichten nicht vorhanden sein werden. Sie müssen daher entweder explizit mit has_ prüfen, ob sie gesetzt sind, oder einen angemessenen Standardwert in Ihrer .proto-Datei mit [default = value] nach der Tag-Nummer angeben. Wenn für ein optionales Element kein Standardwert angegeben ist, wird stattdessen ein typspezifischer Standardwert verwendet: für Zeichenfolgen ist der Standardwert der leere String. Für boolesche Werte ist der Standardwert false. Für numerische Typen ist der Standardwert null. Beachten Sie auch, dass, wenn Sie ein neues wiederholtes Feld hinzugefügt haben, Ihr neuer Code nicht feststellen kann, ob es leer gelassen wurde (von neuem Code) oder gar nicht gesetzt wurde (von altem Code), da es kein has_-Flag dafür gibt.
Erweiterte Nutzung
Protocol Buffers haben Verwendungszwecke, die über einfache Accessoren und Serialisierung hinausgehen. Erkunden Sie unbedingt die Java API-Referenz, um zu sehen, was Sie noch damit tun können.
Eine Schlüsselmerkmal der Protokollnachrichtenklassen ist die Reflexion. Sie können über die Felder einer Nachricht iterieren und deren Werte manipulieren, ohne Ihren Code gegen einen bestimmten Nachrichtentyp zu schreiben. Eine sehr nützliche Anwendung der Reflexion ist die Konvertierung von Protokollnachrichten in und aus anderen Encodings wie XML oder JSON. Eine fortgeschrittenere Verwendung der Reflexion könnte darin bestehen, Unterschiede zwischen zwei Nachrichten desselben Typs zu finden oder eine Art „Reguläre Ausdrücke für Protokollnachrichten“ zu entwickeln, in denen Sie Ausdrücke schreiben können, die bestimmte Nachrichteninhalt abgleichen. Wenn Sie Ihre Vorstellungskraft einsetzen, ist es möglich, Protocol Buffers auf eine viel breitere Palette von Problemen anzuwenden, als Sie vielleicht ursprünglich erwarten!
Reflexion wird als Teil der Schnittstellen Message und Message.Builder bereitgestellt.