Java 產生程式碼指南

說明協定緩衝區編譯器針對任何給定的協定定義所產生的 Java 程式碼。

proto2 和 proto3 產生程式碼之間的任何差異都會被突顯 — 請注意,這些差異在於本文檔中描述的產生程式碼,而不是基本訊息類別/介面,它們在兩個版本中是相同的。在閱讀本文檔之前,您應該先閱讀 proto2 語言指南 和/或 proto3 語言指南

請注意,除非另有說明,否則 Java 協定緩衝區方法都不接受或傳回 null。

編譯器調用

當使用 --java_out= 命令列旗標調用時,協定緩衝區編譯器會產生 Java 輸出。--java_out= 選項的參數是您希望編譯器寫入 Java 輸出的目錄。對於每個 .proto 檔案輸入,編譯器都會建立一個包裝器 .java 檔案,其中包含代表 .proto 檔案本身的 Java 類別。

如果 .proto 檔案包含如下行

option java_multiple_files = true;

那麼編譯器也會為每個類別/列舉建立單獨的 .java 檔案,它將為 .proto 檔案中宣告的每個頂層訊息、列舉和服務產生這些檔案。

否則(當 java_multiple_files 選項停用時,這是預設值),前面提到的包裝器類別也將用作外部類別,並且為 .proto 檔案中宣告的每個頂層訊息、列舉和服務產生的類別/列舉都將巢狀在外部包裝器類別中。因此,編譯器將僅為整個 .proto 檔案產生一個 .java 檔案,並且它在套件中會多一層

包裝器類別的名稱選擇方式如下:如果 .proto 檔案包含如下行

option java_outer_classname = "Foo";

那麼包裝器類別名稱將為 Foo。否則,包裝器類別名稱由將 .proto 檔案基本名稱轉換為駝峰式命名法來決定。例如,foo_bar.proto 將產生類別名稱 FooBar。如果檔案中有一個名稱相同的服務、列舉或訊息(包括巢狀類型),則會在包裝器類別的名稱後附加「OuterClass」。範例

  • 如果 foo_bar.proto 包含一個名為 FooBar 的訊息,則包裝器類別將產生類別名稱 FooBarOuterClass
  • 如果 foo_bar.proto 包含一個名為 FooService 的服務,並且 java_outer_classname 也設定為字串 FooService,則包裝器類別將產生類別名稱 FooServiceOuterClass

除了任何巢狀類別之外,包裝器類別本身還將具有以下 API(假設包裝器類別名為 Foo 且是從 foo.proto 產生的)

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)
}

Java 套件名稱的選擇方式如下所述:套件,如下所述。

輸出檔案的選擇方式是將 --java_out= 的參數、套件名稱(將 . 替換為 /)和 .java 檔案名稱串連起來。

因此,例如,假設您如下調用編譯器

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

如果 foo.proto 的 Java 套件是 com.example,並且它未啟用 java_multiple_files,且其外部類別名稱為 FooProtos,則協定緩衝區編譯器將產生檔案 build/gen/com/example/FooProtos.java。如果需要,協定緩衝區編譯器將自動建立 build/gen/combuild/gen/com/example 目錄。但是,它不會建立 build/genbuild;它們必須已存在。您可以在單次調用中指定多個 .proto 檔案;所有輸出檔案將一次產生。

在輸出 Java 程式碼時,協定緩衝區編譯器直接輸出到 JAR 歸檔檔案的功能特別方便,因為許多 Java 工具都能夠直接從 JAR 檔案讀取原始碼。若要輸出到 JAR 檔案,只需提供以 .jar 結尾的輸出位置即可。請注意,只有 Java 原始碼會放置在歸檔檔案中;您仍然必須單獨編譯它才能產生 Java 類別檔案。

套件

產生的類別會根據 java_package 選項放置在 Java 套件中。如果省略該選項,則會改為使用 package 宣告。

例如,如果 .proto 檔案包含

package foo.bar;

那麼產生的 Java 類別將放置在 Java 套件 foo.bar 中。但是,如果 .proto 檔案也包含 java_package 選項,如下所示

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

那麼該類別會改為放置在 com.example.foo.bar 套件中。提供 java_package 選項的原因是,一般的 .proto package 宣告預期不會以反向網域名稱開頭。

訊息

如果您正在設計新的協定緩衝區結構描述,請參閱 Java proto 名稱的建議

給定一個簡單的訊息宣告

message Foo {}

協定緩衝區編譯器會產生一個名為 Foo 的類別,它實作了 Message 介面。該類別宣告為 final;不允許進一步子類別化。Foo 擴充了 GeneratedMessage,但這應被視為實作細節。預設情況下,Foo 會使用針對最大速度的特殊版本覆寫 GeneratedMessage 的許多方法。但是,如果 .proto 檔案包含以下行

option optimize_for = CODE_SIZE;

那麼 Foo 將僅覆寫運作所需的最少方法集,並依賴 GeneratedMessage 的其餘方法的基於反射的實作。這顯著減少了產生的程式碼大小,但也降低了效能。或者,如果 .proto 檔案包含

option optimize_for = LITE_RUNTIME;

那麼 Foo 將包含所有方法的快速實作,但將實作 MessageLite 介面,其中包含 Message 方法的子集。特別是,它不支援描述符、巢狀建構器或反射。但是,在此模式下,產生的程式碼只需要連結到 libprotobuf-lite.jar 而不是 libprotobuf.jar。「lite」程式庫比完整程式庫小得多,更適合資源受限的系統,例如手機。

Message 介面定義了讓您檢查、操作、讀取或寫入整個訊息的方法。除了這些方法之外,Foo 類別還定義了以下靜態方法

  • static Foo getDefaultInstance():傳回 Foo單例實例。此實例的內容與您調用 Foo.newBuilder().build() 所獲得的內容相同(因此所有單數欄位都未設定,並且所有重複欄位都為空)。請注意,訊息的預設實例可以透過調用其 newBuilderForType() 方法用作factory。
  • static Descriptor getDescriptor():傳回類型的描述符。其中包含有關類型的信息,包括它具有哪些欄位以及它們的類型是什麼。這可以與 Message 的反射方法(例如 getField())一起使用。
  • static Foo parseFrom(...):從給定的來源剖析 Foo 類型的訊息並傳回它。Message.Builder 介面中的每個 mergeFrom() 變體都對應一個 parseFrom 方法。請注意,parseFrom() 永遠不會擲回 UninitializedMessageException;如果剖析的訊息缺少必要欄位,則會擲回 InvalidProtocolBufferException。這使得它與調用 Foo.newBuilder().mergeFrom(...).build() 略有不同。
  • static Parser parser():傳回 Parser 的實例,它實作了各種 parseFrom() 方法。
  • Foo.Builder newBuilder():建立新的建構器(如下所述)。
  • Foo.Builder newBuilder(Foo prototype):建立一個新的建構器,其中所有欄位都初始化為與 prototype 中相同的值。由於嵌入式訊息和字串物件是不可變的,因此它們在原始物件和副本之間共享。

建構器

訊息物件(例如上述 Foo 類別的實例)是不可變的,就像 Java String 一樣。若要建構訊息物件,您需要使用建構器。每個訊息類別都有自己的建構器類別 — 因此在我們的 Foo 範例中,協定緩衝區編譯器會產生一個巢狀類別 Foo.Builder,可用於建構 FooFoo.Builder 實作了 Message.Builder 介面。它擴充了 GeneratedMessage.Builder 類別,但是,同樣,這應被視為實作細節。與 Foo 類似,Foo.Builder 可能依賴 GeneratedMessage.Builder 中的通用方法實作,或者,當使用 optimize_for 選項時,產生速度更快的自訂程式碼。您可以透過調用靜態方法 Foo.newBuilder() 來取得 Foo.Builder

Foo.Builder 未定義任何靜態方法。它的介面與 Message.Builder 介面定義的完全相同,但傳回類型更具體:修改建構器的方法傳回類型 Foo.Builder,而 build() 傳回類型 Foo

修改建構器內容的方法(包括欄位 Setter)始終傳回對建構器的參考(即,它們「return this;」)。這允許將多個方法調用鏈接在一起在一行中。例如:builder.mergeFrom(obj).setFoo(1).setBar("abc").clearBaz();

請注意,建構器不是執行緒安全的,因此,只要多個不同的執行緒需要修改單個建構器的內容,就應使用 Java 同步。

子建構器

對於包含子訊息的訊息,編譯器還會產生子建構器。這允許您重複修改深層巢狀的子訊息,而無需重建它們。例如

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

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

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

如果您已經有一個 Baz 訊息,並且想要變更 Foo 中深層巢狀的 val。而不是

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

您可以寫成

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

巢狀類型

訊息可以宣告在另一個訊息內部。例如

message Foo {
  message Bar { }
}

在這種情況下,編譯器只會將 Bar 產生為巢狀在 Foo 內部的內部類別。

欄位

除了上一節中描述的方法之外,協定緩衝區編譯器還會為 .proto 檔案中訊息內定義的每個欄位產生一組存取器方法。讀取欄位值的方法在訊息類別及其對應的建構器中都有定義;修改值的方法僅在建構器中定義。

請注意,方法名稱始終使用駝峰式命名法,即使 .proto 檔案中的欄位名稱使用帶底線的小寫字母 (應該如此)。大小寫轉換的工作方式如下

  • 對於名稱中的每個底線,底線都會被移除,並且後面的字母會大寫。
  • 如果名稱將附加前綴(例如「get」),則第一個字母會大寫。否則,它會小寫。
  • 方法名稱中每個數字的最後一位數字後面的字母都會大寫。

因此,欄位 foo_bar_baz 變成 fooBarBaz。如果前綴為 get,則會是 getFooBarBaz。而 foo_ba23r_baz 變成 fooBa23RBaz

除了存取器方法之外,編譯器還會為每個欄位產生一個整數常數,其中包含其欄位編號。常數名稱是轉換為大寫的欄位名稱,後跟 _FIELD_NUMBER。例如,給定欄位 optional int32 foo_bar = 5;,編譯器將產生常數 public static final int FOO_BAR_FIELD_NUMBER = 5;

單數欄位 (proto2)

對於這些欄位定義中的任何一個

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

編譯器將在訊息類別及其建構器中產生以下存取器方法

  • boolean hasFoo():如果欄位已設定,則傳回 true
  • int getFoo():傳回欄位的目前值。如果欄位未設定,則傳回預設值。

編譯器將僅在訊息的建構器中產生以下方法

  • Builder setFoo(int value):設定欄位的值。在調用此方法之後,hasFoo() 將傳回 true,而 getFoo() 將傳回 value
  • Builder clearFoo():清除欄位的值。在調用此方法之後,hasFoo() 將傳回 false,而 getFoo() 將傳回預設值。

對於其他簡單欄位類型,會根據 純量值類型表 選擇對應的 Java 類型。對於訊息和列舉類型,值類型會替換為訊息或列舉類別。

嵌入式訊息欄位

對於訊息類型,setFoo() 也接受訊息的建構器類型的實例作為參數。這只是一個快捷方式,相當於在建構器上調用 .build() 並將結果傳遞給該方法。

如果欄位未設定,getFoo() 將傳回一個 Foo 實例,其中未設定任何欄位(可能是 Foo.getDefaultInstance() 傳回的實例)。

此外,編譯器會產生兩個存取器方法,讓您可以存取訊息類型的相關子建構器。以下方法會在訊息類別及其建構器中產生

  • FooOrBuilder getFooOrBuilder():如果欄位的建構器已存在,則傳回該建構器,否則傳回訊息。在建構器上調用此方法不會為欄位建立子建構器。

編譯器僅在訊息的建構器中產生以下方法。

  • Builder getFooBuilder():傳回欄位的建構器。

單數欄位 (proto3)

對於此欄位定義

int32 foo = 1;

編譯器將在訊息類別及其建構器中產生以下存取器方法

  • int getFoo():傳回欄位的目前值。如果欄位未設定,則傳回欄位類型的預設值。

編譯器將僅在訊息的建構器中產生以下方法

  • Builder setFoo(int value):設定欄位的值。在調用此方法之後,getFoo() 將傳回 value
  • Builder clearFoo():清除欄位的值。在調用此方法之後,getFoo() 將傳回欄位類型的預設值。

對於其他簡單欄位類型,會根據 純量值類型表 選擇對應的 Java 類型。對於訊息和列舉類型,值類型會替換為訊息或列舉類別。

嵌入式訊息欄位

對於訊息欄位類型,會在訊息類別及其建構器中產生額外的存取器方法

  • boolean hasFoo():如果欄位已設定,則傳回 true

setFoo() 也接受訊息的建構器類型的實例作為參數。這只是一個快捷方式,相當於在建構器上調用 .build() 並將結果傳遞給該方法。

如果欄位未設定,getFoo() 將傳回一個 Foo 實例,其中未設定任何欄位(可能是 Foo.getDefaultInstance() 傳回的實例)。

此外,編譯器會產生兩個存取器方法,讓您可以存取訊息類型的相關子建構器。以下方法會在訊息類別及其建構器中產生

  • FooOrBuilder getFooOrBuilder():如果欄位的建構器已存在,則傳回該建構器,否則傳回訊息。在建構器上調用此方法不會為欄位建立子建構器。

編譯器僅在訊息的建構器中產生以下方法。

  • Builder getFooBuilder():傳回欄位的建構器。

列舉欄位

對於列舉欄位類型,會在訊息類別及其建構器中產生額外的存取器方法

  • int getFooValue():傳回列舉的整數值。

編譯器將僅在訊息的建構器中產生以下額外方法

  • Builder setFooValue(int value):設定列舉的整數值。

此外,如果列舉值未知,getFoo() 將傳回 UNRECOGNIZED — 這是 proto3 編譯器新增至產生的 列舉類型 的特殊附加值。

重複欄位

對於此欄位定義

repeated string foos = 1;

編譯器將在訊息類別及其建構器中產生以下存取器方法

  • int getFoosCount():傳回欄位中目前元素的數量。
  • String getFoos(int index):傳回給定從零開始的索引處的元素。
  • ProtocolStringList getFoosList():以 ProtocolStringList 形式傳回整個欄位。如果欄位未設定,則傳回空列表。

編譯器將僅在訊息的建構器中產生以下方法

  • Builder setFoos(int index, String value):設定給定從零開始的索引處的元素的值。
  • Builder addFoos(String value):將一個新元素附加到具有給定值的欄位。
  • Builder addAllFoos(Iterable<? extends String> value):將給定 Iterable 中的所有元素附加到欄位。
  • Builder clearFoos():從欄位中移除所有元素。在調用此方法之後,getFoosCount() 將傳回零。

對於其他簡單欄位類型,會根據 純量值類型表 選擇對應的 Java 類型。對於訊息和列舉類型,類型是訊息或列舉類別。

重複嵌入式訊息欄位

對於訊息類型,setFoos()addFoos() 也接受訊息的建構器類型的實例作為參數。這只是一個快捷方式,相當於在建構器上調用 .build() 並將結果傳遞給該方法。還有一個額外產生的方法

  • Builder addFoos(int index, Field value):在給定從零開始的索引處插入一個新元素。將目前位於該位置的元素(如果有的話)以及任何後續元素向右移動(將其索引加一)。

此外,編譯器會在訊息類別及其建構器中產生以下額外存取器方法,以用於訊息類型,讓您可以存取相關的子建構器

  • FooOrBuilder getFoosOrBuilder(int index):如果指定元素的建構器已存在,則傳回該建構器,否則如果不存在,則擲回 IndexOutOfBoundsException。如果從訊息類別調用此方法,它將始終傳回訊息(或擲回例外狀況)而不是建構器。在建構器上調用此方法不會為欄位建立子建構器。
  • List<FooOrBuilder> getFoosOrBuilderList():以不可修改的建構器列表(如果可用)或訊息(如果不可用)形式傳回整個欄位。如果從訊息類別調用此方法,它將始終傳回不可變的訊息列表,而不是不可修改的建構器列表。

編譯器將僅在訊息的建構器中產生以下方法

  • Builder getFoosBuilder(int index):傳回指定索引處元素的建構器,如果索引超出範圍,則擲回 IndexOutOfBoundsException
  • Builder addFoosBuilder(int index):在指定索引處為重複訊息的預設訊息實例插入並傳回建構器。現有的項目會移至較高的索引,以為插入的建構器騰出空間。
  • Builder addFoosBuilder():附加並傳回重複訊息的預設訊息實例的建構器。
  • Builder removeFoos(int index):移除給定從零開始的索引處的元素。
  • List<Builder> getFoosBuilderList():以不可修改的建構器列表形式傳回整個欄位。

重複列舉欄位 (僅限 proto3)

編譯器將在訊息類別及其建構器中產生以下額外方法

  • int getFoosValue(int index):傳回指定索引處的列舉的整數值。
  • List<java.lang.Integer> getFoosValueList():以整數列表形式傳回整個欄位。

編譯器將僅在訊息的建構器中產生以下額外方法

  • Builder setFoosValue(int index, int value):設定指定索引處的列舉的整數值。

名稱衝突

如果另一個非重複欄位的名稱與其中一個重複欄位的產生方法衝突,則兩個欄位名稱都會在其末尾附加 protobuf 欄位編號。

對於這些欄位定義

int32 foos_count = 1;
repeated string foos = 2;

編譯器將首先將它們重新命名為以下名稱

int32 foos_count_1 = 1;
repeated string foos_2 = 2;

存取器方法隨後將如上所述產生。

Oneof 欄位

對於此 oneof 欄位定義

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

choice oneof 中的所有欄位都將使用單個私有欄位來表示其值。此外,協定緩衝區編譯器將為 oneof case 產生 Java 列舉類型,如下所示

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

此列舉類型的值具有以下特殊方法

  • int getNumber():傳回物件的數值,如 .proto 檔案中所定義。
  • static ChoiceCase forNumber(int value):傳回對應於給定數值的列舉物件(或針對其他數值的 null)。

編譯器將在訊息類別及其建構器中產生以下存取器方法

  • boolean hasFooInt():如果 oneof case 為 FOO_INT,則傳回 true
  • int getFooInt():如果 oneof case 為 FOO_INT,則傳回 foo 的目前值。否則,傳回此欄位的預設值。
  • ChoiceCase getChoiceCase():傳回指示設定了哪個欄位的列舉。如果未設定任何欄位,則傳回 CHOICE_NOT_SET

編譯器將僅在訊息的建構器中產生以下方法

  • Builder setFooInt(int value):將 Foo 設定為此值,並將 oneof case 設定為 FOO_INT。在調用此方法之後,hasFooInt() 將傳回 truegetFooInt() 將傳回 value,而 getChoiceCase() 將傳回 FOO_INT
  • Builder clearFooInt():
    • 如果 oneof case 不是 FOO_INT,則不會變更任何內容。
    • 如果 oneof case 為 FOO_INT,則將 Foo 設定為 null,並將 oneof case 設定為 CHOICE_NOT_SET。在調用此方法之後,hasFooInt() 將傳回 falsegetFooInt() 將傳回預設值,而 getChoiceCase() 將傳回 CHOICE_NOT_SET
  • Builder.clearChoice():重設 choice 的值,傳回建構器。

對於其他簡單欄位類型,會根據 純量值類型表 選擇對應的 Java 類型。對於訊息和列舉類型,值類型會替換為訊息或列舉類別。

Map 欄位

對於此 map 欄位定義

map<int32, int32> weight = 1;

編譯器將在訊息類別及其建構器中產生以下存取器方法

  • Map<Integer, Integer> getWeightMap();:傳回不可修改的 Map
  • int getWeightOrDefault(int key, int default);:傳回鍵的值,如果不存在,則傳回預設值。
  • int getWeightOrThrow(int key);:傳回鍵的值,如果不存在,則擲回 IllegalArgumentException。
  • boolean containsWeight(int key);:指示此欄位中是否存在鍵。
  • int getWeightCount();:傳回 map 中的元素數量。

編譯器將僅在訊息的建構器中產生以下方法

  • Builder putWeight(int key, int value);:將權重新增至此欄位。
  • Builder putAllWeight(Map<Integer, Integer> value);:將給定 map 中的所有項目新增至此欄位。
  • Builder removeWeight(int key);:從此欄位中移除權重。
  • Builder clearWeight();:從此欄位中移除所有權重。
  • @Deprecated Map<Integer, Integer> getMutableWeight();:傳回可修改的 Map。請注意,多次調用此方法可能會傳回不同的 map 實例。傳回的 map 參考可能會因後續對建構器的任何方法調用而失效。

訊息值 Map 欄位

對於以訊息類型作為值的 map,編譯器將在訊息的建構器中產生額外的方法

  • Foo.Builder putFooBuilderIfAbsent(int key);:確保 key 存在於映射中,並且如果 Foo.Builder 尚不存在,則插入一個新的 Foo.Builder。對傳回的 Foo.Builder 的變更將反映在最終訊息中。

Any

給定一個 Any 欄位,如下所示

import "google/protobuf/any.proto";

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

在我們產生的程式碼中,details 欄位的 Getter 傳回 com.google.protobuf.Any 的實例。這提供了以下特殊方法來封裝和解封裝 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);

  // 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;
}

列舉

給定一個列舉定義,如下所示

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

協定緩衝區編譯器將產生一個名為 Foo 的 Java 列舉類型,其中包含相同的值集。如果您使用的是 proto3,它還會將特殊值 UNRECOGNIZED 新增至列舉類型。產生的列舉類型的值具有以下特殊方法

  • int getNumber():傳回物件的數值,如 .proto 檔案中所定義。
  • EnumValueDescriptor getValueDescriptor():傳回值的描述符,其中包含有關值的名稱、編號和類型的資訊。
  • EnumDescriptor getDescriptorForType():傳回列舉類型的描述符,其中包含例如有關每個已定義值的資訊。

此外,Foo 列舉類型包含以下靜態方法

  • static Foo forNumber(int value):傳回對應於給定數值的列舉物件。當沒有對應的列舉物件時,傳回 null。
  • static Foo valueOf(int value):傳回對應於給定數值的列舉物件。此方法已棄用,建議使用 forNumber(int value),並將在即將發佈的版本中移除。
  • static Foo valueOf(EnumValueDescriptor descriptor):傳回對應於給定值描述符的列舉物件。可能比 valueOf(int) 更快。在 proto3 中,如果傳遞未知的value descriptor,則傳回 UNRECOGNIZED
  • EnumDescriptor getDescriptor():傳回列舉類型的描述符,其中包含例如有關每個已定義值的資訊。(這與 getDescriptorForType() 的不同之處僅在於它是一個靜態方法。)

還會為每個列舉值產生一個帶有後綴 _VALUE 的整數常數。

請注意,.proto 語言允許讓多個列舉符號具有相同的數值。具有相同數值的符號是同義詞。例如

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

在這種情況下,BAZBAR 的同義詞。在 Java 中,BAZ 將定義為靜態最終欄位,如下所示

static final Foo BAZ = BAR;

因此,BARBAZ 比較相等,並且 BAZ 永遠不應出現在 switch 語句中。編譯器始終選擇使用給定數值定義的第一個符號作為該符號的「標準」版本;所有後續具有相同編號的符號都只是別名。

列舉可以定義為巢狀在訊息類型內。編譯器會產生巢狀在該訊息類型的類別內的 Java 列舉定義。

注意:在產生 Java 程式碼時,protobuf 列舉中的最大值數量可能出乎意料地低 — 在最壞的情況下,最大值略高於 1,700 個值。此限制是由於 Java 位元組碼的每個方法大小限制,並且它在 Java 實作、不同版本的 protobuf 套件以及 .proto 檔案中列舉上設定的任何選項之間有所不同。

擴充功能 (僅限 proto2)

給定一個帶有擴充範圍的訊息

message Foo {
  extensions 100 to 199;
}

協定緩衝區編譯器會使 Foo 擴充 GeneratedMessage.ExtendableMessage 而不是通常的 GeneratedMessage。同樣地,Foo 的建構器也會擴充 GeneratedMessage.ExtendableBuilder。您永遠不應按名稱引用這些基本類型(GeneratedMessage 被視為實作細節)。但是,這些超類別定義了許多額外的方法,您可以使用這些方法來操作擴充功能。

特別是,FooFoo.Builder 將繼承方法 hasExtension()getExtension()getExtensionCount()。此外,Foo.Builder 將繼承方法 setExtension()clearExtension()。這些方法中的每一個都將擴充功能識別碼(如下所述)作為其第一個參數,該識別碼識別擴充功能欄位。其餘參數和傳回值與將為與擴充功能識別碼類型相同的正常(非擴充功能)欄位產生的對應存取器方法完全相同。

給定一個擴充功能定義

extend Foo {
  optional int32 bar = 123;
}

協定緩衝區編譯器會產生一個名為 bar 的「擴充功能識別碼」,您可以將其與 Foo 的擴充功能存取器一起使用來存取此擴充功能,如下所示

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

(擴充功能識別碼的確切實作很複雜,並且涉及泛型的神奇用法 — 但是,您無需擔心擴充功能識別碼如何運作即可使用它們。)

請注意,bar 會宣告為 .proto 檔案的包裝器類別的靜態欄位,如上所述;我們在範例中省略了包裝器類別名稱。

擴充功能可以在另一種類型的範圍內宣告,以為其產生的符號名稱加上前置字串。例如,常見的模式是透過在欄位型別的宣告內部擴充訊息。

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

在此情況下,具有識別碼 foo_ext 和型別 Baz 的擴充功能會在 Baz 的宣告內部宣告,而參照 foo_ext 需要加上 Baz. 前置字串。

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

當剖析可能具有擴充功能的訊息時,您必須提供 ExtensionRegistry,您已在其中註冊任何您想要能夠剖析的擴充功能。否則,這些擴充功能將會被視為不明欄位,而觀察擴充功能的方法行為會如同它們不存在一般。

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;

服務

如果 .proto 檔案包含以下這行程式碼

option java_generic_services = true;

那麼 Protocol Buffer 編譯器將會根據檔案中找到的服務定義產生程式碼,如本節所述。然而,產生的程式碼可能不盡理想,因為它並未與任何特定的 RPC 系統連結,因此比起針對單一系統量身打造的程式碼,需要更多層級的間接性。如果您「不」想要產生此程式碼,請將這行程式碼新增至檔案

option java_generic_services = false;

如果以上兩行程式碼都未提供,則選項預設為 false,因為通用服務已被棄用。(請注意,在 2.4.0 之前的版本中,選項預設為 true)

基於 .proto 語言服務定義的 RPC 系統應提供 外掛程式,以產生適用於該系統的程式碼。這些外掛程式可能需要停用抽象服務,以便它們可以產生自己同名的類別。外掛程式是 2.3.0 版 (2010 年 1 月) 中的新功能。

本節的其餘部分說明當啟用抽象服務時,Protocol Buffer 編譯器會產生什麼。

介面

給定一個服務定義

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

Protocol Buffer 編譯器將產生一個抽象類別 Foo 來表示此服務。Foo 將為服務定義中定義的每個方法提供一個抽象方法。在此情況下,方法 Bar 定義為

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

這些參數等同於 Service.CallMethod() 的參數,但 method 引數是隱含的,而 requestdone 則指定它們的確切型別。

FooService 介面的子類別。Protocol Buffer 編譯器會自動產生 Service 方法的實作,如下所示

  • getDescriptorForType:傳回服務的 ServiceDescriptor
  • callMethod:根據提供的方法描述元判斷要呼叫哪個方法,並直接呼叫它,將請求訊息和回呼向下轉換為正確的型別。
  • getRequestPrototypegetResponsePrototype:傳回指定方法之正確型別的請求或回應的預設執行個體。

也會產生以下靜態方法

  • static ServiceDescriptor getServiceDescriptor():傳回型別的描述元,其中包含有關此服務有哪些方法及其輸入和輸出型別的資訊。

Foo 也將包含一個巢狀介面 Foo.Interface。這是一個純介面,再次包含與您的服務定義中每個方法對應的方法。但是,此介面不會擴充 Service 介面。這是一個問題,因為 RPC 伺服器實作通常是編寫為使用抽象 Service 物件,而不是您的特定服務。為了解決這個問題,如果您有一個物件 impl 實作 Foo.Interface,您可以呼叫 Foo.newReflectiveService(impl) 來建構一個 Foo 的執行個體,該執行個體只會委派給 impl,並實作 Service

總而言之,當實作您自己的服務時,您有兩個選項

  • Foo 子類別化並實作其適當的方法,然後將子類別的執行個體直接交給 RPC 伺服器實作。這通常最容易,但有些人認為它比較不「純粹」。
  • 實作 Foo.Interface 並使用 Foo.newReflectiveService(Foo.Interface) 來建構一個包裝它的 Service,然後將包裝器傳遞給您的 RPC 實作。

Stub

Protocol Buffer 編譯器也會為每個服務介面產生一個「stub」實作,供希望將請求傳送至實作服務的伺服器的用戶端使用。對於 Foo 服務 (如上),stub 實作 Foo.Stub 將定義為巢狀類別。

Foo.StubFoo 的子類別,它也實作了以下方法

  • Foo.Stub(RpcChannel channel):建構一個新的 stub,它會在給定的通道上傳送請求。
  • RpcChannel getChannel():傳回此 stub 的通道,如同傳遞至建構函式一樣。

stub 另外將每個服務的方法實作為通道周圍的包裝器。呼叫其中一個方法只會呼叫 channel.callMethod()

Protocol Buffer 程式庫不包含 RPC 實作。但是,它包含將產生的服務類別連接到您選擇的任何任意 RPC 實作所需的所有工具。您只需要提供 RpcChannelRpcController 的實作即可。

封鎖介面

以上描述的 RPC 類別都具有非封鎖語意:當您呼叫方法時,您會提供一個回呼物件,該物件會在方法完成後叫用。通常,使用封鎖語意編寫程式碼會更容易 (雖然可能擴充性較差),其中方法在完成之前不會傳回。為了適應這一點,Protocol Buffer 編譯器也會產生服務類別的封鎖版本。Foo.BlockingInterface 等同於 Foo.Interface,但每個方法只會傳回結果,而不是呼叫回呼。因此,舉例來說,bar 定義為

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

與非封鎖服務類似,Foo.newReflectiveBlockingService(Foo.BlockingInterface) 會傳回一個 BlockingService,包裝一些 Foo.BlockingInterface。最後,Foo.BlockingStub 會傳回 Foo.BlockingInterface 的 stub 實作,該實作會將請求傳送至特定的 BlockingRpcChannel

外掛程式插入點

程式碼產生器外掛程式 若要擴充 Java 程式碼產生器的輸出,可以使用給定的插入點名稱插入以下型別的程式碼。

  • outer_class_scope:屬於檔案包裝器類別的成員宣告。
  • class_scope:TYPENAME:屬於訊息類別的成員宣告。TYPENAME 是完整的 Proto 名稱,例如 package.MessageType
  • builder_scope:TYPENAME:屬於訊息的建構器類別的成員宣告。TYPENAME 是完整的 Proto 名稱,例如 package.MessageType
  • enum_scope:TYPENAME:屬於列舉類別的成員宣告。TYPENAME 是完整的 Proto 列舉名稱,例如 package.EnumType
  • message_implements:TYPENAME:訊息類別的類別實作宣告。TYPENAME 是完整的 Proto 名稱,例如 package.MessageType
  • builder_implements:TYPENAME:建構器類別的類別實作宣告。TYPENAME 是完整的 Proto 名稱,例如 package.MessageType

產生的程式碼不能包含 import 陳述式,因為這些陳述式很容易與產生的程式碼本身中定義的型別名稱衝突。相反地,當參照外部類別時,您必須始終使用其完整限定名稱。

Java 程式碼產生器中用於判斷輸出檔案名稱的邏輯相當複雜。您可能應該查看 protoc 原始碼,特別是 java_headers.cc,以確保您已涵蓋所有情況。

請勿產生依賴標準程式碼產生器宣告的私有類別成員的程式碼,因為這些實作細節可能會在 Protocol Buffer 的未來版本中變更。

公用程式類別

Protocol Buffer 提供 公用程式類別,用於訊息比較、JSON 轉換以及使用 常見型別 (預先定義的 Protocol Buffer 訊息,用於常見的使用案例)