Java Generated Code Guide

Beschreibt genau, welchen Java-Code der Protokollpuffer-Compiler für eine gegebene Protokolldefinition generiert.

Alle Unterschiede zwischen den von proto2, proto3 und Editions generierten Codes werden hervorgehoben – beachten Sie, dass sich diese Unterschiede im generierten Code befinden, wie in diesem Dokument beschrieben, nicht in den Basisnachrichtenklassen/-schnittstellen, die in allen Versionen gleich sind. Sie sollten die Sprachleitfäden für proto2, proto3 und/oder Editions lesen, bevor Sie dieses Dokument lesen.

Beachten Sie, dass keine Java-Protokollpuffer-Methoden Nullwerte akzeptieren oder zurückgeben, sofern nicht anders angegeben.

Compiler-Aufruf

Der Protokollpuffer-Compiler erzeugt Java-Ausgaben, wenn er mit dem Befehlszeilenflag --java_out= aufgerufen wird. Der Parameter der Option --java_out= ist das Verzeichnis, in das der Compiler Ihre Java-Ausgaben schreiben soll. Für jede .proto-Eingabedatei erstellt der Compiler eine Wrapper-.java-Datei, die eine Java-Klasse enthält, die die .proto-Datei selbst darstellt.

Wenn die .proto-Datei eine Zeile wie die folgende enthält

option java_multiple_files = true;

Dann erstellt der Compiler auch separate .java-Dateien für jede der Klassen/Enums, die er für jede Top-Level-Nachricht, Enumeration und jeden Dienst generieren wird, die in der .proto-Datei deklariert sind.

Andernfalls (wenn die Option java_multiple_files deaktiviert ist, was der Standard ist), wird die oben erwähnte Wrapper-Klasse auch als äußere Klasse verwendet, und die generierten Klassen/Enums für jede Top-Level-Nachricht, Enumeration und jeden Dienst, die in der .proto-Datei deklariert sind, werden alle innerhalb der äußeren Wrapper-Klasse verschachtelt. Somit generiert der Compiler nur eine einzige .java-Datei für die gesamte .proto-Datei, und sie hat eine zusätzliche Ebene im Paket

Der Name der Wrapper-Klasse wird wie folgt gewählt: Wenn die .proto-Datei eine Zeile wie die folgende enthält

option java_outer_classname = "Foo";

Dann ist der Klassenname des Wrappers Foo. Andernfalls wird der Klassenname des Wrappers durch Umwandlung des Basisnamens der .proto-Datei in Camel-Case bestimmt. Zum Beispiel wird foo_bar.proto einen Klassennamen von FooBar generieren. Wenn es einen Dienst, ein Enum oder eine Nachricht (einschließlich verschachtelter Typen) in der Datei mit demselben Namen gibt, wird "OuterClass" an den Klassennamen des Wrappers angehängt. Beispiele

  • Wenn foo_bar.proto eine Nachricht namens FooBar enthält, generiert die Wrapper-Klasse einen Klassennamen von FooBarOuterClass.
  • Wenn foo_bar.proto einen Dienst namens FooService enthält und java_outer_classname ebenfalls auf den String FooService gesetzt ist, generiert die Wrapper-Klasse einen Klassennamen von FooServiceOuterClass.

Zusätzlich zu allen verschachtelten Klassen verfügt die Wrapper-Klasse selbst über die folgende API (angenommen, die Wrapper-Klasse heißt Foo und wurde aus foo.proto generiert)

public final class Foo {
  private Foo() {}  // Not instantiable.

  /** Returns a FileDescriptor message describing the contents of {@code foo.proto}. */
  public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor();
  /** Adds all extensions defined in {@code foo.proto} to the given registry. */
  public static void registerAllExtensions(com.google.protobuf.ExtensionRegistry registry);
  public static void registerAllExtensions(com.google.protobuf.ExtensionRegistryLite registry);

  // (Nested classes omitted)
}

Der Java-Paketname wird wie unter Packages unten beschrieben gewählt.

Die Ausgabedatei wird durch Verketten des Parameters von --java_out=, des Paketnamens (mit ersetzten . durch /) und des .java-Dateinamens gewählt.

Nehmen wir also zum Beispiel an, Sie rufen den Compiler wie folgt auf:

protoc --proto_path=src --java_out=build/gen src/foo.proto

Wenn das Java-Paket von foo.proto com.example ist und java_multiple_files nicht aktiviert ist und sein outer classname FooProtos ist, generiert der Protokollpuffer-Compiler die Datei build/gen/com/example/FooProtos.java. Der Protokollpuffer-Compiler erstellt automatisch die Verzeichnisse build/gen/com und build/gen/com/example, falls erforderlich. Er erstellt jedoch nicht build/gen oder build; diese müssen bereits vorhanden sein. Sie können mehrere .proto-Dateien in einem einzigen Aufruf angeben; alle Ausgabedateien werden gleichzeitig generiert.

Bei der Ausgabe von Java-Code ist die Fähigkeit des Protokollpuffer-Compilers, direkt in JAR-Archive auszugeben, besonders praktisch, da viele Java-Tools Quellcode direkt aus JAR-Dateien lesen können. Um in eine JAR-Datei auszugeben, geben Sie einfach einen Ausgabespeicherort an, der auf .jar endet. Beachten Sie, dass nur der Java-Quellcode in das Archiv aufgenommen wird; Sie müssen ihn immer noch separat kompilieren, um Java-Klassendateien zu erzeugen.

Pakete (Packages)

Die generierte Klasse wird in einem Java-Paket basierend auf der Option java_package platziert. Wenn die Option weggelassen wird, wird stattdessen die Deklaration package verwendet.

Wenn beispielsweise die .proto-Datei Folgendes enthält

package foo.bar;

Dann wird die resultierende Java-Klasse im Java-Paket foo.bar platziert. Wenn die .proto-Datei jedoch auch eine Option java_package enthält, wie folgt

package foo.bar;
option java_package = "com.example.foo.bar";

Dann wird die Klasse stattdessen im Paket com.example.foo.bar platziert. Die Option java_package wird bereitgestellt, da normale .proto-package-Deklarationen nicht mit einem umgekehrten Domainnamen beginnen sollen.

Nachrichten

Wenn Sie ein neues Protokollpuffer-Schema entwerfen, siehe die Empfehlungen für Java Proto-Namen.

Angesichts einer einfachen Nachrichten Deklaration:

message Foo {}

Der Protokollpuffer-Compiler generiert eine Klasse namens Foo, die die Schnittstelle Message implementiert. Die Klasse wird als final deklariert; keine weitere Unterklassifizierung ist erlaubt. Foo erweitert GeneratedMessage, dies sollte jedoch als Implementierungsdetail betrachtet werden. Standardmäßig überschreibt Foo viele Methoden von GeneratedMessage mit spezialisierten Versionen für maximale Geschwindigkeit. Wenn die .proto-Datei jedoch die Zeile enthält

option optimize_for = CODE_SIZE;

Dann überschreibt Foo nur die minimale Menge an notwendigen Methoden zur Funktion und verlässt sich auf die reflektionsbasierten Implementierungen von GeneratedMessage für die restlichen. Dies reduziert die Größe des generierten Codes erheblich, verringert aber auch die Leistung. Alternativ, wenn die .proto-Datei enthält

option optimize_for = LITE_RUNTIME;

Dann enthält Foo schnelle Implementierungen aller Methoden, implementiert aber die Schnittstelle MessageLite, die eine Teilmenge der Methoden von Message enthält. Insbesondere werden keine Deskriptoren, verschachtelten Builder oder Reflexion unterstützt. In diesem Modus muss der generierte Code jedoch nur gegen libprotobuf-lite.jar statt gegen libprotobuf.jar verlinkt werden. Die "lite"-Bibliothek ist viel kleiner als die vollständige Bibliothek und eignet sich besser für ressourcenbeschränkte Systeme wie Mobiltelefone.

Die Schnittstelle Message definiert Methoden, mit denen Sie die gesamte Nachricht prüfen, manipulieren, lesen oder schreiben können. Zusätzlich zu diesen Methoden definiert die Klasse Foo die folgenden statischen Methoden

  • static Foo getDefaultInstance(): Gibt die *Singleton*-Instanz von Foo zurück. Der Inhalt dieser Instanz ist identisch mit dem, was Sie erhalten würden, wenn Sie Foo.newBuilder().build() aufrufen würden (sodass alle singulären Felder auf nicht gesetzt und alle wiederholten Felder leer sind). Beachten Sie, dass die Standardinstanz einer Nachricht als Fabrik verwendet werden kann, indem ihre Methode newBuilderForType() aufgerufen wird.
  • static Descriptor getDescriptor(): Gibt den Deskriptor des Typs zurück. Dieser enthält Informationen über den Typ, einschließlich seiner Felder und deren Typen. Dies kann mit den Reflexionsmethoden von Message wie getField() verwendet werden.
  • static Foo parseFrom(...): Parst eine Nachricht vom Typ Foo aus der gegebenen Quelle und gibt sie zurück. Für jede Variante von mergeFrom() in der Schnittstelle Message.Builder gibt es eine entsprechende parseFrom-Methode. Beachten Sie, dass parseFrom() niemals UninitializedMessageException wirft; es wirft InvalidProtocolBufferException, wenn die geparste Nachricht erforderliche Felder fehlen. Dies macht es subtil anders als der Aufruf von Foo.newBuilder().mergeFrom(...).build().
  • static Parser parser(): Gibt eine Instanz des Parser zurück, der verschiedene parseFrom()-Methoden implementiert.
  • Foo.Builder newBuilder(): Erstellt einen neuen Builder (wird unten beschrieben).
  • Foo.Builder newBuilder(Foo prototype): Erstellt einen neuen Builder, bei dem alle Felder mit denselben Werten initialisiert sind, die sie in prototype haben. Da eingebettete Nachrichten- und Zeichenkettenobjekte unveränderlich sind, werden sie zwischen Original und Kopie geteilt.

Builder

Nachrichtenobjekte – wie Instanzen der oben beschriebenen Klasse Foo – sind unveränderlich, genau wie eine Java String. Um ein Nachrichtenobjekt zu konstruieren, müssen Sie einen *Builder* verwenden. Jede Nachrichtenklasse hat ihre eigene Builder-Klasse – in unserem Foo-Beispiel generiert der Protokollpuffer-Compiler eine verschachtelte Klasse Foo.Builder, die zum Erstellen eines Foo verwendet werden kann. Foo.Builder implementiert die Schnittstelle Message.Builder. Sie erweitert die Klasse GeneratedMessage.Builder, aber auch dies sollte als Implementierungsdetail betrachtet werden. Wie Foo kann Foo.Builder auf generische Implementierungen in GeneratedMessage.Builder zurückgreifen oder, wenn die Option optimize_for verwendet wird, generierten benutzerdefinierten Code, der viel schneller ist. Sie können einen Foo.Builder erhalten, indem Sie die statische Methode Foo.newBuilder() aufrufen.

Foo.Builder definiert keine statischen Methoden. Seine Schnittstelle ist genau wie von der Schnittstelle Message.Builder definiert, mit der Ausnahme, dass die Rückgabetypen spezifischer sind: Methoden von Foo.Builder, die den Builder modifizieren, geben den Typ Foo.Builder zurück, und build() gibt den Typ Foo zurück.

Methoden, die den Inhalt eines Builders ändern – einschließlich Feldsettern – geben immer eine Referenz auf den Builder zurück (d. h. sie „return this;“). Dies ermöglicht die Verkettung mehrerer Methodenaufrufe in einer Zeile. Zum Beispiel: builder.mergeFrom(obj).setFoo(1).setBar("abc").clearBaz();

Beachten Sie, dass Builder nicht threadsicher sind, daher sollte Java-Synchronisation verwendet werden, wenn es für mehrere verschiedene Threads notwendig ist, den Inhalt eines einzelnen Builders zu modifizieren.

Unter-Builder

Für Nachrichten, die Unternachrichten enthalten, generiert der Compiler auch Unter-Builder. Dies ermöglicht es Ihnen, tief verschachtelte Unternachrichten wiederholt zu modifizieren, ohne sie neu zu erstellen. Zum Beispiel

message Foo {
  int32 val = 1;
  // some other fields.
}

message Bar {
  Foo foo = 1;
  // some other fields.
}

message Baz {
  Bar bar = 1;
  // some other fields.
}

Wenn Sie bereits eine Baz-Nachricht haben und den tief verschachtelten val in Foo ändern möchten. Anstatt

baz = baz.toBuilder().setBar(
    baz.getBar().toBuilder().setFoo(
        baz.getBar().getFoo().toBuilder().setVal(10).build()
    ).build()).build();

Sie können schreiben

Baz.Builder builder = baz.toBuilder();
builder.getBarBuilder().getFooBuilder().setVal(10);
baz = builder.build();

Verschachtelte Typen

Eine Nachricht kann innerhalb einer anderen Nachricht deklariert werden. Zum Beispiel:

message Foo {
  message Bar { }
}

In diesem Fall generiert der Compiler einfach Bar als innere Klasse, die in Foo verschachtelt ist.

Felder

Zusätzlich zu den in den vorherigen Abschnitten beschriebenen Methoden generiert der Protokollpuffer-Compiler eine Reihe von Zugriffsmethoden für jedes Feld, das in der Nachricht in der .proto-Datei definiert ist. Die Methoden, die den Feldwert lesen, sind sowohl in der Nachrichtenklasse als auch in ihrem entsprechenden Builder definiert; die Methoden, die den Wert ändern, sind nur im Builder definiert.

Beachten Sie, dass Methodennamen immer Camel-Case-Namensgebung verwenden, auch wenn der Feldname in der .proto-Datei Kleinbuchstaben mit Unterstrichen verwendet (wie es sein sollte). Die Fallumwandlung funktioniert wie folgt

  • Für jeden Unterstrich im Namen wird der Unterstrich entfernt und der folgende Buchstabe großgeschrieben.
  • Wenn dem Namen ein Präfix (z. B. "get") vorangestellt wird, wird der erste Buchstabe großgeschrieben. Andernfalls wird er kleingeschrieben.
  • Der Buchstabe nach jeder Ziffer in einer Zahl im Methodennamen wird großgeschrieben.

Somit wird das Feld foo_bar_baz zu fooBarBaz. Wenn es mit get präfixiert wäre, wäre es getFooBarBaz. Und foo_ba23r_baz wird zu fooBa23RBaz.

Zusätzlich zu den Zugriffsmethoden generiert der Compiler eine Ganzzahlkonstante für jedes Feld, die seine Feldnummer enthält. Der Konstantenname ist der Feldname, umgewandelt in Großbuchstaben, gefolgt von _FIELD_NUMBER. Zum Beispiel wird für das Feld int32 foo_bar = 5; vom Compiler die Konstante public static final int FOO_BAR_FIELD_NUMBER = 5; generiert.

Die folgenden Abschnitte sind in explizite und implizite Anwesenheit unterteilt. Proto2 hat explizite Anwesenheit und proto3 hat standardmäßig implizite Anwesenheit. Editions haben standardmäßig explizite Anwesenheit, aber Sie können dies mit features.field_presence überschreiben.

Singuläre Felder mit expliziter Anwesenheit

Für jede dieser Felddefinitionen

optional int32 foo = 1;
required int32 foo = 1;

Der Compiler generiert die folgenden Zugriffsmethoden sowohl in der Nachrichtenklasse als auch in ihrem Builder

  • boolean hasFoo(): Gibt true zurück, wenn das Feld gesetzt ist.
  • int getFoo(): Gibt den aktuellen Wert des Feldes zurück. Wenn das Feld nicht gesetzt ist, gibt es den Standardwert zurück.

Der Compiler generiert die folgenden Methoden nur im Builder der Nachricht

  • Builder setFoo(int value): Setzt den Wert des Feldes. Nach dem Aufruf dieser Methode gibt hasFoo() true zurück und getFoo() gibt value zurück.
  • Builder clearFoo(): Löscht den Wert des Feldes. Nach dem Aufruf dieser Methode gibt hasFoo() false zurück und getFoo() gibt den Standardwert zurück.

Für andere einfache Feldtypen wird der entsprechende Java-Typ gemäß der Tabelle der Skalarwerttypen gewählt. Für Nachrichten- und Enum-Typen wird der Werttyp durch die Nachrichten- oder Enum-Klasse ersetzt.

Eingebettete Nachrichtenfelder

Für Nachrichtentypen akzeptiert setFoo() auch eine Instanz des Builder-Typs der Nachricht als Parameter. Dies ist nur eine Abkürzung, die dem Aufruf von .build() auf dem Builder und der Übergabe des Ergebnisses an die Methode entspricht. Weitere Modifikationen des Unter-Builders, der an setFoo übergeben wird, werden sich **nicht** auf den Builder der Nachrichtenklasse auswirken. Der Builder der Nachrichtenklasse "übernimmt den Besitz" der Unter-Nachricht.

Wenn das Feld nicht gesetzt ist, gibt getFoo() eine Foo-Instanz zurück, bei der keine Felder gesetzt sind (möglicherweise die Instanz, die von Foo.getDefaultInstance() zurückgegeben wird).

Zusätzlich generiert der Compiler zwei Zugriffsmethoden, die es Ihnen ermöglichen, auf die relevanten Unter-Builder für Nachrichtentypen zuzugreifen. Die folgende Methode wird sowohl in der Nachrichtenklasse als auch in ihrem Builder generiert

  • FooOrBuilder getFooOrBuilder(): Gibt den Builder für das Feld zurück, wenn er bereits existiert, oder die Nachricht, wenn nicht. Das Aufrufen dieser Methode auf Builder erstellt keinen Unter-Builder für das Feld.

Der Compiler generiert die folgende Methode nur im Builder der Nachricht.

  • Builder getFooBuilder(): Gibt den Builder für das Feld zurück.

Singuläre Felder mit impliziter Anwesenheit

Für diese Felddefinition:

int32 foo = 1;

Der Compiler generiert die folgende Zugriffsmethode sowohl in der Nachrichtenklasse als auch in ihrem Builder

  • int getFoo(): Gibt den aktuellen Wert des Feldes zurück. Wenn das Feld nicht gesetzt ist, gibt es den Standardwert für den Typ des Feldes zurück.

Der Compiler generiert die folgenden Methoden nur im Builder der Nachricht

  • Builder setFoo(int value): Setzt den Wert des Feldes. Nach dem Aufruf dieser Methode gibt getFoo() value zurück.
  • Builder clearFoo(): Löscht den Wert des Feldes. Nach dem Aufruf dieser Methode gibt getFoo() den Standardwert für den Typ des Feldes zurück.

Für andere einfache Feldtypen wird der entsprechende Java-Typ gemäß der Tabelle der Skalarwerttypen gewählt. Für Nachrichten- und Enum-Typen wird der Werttyp durch die Nachrichten- oder Enum-Klasse ersetzt.

Enum-Felder

Für Enum-Feldtypen wird eine zusätzliche Zugriffsmethode sowohl in der Nachrichtenklasse als auch in ihrem Builder generiert

  • int getFooValue(): Gibt den Ganzzahlwert des Enums zurück.

Der Compiler generiert die folgende zusätzliche Methode nur im Builder der Nachricht

  • Builder setFooValue(int value): Setzt den Ganzzahlwert des Enums.

Zusätzlich gibt getFoo() UNRECOGNIZED zurück, wenn der Enum-Wert unbekannt ist – dies ist ein spezieller zusätzlicher Wert, der vom proto3-Compiler zum generierten Enum-Typ hinzugefügt wird.

Wiederholte Felder

Für diese Felddefinition:

repeated string foos = 1;

Der Compiler generiert die folgenden Zugriffsmethoden sowohl in der Nachrichtenklasse als auch in ihrem Builder

  • int getFoosCount(): Gibt die aktuelle Anzahl der Elemente im Feld zurück.
  • String getFoos(int index): Gibt das Element am gegebenen nullbasierten Index zurück.
  • ProtocolStringList getFoosList(): Gibt das gesamte Feld als ProtocolStringList zurück. Wenn das Feld nicht gesetzt ist, wird eine leere Liste zurückgegeben.

Der Compiler generiert die folgenden Methoden nur im Builder der Nachricht

  • Builder setFoos(int index, String value): Setzt den Wert des Elements am gegebenen nullbasierten Index.
  • Builder addFoos(String value): Fügt dem Feld ein neues Element mit dem gegebenen Wert hinzu.
  • Builder addAllFoos(Iterable<? extends String> value): Fügt alle Elemente des gegebenen Iterable zum Feld hinzu.
  • Builder clearFoos(): Entfernt alle Elemente aus dem Feld. Nach dem Aufruf dieser Methode gibt getFoosCount() null zurück.

Für andere einfache Feldtypen wird der entsprechende Java-Typ gemäß der Tabelle der Skalarwerttypen gewählt. Für Nachrichten- und Enum-Typen ist der Typ die Nachrichten- oder Enum-Klasse.

Wiederholte eingebettete Nachrichtenfelder

Für Nachrichtentypen akzeptieren setFoos() und addFoos() auch eine Instanz des Builder-Typs der Nachricht als Parameter. Dies ist nur eine Abkürzung, die dem Aufruf von .build() auf dem Builder und der Übergabe des Ergebnisses an die Methode entspricht. Es gibt auch eine zusätzliche generierte Methode

  • Builder addFoos(int index, Field value): Fügt ein neues Element am gegebenen nullbasierten Index ein. Verschiebt das Element, das sich derzeit an dieser Position befindet (falls vorhanden) und alle nachfolgenden Elemente nach rechts (erhöht ihre Indizes um eins).

Zusätzlich generiert der Compiler die folgenden zusätzlichen Zugriffsmethoden sowohl in der Nachrichtenklasse als auch in ihrem Builder für Nachrichtentypen, die es Ihnen ermöglichen, auf die relevanten Unter-Builder zuzugreifen

  • FooOrBuilder getFoosOrBuilder(int index): Gibt den Builder für das angegebene Element zurück, wenn er bereits existiert, oder wirft IndexOutOfBoundsException, wenn nicht. Wenn diese Methode von einer Nachrichtenklasse aufgerufen wird, gibt sie immer eine Nachricht (oder wirft eine Ausnahme) anstelle eines Builders zurück. Das Aufrufen dieser Methode auf Builder erstellt keinen Unter-Builder für das Feld.
  • List<FooOrBuilder> getFoosOrBuilderList(): Gibt das gesamte Feld als unveränderliche Liste von Buildern (falls verfügbar) oder Nachrichten zurück, wenn nicht. Wenn diese Methode von einer Nachrichtenklasse aufgerufen wird, gibt sie immer eine unveränderliche Liste von Nachrichten anstelle einer unveränderlichen Liste von Buildern zurück.

Der Compiler generiert die folgenden Methoden nur im Builder der Nachricht

  • Builder getFoosBuilder(int index): Gibt den Builder für das Element am angegebenen Index zurück oder wirft IndexOutOfBoundsException, wenn der Index außerhalb der Grenzen liegt.
  • Builder addFoosBuilder(int index): Fügt einen Builder für eine Standardnachrichteninstanz der wiederholten Nachricht am angegebenen Index ein und gibt ihn zurück. Die vorhandenen Einträge werden zu höheren Indizes verschoben, um Platz für den eingefügten Builder zu schaffen.
  • Builder addFoosBuilder(): Fügt einen Builder für eine Standardnachrichteninstanz der wiederholten Nachricht hinzu und gibt ihn zurück.
  • Builder removeFoos(int index): Entfernt das Element am gegebenen nullbasierten Index.
  • List<Builder> getFoosBuilderList(): Gibt das gesamte Feld als unveränderliche Liste von Buildern zurück.

Wiederholte Enum-Felder (nur proto3)

Der Compiler generiert die folgenden zusätzlichen Methoden sowohl in der Nachrichtenklasse als auch in ihrem Builder

  • int getFoosValue(int index): Gibt den Ganzzahlwert des Enums am angegebenen Index zurück.
  • List<java.lang.Integer> getFoosValueList(): Gibt das gesamte Feld als Liste von Integers zurück.

Der Compiler generiert die folgende zusätzliche Methode nur im Builder der Nachricht

  • Builder setFoosValue(int index, int value): Setzt den Ganzzahlwert des Enums am angegebenen Index.

Namenskollisionen

Wenn ein anderes nicht-wiederholtes Feld einen Namen hat, der mit einer der generierten Methoden des wiederholten Feldes kollidiert, wird beiden Feldnamen ihre Protobuf-Feldnummer am Ende angehängt.

Für diese Felddefinitionen

int32 foos_count = 1;
repeated string foos = 2;

Der Compiler benennt sie zunächst wie folgt um

int32 foos_count_1 = 1;
repeated string foos_2 = 2;

Die Zugriffsmethoden werden anschließend wie oben beschrieben generiert.

Oneof-Felder

Für diese oneof-Felddefinition

oneof choice {
    int32 foo_int = 4;
    string foo_string = 9;
    ...
}

Alle Felder im choice oneof verwenden ein einzelnes privates Feld für ihren Wert. Zusätzlich generiert der Protokollpuffer-Compiler einen Java-Enum-Typ für den oneof-Fall wie folgt

public enum ChoiceCase
        implements com.google.protobuf.Internal.EnumLite {
      FOO_INT(4),
      FOO_STRING(9),
      ...
      CHOICE_NOT_SET(0);
      ...
    };

Die Werte dieses Enum-Typs haben die folgenden speziellen Methoden

  • int getNumber(): Gibt den numerischen Wert des Objekts zurück, wie in der .proto-Datei definiert.
  • static ChoiceCase forNumber(int value): Gibt das Enum-Objekt zurück, das dem gegebenen numerischen Wert entspricht (oder null für andere numerische Werte).

Der Compiler generiert die folgenden Zugriffsmethoden sowohl in der Nachrichtenklasse als auch in ihrem Builder

  • boolean hasFooInt(): Gibt true zurück, wenn der oneof-Fall FOO_INT ist.
  • int getFooInt(): Gibt den aktuellen Wert von foo zurück, wenn der oneof-Fall FOO_INT ist. Andernfalls gibt er den Standardwert dieses Feldes zurück.
  • ChoiceCase getChoiceCase(): Gibt das Enum zurück, das angibt, welches Feld gesetzt ist. Gibt CHOICE_NOT_SET zurück, wenn keines von ihnen gesetzt ist.

Der Compiler generiert die folgenden Methoden nur im Builder der Nachricht

  • Builder setFooInt(int value): Setzt Foo auf diesen Wert und setzt den oneof-Fall auf FOO_INT. Nach dem Aufruf dieser Methode gibt hasFooInt() true zurück, getFooInt() gibt value zurück und getChoiceCase() gibt FOO_INT zurück.
  • Builder clearFooInt():
    • Nichts wird geändert, wenn der oneof-Fall nicht FOO_INT ist.
    • Wenn der oneof-Fall FOO_INT ist, wird Foo auf null gesetzt und der oneof-Fall auf CHOICE_NOT_SET. Nach dem Aufruf dieser Methode gibt hasFooInt() false zurück, getFooInt() gibt den Standardwert zurück und getChoiceCase() gibt CHOICE_NOT_SET zurück.
  • Builder.clearChoice(): Setzt den Wert für choice zurück und gibt den Builder zurück.

Für andere einfache Feldtypen wird der entsprechende Java-Typ gemäß der Tabelle der Skalarwerttypen gewählt. Für Nachrichten- und Enum-Typen wird der Werttyp durch die Nachrichten- oder Enum-Klasse ersetzt.

Map-Felder

Für diese Map-Felddefinition

map<int32, int32> weight = 1;

Der Compiler generiert die folgenden Zugriffsmethoden sowohl in der Nachrichtenklasse als auch in ihrem Builder

  • Map<Integer, Integer> getWeightMap();: Gibt eine unveränderliche Map zurück.
  • int getWeightOrDefault(int key, int default);: Gibt den Wert für den Schlüssel oder den Standardwert zurück, wenn er nicht vorhanden ist.
  • int getWeightOrThrow(int key);: Gibt den Wert für den Schlüssel zurück oder wirft IllegalArgumentException, wenn er nicht vorhanden ist.
  • boolean containsWeight(int key);: Zeigt an, ob der Schlüssel in diesem Feld vorhanden ist.
  • int getWeightCount();: Gibt die Anzahl der Elemente in der Map zurück.

Der Compiler generiert die folgenden Methoden nur im Builder der Nachricht

  • Builder putWeight(int key, int value);: Fügt das Gewicht zu diesem Feld hinzu.
  • Builder putAllWeight(Map<Integer, Integer> value);: Fügt alle Einträge der gegebenen Map zu diesem Feld hinzu.
  • Builder removeWeight(int key);: Entfernt das Gewicht aus diesem Feld.
  • Builder clearWeight();: Entfernt alle Gewichte aus diesem Feld.
  • @Deprecated Map<Integer, Integer> getMutableWeight();: Gibt eine veränderbare Map zurück. Beachten Sie, dass mehrere Aufrufe dieser Methode unterschiedliche Map-Instanzen zurückgeben können. Die zurückgegebene Map-Referenz kann durch nachfolgende Methodenaufrufe an den Builder ungültig gemacht werden.

Map-Felder mit Nachrichten als Wert

Für Maps mit Nachrichtentypen als Werten generiert der Compiler eine zusätzliche Methode im Builder der Nachricht

  • Foo.Builder putFooBuilderIfAbsent(int key);: Stellt sicher, dass key in der Zuordnung vorhanden ist, und fügt einen neuen Foo.Builder ein, falls noch keiner existiert. Änderungen am zurückgegebenen Foo.Builder werden in der endgültigen Nachricht widergespiegelt.

Any

Angenommen, ein Feld vom Typ Any wie folgt

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  google.protobuf.Any details = 2;
}

In unserem generierten Code gibt der Getter für das Feld details eine Instanz von com.google.protobuf.Any zurück. Dies bietet die folgenden speziellen Methoden zum Packen und Entpacken der Werte von Any

class Any {
  // Packs the given message into an Any using the default type URL
  // prefix “type.googleapis.com”.
  public static Any pack(Message message);
  // Packs the given message into an Any using the given type URL
  // prefix.
  public static Any pack(Message message,
                         String typeUrlPrefix);

  // Checks whether this Any message’s payload is the given type.
  public <T extends Message> boolean is(class<T> clazz);

  // Checks whether this Any message’s payload has the same type as the given
  // message.
  public boolean isSameTypeAs(Message message);

  // Unpacks Any into a message with the same type as the given messsage.
  // Throws exception if the type doesn’t match or parsing the payload fails.
  public <T extends Message> T unpackSameTypeAs(T message)
      throws InvalidProtocolBufferException;

  // Unpacks Any into the given message type. Throws exception if
  // the type doesn’t match or parsing the payload has failed.
  public <T extends Message> T unpack(class<T> clazz)
      throws InvalidProtocolBufferException;
}

Aufzählungen (Enums)

Gegeben eine Enum-Definition wie

enum Foo {
  VALUE_A = 0;
  VALUE_B = 5;
  VALUE_C = 1234;
}

Der Protokollpuffer-Compiler generiert einen Java-Enum-Typ namens Foo mit demselben Satz von Werten. Wenn Sie proto3 verwenden, fügt er auch den speziellen Wert UNRECOGNIZED zum Enum-Typ hinzu. In Editions haben OPEN-Enums auch einen UNRECOGNIZED-Wert, während CLOSED-Enums dies nicht haben. Die Werte des generierten Enum-Typs haben die folgenden speziellen Methoden

  • int getNumber(): Gibt den numerischen Wert des Objekts zurück, wie in der .proto-Datei definiert.
  • EnumValueDescriptor getValueDescriptor(): Gibt den Deskriptor des Wertes zurück, der Informationen über den Namen, die Nummer und den Typ des Wertes enthält.
  • EnumDescriptor getDescriptorForType(): Gibt den Deskriptor des Enum-Typs zurück, der z. B. Informationen über jeden definierten Wert enthält.

Zusätzlich enthält der Enum-Typ Foo die folgenden statischen Methoden

  • static Foo forNumber(int value): Gibt das Enum-Objekt zurück, das dem gegebenen numerischen Wert entspricht. Gibt null zurück, wenn kein entsprechendes Enum-Objekt vorhanden ist.
  • static Foo valueOf(int value): Gibt das Enum-Objekt zurück, das dem gegebenen numerischen Wert entspricht. Diese Methode ist zugunsten von forNumber(int value) veraltet und wird in einer zukünftigen Version entfernt.
  • static Foo valueOf(EnumValueDescriptor descriptor): Gibt das Enum-Objekt zurück, das dem gegebenen Wertdeskriptor entspricht. Kann schneller sein als valueOf(int). In proto3 und OPEN-Enums gibt es UNRECOGNIZED zurück, wenn ein unbekannter Wertdeskriptor übergeben wird.
  • EnumDescriptor getDescriptor(): Gibt den Deskriptor des Enum-Typs zurück, der z. B. Informationen über jeden definierten Wert enthält. (Dies unterscheidet sich von getDescriptorForType() nur dadurch, dass es sich um eine statische Methode handelt.)

Eine Ganzzahlkonstante wird auch mit dem Suffix _VALUE für jeden Enum-Wert generiert.

Beachten Sie, dass die .proto-Sprache erlaubt, dass mehrere Enum-Symbole denselben numerischen Wert haben. Symbole mit demselben numerischen Wert sind Synonyme. Zum Beispiel

enum Foo {
  BAR = 0;
  BAZ = 0;
}

In diesem Fall ist BAZ ein Synonym für BAR. In Java wird BAZ als statisches final Feld wie folgt definiert

static final Foo BAZ = BAR;

Daher sind BAR und BAZ gleichwertig, und BAZ sollte niemals in switch-Anweisungen erscheinen. Der Compiler wählt immer das erste mit einem gegebenen numerischen Wert definierte Symbol als die "kanonische" Version dieses Symbols; alle nachfolgenden Symbole mit derselben Nummer sind nur Aliase.

Ein Enum kann verschachtelt innerhalb eines Nachrichtentyps definiert werden. Der Compiler generiert die Java-Enum-Definition verschachtelt innerhalb der Klasse dieses Nachrichtentyps.

Vorsicht: Bei der Generierung von Java-Code kann die maximale Anzahl von Werten in einem Protobuf-Enum überraschend niedrig sein – im schlimmsten Fall liegt das Maximum bei etwas über 1.700 Werten. Dieses Limit ist auf Größenbeschränkungen pro Methode für Java-Bytecode zurückzuführen und variiert je nach Java-Implementierung, verschiedenen Versionen der Protobuf-Suite und Optionen, die im .proto-File für das Enum gesetzt wurden.

Erweiterungen (Extensions)

Gegeben eine Nachricht mit einem Erweiterungsbereich

edition = "2023";

message Foo {
  extensions 100 to 199;
}

Der Protokollpuffer-Compiler lässt Foo von GeneratedMessage.ExtendableMessage statt des üblichen GeneratedMessage erben. Ebenso wird der Builder von Foo von GeneratedMessage.ExtendableBuilder erben. Sie sollten niemals auf diese Basistypen namentlich verweisen (GeneratedMessage wird als Implementierungsdetail betrachtet). Diese Superklassen definieren jedoch eine Reihe zusätzlicher Methoden, mit denen Sie Erweiterungen manipulieren können.

Insbesondere Foo und Foo.Builder erben die Methoden hasExtension(), getExtension() und getExtensionCount(). Zusätzlich erbt Foo.Builder die Methoden setExtension() und clearExtension(). Jede dieser Methoden nimmt als ersten Parameter einen Erweiterungsidentifikator (wird unten beschrieben), der ein Erweiterungsfeld identifiziert. Die übrigen Parameter und der Rückgabewert sind genau dieselben wie bei den entsprechenden Zugriffsmethoden, die für ein normales (nicht-erweitertes) Feld desselben Typs wie der Erweiterungsidentifikator generiert würden.

Gegeben eine Erweiterungsdefinition

edition = "2023";

import "foo.proto";

extend Foo {
  int32 bar = 123;
}

Der Protokollpuffer-Compiler generiert einen "Erweiterungsidentifikator" namens bar, den Sie mit den Erweiterungszugreifern von Foo verwenden können, um auf diese Erweiterung zuzugreifen, wie folgt

Foo foo =
  Foo.newBuilder()
     .setExtension(bar, 1)
     .build();
assert foo.hasExtension(bar);
assert foo.getExtension(bar) == 1;

(Die genaue Implementierung von Erweiterungsidentifikatoren ist kompliziert und beinhaltet magischen Gebrauch von Generics – Sie müssen jedoch nicht wissen, wie Erweiterungsidentifikatoren funktionieren, um sie zu verwenden.)

Beachten Sie, dass bar als statisches Feld der Wrapper-Klasse für die .proto-Datei deklariert würde, wie oben beschrieben; wir haben den Namen der Wrapper-Klasse im Beispiel weggelassen.

Erweiterungen können innerhalb des Geltungsbereichs eines anderen Typs deklariert werden, um ihre generierten Symbolnamen zu präfixieren. Ein gängiges Muster ist beispielsweise, eine Nachricht durch ein Feld *innerhalb* der Deklaration des Typs des Feldes zu erweitern

edition = "2023";

import "foo.proto";

message Baz {
  extend Foo {
    Baz foo_ext = 124;
  }
}

In diesem Fall wird eine Erweiterung mit dem Bezeichner foo_ext und dem Typ Baz innerhalb der Deklaration von Baz deklariert, und die Referenzierung von foo_ext erfordert die Hinzufügung eines Baz. Präfixes

Baz baz = createMyBaz();
Foo foo =
  Foo.newBuilder()
     .setExtension(Baz.fooExt, baz)
     .build();
assert foo.hasExtension(Baz.fooExt);
assert foo.getExtension(Baz.fooExt) == baz;

Beim Parsen einer Nachricht, die Erweiterungen enthalten könnte, müssen Sie ein ExtensionRegistry bereitstellen, in dem Sie alle Erweiterungen registriert haben, die Sie parsen möchten. Andernfalls werden diese Erweiterungen wie unbekannte Felder behandelt und die Methoden, die Erweiterungen beobachten, verhalten sich so, als ob sie nicht existieren.

ExtensionRegistry registry = ExtensionRegistry.newInstance();
registry.add(Baz.fooExt);
Foo foo = Foo.parseFrom(input, registry);
assert foo.hasExtension(Baz.fooExt);
ExtensionRegistry registry = ExtensionRegistry.newInstance();
Foo foo = Foo.parseFrom(input, registry);
assert foo.hasExtension(Baz.fooExt) == false;

Services

Wenn die .proto-Datei die folgende Zeile enthält:

option java_generic_services = true;

Dann generiert der Protokollpuffer-Compiler Code basierend auf den in der Datei gefundenen Dienstdefinitionen, wie in diesem Abschnitt beschrieben. Der generierte Code ist jedoch möglicherweise unerwünscht, da er nicht an ein bestimmtes RPC-System gebunden ist und daher mehr Indirektionsstufen als Code erfordert, der auf ein System zugeschnitten ist. Wenn Sie nicht möchten, dass dieser Code generiert wird, fügen Sie diese Zeile zur Datei hinzu

option java_generic_services = false;

Wenn keine der oben genannten Zeilen angegeben ist, ist die Option standardmäßig false, da generische Dienste veraltet sind. (Beachten Sie, dass vor 2.4.0 die Option standardmäßig true ist)

RPC-Systeme, die auf .proto-Sprachservice-Definitionen basieren, sollten Plugins bereitstellen, um Code zu generieren, der für das System geeignet ist. Diese Plugins erfordern wahrscheinlich, dass abstrakte Dienste deaktiviert werden, damit sie eigene Klassen mit denselben Namen generieren können.

Der Rest dieses Abschnitts beschreibt, was der Protocol Buffer Compiler generiert, wenn abstrakte Dienste aktiviert sind.

Schnittstelle

Gegeben eine Service-Definition

service Foo {
  rpc Bar(FooRequest) returns(FooResponse);
}

Der Protokollpuffer-Compiler generiert eine abstrakte Klasse Foo, um diesen Dienst darzustellen. Foo hat eine abstrakte Methode für jede Methode, die in der Dienstdefinition enthalten ist. In diesem Fall ist die Methode Bar wie folgt definiert

abstract void bar(RpcController controller, FooRequest request,
                  RpcCallback<FooResponse> done);

Die Parameter sind äquivalent zu den Parametern von Service.CallMethod(), mit der Ausnahme, dass das Argument method impliziert ist und request und done ihren exakten Typ angeben.

Foo unterklassifiziert die Service-Schnittstelle. Der Protocol Buffer Compiler generiert automatisch Implementierungen der Methoden von Service wie folgt:

  • getDescriptorForType: Gibt den ServiceDescriptor des Dienstes zurück.
  • callMethod: Ermittelt anhand des bereitgestellten Methodendeskriptors, welche Methode aufgerufen wird, und ruft sie direkt auf, wobei die Request-Nachricht und der Callback auf die korrekten Typen abwärtskonvertiert werden.
  • getRequestPrototype und getResponsePrototype: Gibt die Standardinstanz der Anfrage oder Antwort des korrekten Typs für die gegebene Methode zurück.

Die folgende statische Methode wird ebenfalls generiert

  • static ServiceDescriptor getServiceDescriptor(): Gibt den Deskriptor des Typs zurück, der Informationen darüber enthält, welche Methoden dieser Dienst hat und welche Eingabe- und Ausgabetypen sie haben.

Foo wird auch eine verschachtelte Schnittstelle Foo.Interface enthalten. Dies ist eine reine Schnittstelle, die wiederum Methoden enthält, die jeder Methode in Ihrer Dienstdefinition entsprechen. Diese Schnittstelle erbt jedoch nicht von der Service-Schnittstelle. Dies ist ein Problem, da RPC-Serverimplementierungen normalerweise für die Verwendung abstrakter Service-Objekte und nicht Ihres speziellen Dienstes geschrieben werden. Um dieses Problem zu lösen, können Sie, wenn Sie ein Objekt impl haben, das Foo.Interface implementiert, Foo.newReflectiveService(impl) aufrufen, um eine Instanz von Foo zu erstellen, die einfach an impl delegiert und Service implementiert.

Zusammenfassend lässt sich sagen, dass Sie bei der Implementierung Ihres eigenen Dienstes zwei Möglichkeiten haben

  • Leiten Sie von Foo ab und implementieren Sie seine Methoden entsprechend, übergeben Sie dann Instanzen Ihrer abgeleiteten Klasse direkt an die RPC-Serverimplementierung. Dies ist in der Regel am einfachsten, wird aber von einigen als weniger „rein“ angesehen.
  • Implementieren Sie Foo.Interface und verwenden Sie Foo.newReflectiveService(Foo.Interface), um einen Service zu erstellen, der ihn umschließt, und übergeben Sie dann den Wrapper an Ihre RPC-Implementierung.

Stub

Der Protocol Buffer-Compiler generiert auch eine „Stub“-Implementierung jeder Dienstschnittstelle, die von Clients verwendet wird, die Anfragen an Server senden möchten, die den Dienst implementieren. Für den Foo-Dienst (oben) wird die Stub-Implementierung Foo.Stub als verschachtelte Klasse definiert.

Foo.Stub ist eine Unterklasse von Foo, die außerdem die folgenden Methoden implementiert

  • Foo.Stub(RpcChannel channel): Erstellt einen neuen Stub, der Anfragen über den gegebenen Kanal sendet.
  • RpcChannel getChannel(): Gibt den Kanal dieses Stubs zurück, wie er an den Konstruktor übergeben wurde.

Der Stub implementiert zusätzlich jede der Dienstmethoden als Wrapper um den Kanal. Das Aufrufen einer der Methoden ruft einfach channel.callMethod() auf.

Die Protocol Buffer-Bibliothek enthält keine RPC-Implementierung. Sie enthält jedoch alle Werkzeuge, die Sie benötigen, um eine generierte Dienstklasse mit jeder beliebigen RPC-Implementierung Ihrer Wahl zu verbinden. Sie müssen lediglich Implementierungen von RpcChannel und RpcController bereitstellen.

Blockierende Schnittstellen

Die oben beschriebenen RPC-Klassen haben alle nicht-blockierende Semantik: Wenn Sie eine Methode aufrufen, stellen Sie ein Callback-Objekt bereit, das aufgerufen wird, sobald die Methode abgeschlossen ist. Oft ist es einfacher (wenn auch möglicherweise weniger skalierbar), Code mit blockierender Semantik zu schreiben, bei dem die Methode einfach nicht zurückkehrt, bis sie abgeschlossen ist. Um dem Rechnung zu tragen, generiert der Protocol Buffer-Compiler auch blockierende Versionen Ihrer Dienstklasse. Foo.BlockingInterface ist äquivalent zu Foo.Interface, mit der Ausnahme, dass jede Methode einfach das Ergebnis zurückgibt, anstatt einen Callback aufzurufen. So ist beispielsweise bar definiert als

abstract FooResponse bar(RpcController controller, FooRequest request)
                         throws ServiceException;

Analog zu nicht-blockierenden Diensten gibt Foo.newReflectiveBlockingService(Foo.BlockingInterface) einen BlockingService zurück, der eine Foo.BlockingInterface umschließt. Schließlich gibt Foo.BlockingStub eine Stub-Implementierung von Foo.BlockingInterface zurück, die Anfragen an einen bestimmten BlockingRpcChannel sendet.

Plugin-Einfügepunkte

Code-Generator-Plugins, die die Ausgabe des Java-Code-Generators erweitern möchten, können Code der folgenden Typen unter Angabe der entsprechenden Einfügepunktnamen einfügen.

  • outer_class_scope: Mitgliedsdeklarationen, die in die Wrapper-Klasse der Datei gehören.
  • class_scope:TYPENAME: Mitgliedsdeklarationen, die in eine Nachrichtenklasse gehören. TYPENAME ist der vollständige Proto-Name, z. B. package.MessageType.
  • builder_scope:TYPENAME: Mitgliedsdeklarationen, die in eine Builder-Klasse einer Nachricht gehören. TYPENAME ist der vollständige Proto-Name, z. B. package.MessageType.
  • enum_scope:TYPENAME: Mitgliedsdeklarationen, die in eine Enum-Klasse gehören. TYPENAME ist der vollständige Proto-Enum-Name, z. B. package.EnumType.
  • message_implements:TYPENAME: Klassenschnittstellendeklarationen für eine Nachrichtenklasse. TYPENAME ist der vollständige Proto-Name, z. B. package.MessageType.
  • builder_implements:TYPENAME: Klassenschnittstellendeklarationen für eine Builder-Klasse. TYPENAME ist der vollständige Proto-Name, z. B. package.MessageType.

Generierter Code darf keine Import-Anweisungen enthalten, da diese zu Konflikten mit Typnamen führen können, die im generierten Code selbst definiert sind. Stattdessen müssen Sie beim Verweisen auf eine externe Klasse immer deren vollqualifizierten Namen verwenden.

Die Logik zur Bestimmung von Ausgabedateinamen im Java-Code-Generator ist ziemlich kompliziert. Sie sollten wahrscheinlich den protoc-Quellcode, insbesondere java_headers.cc, konsultieren, um sicherzustellen, dass Sie alle Fälle abgedeckt haben.

Generieren Sie keinen Code, der von privaten Klassenmitgliedern abhängt, die vom Standard-Code-Generator deklariert werden, da sich diese Implementierungsdetails in zukünftigen Versionen von Protocol Buffers ändern können.

Dienstprogramme

Protocol Buffer bietet Dienstprogramme für den Nachrichtenvergleich, die JSON-Konvertierung und die Arbeit mit Well-Known Types (vordefinierte Protocol Buffer-Nachrichten für häufige Anwendungsfälle).