Java 生成程式碼指南

精確描述了 protocol buffer 編譯器為任何給定協議定義生成的 Java 程式碼。

proto2、proto3 和 Editions 生成的程式碼之間的任何差異都會被強調——請注意,這些差異存在於本文件中描述的生成程式碼中,而不是基礎訊息類/介面中,基礎訊息類/介面在所有版本中都是相同的。在閱讀本文件之前,您應該閱讀proto2 語言指南proto3 語言指南和/或Editions 語言指南

請注意,除非另有說明,否則 Java protocol buffer 方法不接受或返回 null。

編譯器呼叫

當使用 --java_out= 命令列標誌呼叫時,protocol buffer 編譯器會生成 Java 輸出。--java_out= 選項的引數是您希望編譯器寫入 Java 輸出的目錄。對於每個 .proto 檔案輸入,編譯器會建立一個包裝器 .java 檔案,其中包含一個 Java 類,該類表示 .proto 檔案本身。

如果 .proto 檔案包含以下行

option java_multiple_files = true;

那麼編譯器還會為它為 .proto 檔案中宣告的每個頂級訊息、列舉和服務生成的每個類/列舉建立單獨的 .java 檔案。

否則(當 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,那麼 protocol buffer 編譯器將生成檔案 build/gen/com/example/FooProtos.java。protocol buffer 編譯器將根據需要自動建立 build/gen/combuild/gen/com/example 目錄。但是,它不會建立 build/genbuild;它們必須已經存在。您可以在單個呼叫中指定多個 .proto 檔案;所有輸出檔案將一次性生成。

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

包(Packages)

生成的類將根據 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 宣告預計不會以反向域名開頭。

訊息

如果您正在設計新的 protocol buffer schema,請參閱Java proto 名稱的建議

給定一個簡單的訊息宣告:

message Foo {}

protocol buffer 編譯器生成一個名為 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() 獲得的內容完全相同(因此所有 singular 欄位都未設定,所有 repeated 欄位都為空)。請注意,訊息的預設例項可以透過呼叫其 newBuilderForType() 方法作為工廠使用。
  • static Descriptor getDescriptor(): 返回型別的描述符。這包含有關型別的資訊,包括它有哪些欄位以及它們的型別。這可以與 Message 的反射方法(如 getField())一起使用。
  • static Foo parseFrom(...): 從給定源解析 Foo 型別的訊息並返回。每個 Message.Builder 介面中 mergeFrom() 的變體都有一個對應的 parseFrom 方法。請注意,parseFrom() 從不丟擲 UninitializedMessageException;如果解析的訊息缺少 required 欄位,它會丟擲 InvalidProtocolBufferException。這使其與呼叫 Foo.newBuilder().mergeFrom(...).build() 略有不同。
  • static Parser parser(): 返回 Parser 的例項,該例項實現各種 parseFrom() 方法。
  • Foo.Builder newBuilder(): 建立一個新的構建器(如下所述)。
  • Foo.Builder newBuilder(Foo prototype): 建立一個新的構建器,所有欄位都初始化為 prototype 中它們所擁有的相同值。由於嵌入式訊息和字串物件是不可變的,因此它們在原始物件和副本之間共享。

構建器

訊息物件——例如上面描述的 Foo 類的例項——是不可變的,就像 Java String 一樣。要構造訊息物件,您需要使用*構建器*。每個訊息類都有自己的構建器類——因此在我們的 Foo 示例中,protocol buffer 編譯器生成了一個巢狀類 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 {
  int32 val = 1;
  // some other fields.
}

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

message Baz {
  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 中巢狀的內部類生成。

欄位

除了上一節中描述的方法之外,protocol buffer 編譯器還會為 .proto 檔案中訊息內定義的每個欄位生成一組訪問器方法。讀取欄位值的方法在訊息類及其相應的構建器中都定義;修改值的方法只在構建器中定義。

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

  • 對於名稱中的每個下劃線,下劃線被移除,其後的字母大寫。
  • 如果名稱將帶有字首(例如“get”),則首字母大寫。否則,小寫。
  • 方法名稱中每個數字後面的字母都會大寫。

因此,欄位 foo_bar_baz 變為 fooBarBaz。如果加上 get 字首,它將是 getFooBarBaz。而 foo_ba23r_baz 變為 fooBa23RBaz

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

以下各節分為顯式存在和隱式存在。Proto2 具有顯式存在,proto3 預設隱式存在。Editions 預設顯式存在,但您可以使用features.field_presence覆蓋它。

具有顯式存在性的單一欄位

對於任何這些欄位定義

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

編譯器將在訊息類及其構建器中生成以下訪問器方法

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

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

  • Builder setFoo(int value): 設定欄位的值。呼叫此方法後,hasFoo() 將返回 truegetFoo() 將返回 value
  • Builder clearFoo(): 清除欄位的值。呼叫此方法後,hasFoo() 將返回 falsegetFoo() 將返回預設值。

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

內嵌訊息欄位

對於訊息型別,setFoo() 也接受訊息構建器型別的例項作為引數。這只是一個快捷方式,相當於在構建器上呼叫 .build() 並將結果傳遞給該方法。進一步修改傳遞給 setFoo 的子構建器將**不會**反映在訊息類的構建器中。訊息類的構建器“擁有”子訊息。

如果欄位未設定,getFoo() 將返回一個所有欄位均未設定的 Foo 例項(可能是 Foo.getDefaultInstance() 返回的例項)。

此外,編譯器生成了兩個訪問器方法,允許您訪問訊息型別的相關子構建器。以下方法在訊息類及其構建器中都生成

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

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

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

具有隱式存在性的單一欄位

對於此欄位定義:

int32 foo = 1;

編譯器將在訊息類及其構建器中生成以下訪問器方法

  • int getFoo(): 返回欄位的當前值。如果欄位未設定,則返回該欄位型別的預設值。

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

  • Builder setFoo(int value): 設定欄位的值。呼叫此方法後,getFoo() 將返回 value
  • Builder clearFoo(): 清除欄位的值。呼叫此方法後,getFoo() 將返回該欄位型別的預設值。

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

列舉欄位

對於列舉欄位型別,訊息類及其構建器中會生成一個額外的訪問器方法

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

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

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

此外,如果列舉值為未知——這是 proto3 編譯器新增到生成的列舉型別中的特殊附加值,則 getFoo() 將返回 UNRECOGNIZED

重複欄位

對於此欄位定義:

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 中的所有欄位都將使用一個私有欄位作為其值。此外,protocol buffer 編譯器將為 oneof 情況生成一個 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() 將返回 valuegetChoiceCase() 將返回 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<int32, int32> weight = 1;

編譯器將在訊息類及其構建器中生成以下訪問器方法

  • Map<Integer, Integer> getWeightMap();: 返回一個不可修改的 Map
  • int getWeightOrDefault(int key, int default);: 返回 key 的值,如果不存在則返回預設值。
  • int getWeightOrThrow(int key);: 返回 key 的值,如果不存在則丟擲 IllegalArgumentException。
  • boolean containsWeight(int key);: 指示此欄位中是否存在 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 引用可能會因後續對 Builder 的任何方法呼叫而失效。

訊息值 Map 欄位

對於值型別為訊息型別的 Map,編譯器將在訊息的構建器中生成一個額外的方法

  • Foo.Builder putFooBuilderIfAbsent(int key);: 確保對映中存在 key,如果不存在則插入一個新的 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);

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

列舉

給定一個列舉定義,例如:

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

protocol buffer 編譯器將生成一個名為 Foo 的 Java 列舉型別,其中包含相同的值集。如果您使用 proto3,它還會向列舉型別新增特殊值 UNRECOGNIZED。在 Editions 中,OPEN 列舉也有 UNRECOGNIZED 值,而 CLOSED 列舉則沒有。生成的列舉型別的值具有以下特殊方法

  • 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 和 OPEN 列舉中,如果傳遞未知值描述符,則返回 UNRECOGNIZED
  • EnumDescriptor getDescriptor(): 返回列舉型別的描述符,其中包含例如每個定義值的資訊。(這與 getDescriptorForType() 的區別僅在於它是一個靜態方法。)

每個列舉值還會生成一個帶字尾 _VALUE 的整數常量。

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

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

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

static final Foo BAZ = BAR;

因此,BARBAZ 比較相等,並且 BAZ 不應出現在 switch 語句中。編譯器總是選擇第一個具有給定數字值的符號作為該符號的“規範”版本;所有後續具有相同數字的符號都只是別名。

列舉可以在訊息型別內巢狀定義。編譯器將在該訊息型別的類中巢狀生成 Java 列舉定義。

注意:生成 Java 程式碼時,protobuf 列舉的最大值數量可能出奇的低——在最壞的情況下,最大值略高於 1,700 個。這個限制是由於 Java 位元組碼的每方法大小限制,並且它因 Java 實現、protobuf 套件的不同版本以及 .proto 檔案中列舉設定的任何選項而異。

擴充套件

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

edition = "2023";

message Foo {
  extensions 100 to 199;
}

protocol buffer 編譯器將使 Foo 擴充套件 GeneratedMessage.ExtendableMessage 而不是通常的 GeneratedMessage。同樣,Foo 的構建器將擴充套件 GeneratedMessage.ExtendableBuilder。您不應按名稱引用這些基型別(GeneratedMessage 被視為實現細節)。但是,這些超類定義了許多您可以用來操作擴充套件的額外方法。

特別是 FooFoo.Builder 將繼承方法 hasExtension()getExtension()getExtensionCount()。此外,Foo.Builder 將繼承方法 setExtension()clearExtension()。每個這些方法都將其第一個引數作為擴充套件識別符號(如下所述),該識別符號標識一個擴充套件欄位。其餘引數和返回值與為具有與擴充套件識別符號相同型別的正常(非擴充套件)欄位生成的相應訪問器方法完全相同。

給定一個擴充套件定義:

edition = "2023";

import "foo.proto";

extend Foo {
  int32 bar = 123;
}

protocol buffer 編譯器生成一個名為 bar 的“擴充套件識別符號”,您可以將其與 Foo 的擴充套件訪問器一起使用以訪問此擴充套件,如下所示

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

(擴充套件識別符號的確切實現很複雜,並且涉及泛型的神奇用法——但是,您無需擔心擴充套件識別符號的工作原理即可使用它們。)

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

擴充套件可以在另一個型別的範圍內宣告,以作為其生成的符號名稱的字首。例如,一種常見的模式是透過在欄位型別宣告*內部*的欄位來擴充套件訊息

edition = "2023";

import "foo.proto";

message Baz {
  extend Foo {
    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;

服務(Services)

如果 .proto 檔案包含以下行:

option java_generic_services = true;

然後 protocol buffer 編譯器將根據檔案中找到的服務定義生成程式碼,如本節所述。但是,生成的程式碼可能不理想,因為它不與任何特定的 RPC 系統繫結,因此比針對一個系統定製的程式碼需要更多的間接層。如果您不希望生成此程式碼,請將此行新增到檔案中

option java_generic_services = false;

如果上述兩行都未給出,該選項預設為 false,因為通用服務已被棄用。(請注意,在 2.4.0 之前,該選項預設為 true

基於 .proto 語言服務定義的 RPC 系統應提供外掛來生成適合該系統的程式碼。這些外掛很可能要求停用抽象服務,以便它們可以生成自己同名的類。

本節的其餘部分描述了當啟用抽象服務時,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 物件編寫的,而不是您的特定服務。為了解決這個問題,如果您有一個實現 Foo.Interface 的物件 impl,您可以呼叫 Foo.newReflectiveService(impl) 來構造一個 Foo 例項,該例項只是委託給 impl,並實現了 Service

總結一下,當實現您自己的服務時,您有兩種選擇

  • 子類化 Foo 並適當地實現其方法,然後將您的子類例項直接傳遞給 RPC 伺服器實現。這通常是最簡單的,但有些人認為它“不夠純粹”。
  • 實現 Foo.Interface 並使用 Foo.newReflectiveService(Foo.Interface) 構造一個包裝它的 Service 例項,然後將包裝器傳遞給您的 RPC 實現。

存根

protocol buffer 編譯器還會生成每個服務介面的“存根”實現,客戶端使用它向實現該服務的伺服器傳送請求。對於 Foo 服務(上面),存根實現 Foo.Stub 將定義為巢狀類。

Foo.StubFoo 的子類,它還實現了以下方法

  • Foo.Stub(RpcChannel channel): 構造一個新的存根,它透過給定通道傳送請求。
  • RpcChannel getChannel(): 返回此存根的通道,如傳遞給建構函式。

存根還實現了服務的每個方法,作為通道的包裝器。呼叫其中一個方法只是呼叫 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) 返回一個包裝某些 Foo.BlockingInterfaceBlockingService。最後,Foo.BlockingStub 返回一個 Foo.BlockingInterface 的存根實現,該實現向特定的 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 Buffers 中可能會改變。

工具類

Protocol buffer 提供了工具類用於訊息比較、JSON 轉換以及處理眾所周知型別(用於常見用例的預定義 protocol buffer 訊息)