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
的類別。
注意:如果您使用的是已棄用的 protobuf API v1,則無論與訊息名稱是否有任何衝突,都會新增 OuterClass
。
除了任何巢狀類別外,包裝器類別本身將具有以下 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=
的參數、套件名稱(其中 .
s 替換為 /
s)和 .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/com
和 build/gen/com/example
目錄。但是,它不會建立 build/gen
或 build
;它們必須已經存在。您可以在單次叫用中指定多個 .proto
檔案;所有輸出檔案將一次產生。
在輸出 Java 程式碼時,協定緩衝區編譯器直接輸出到 JAR 封存的能力特別方便,因為許多 Java 工具可以直接從 JAR 檔案讀取原始碼。若要輸出到 JAR 檔案,只需提供以 .jar
結尾的輸出位置。請注意,只有 Java 原始碼會放置在封存中;您仍然必須單獨編譯它以產生 Java 類別檔案。
例如,如果 .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
宣告預期不會以反向網域名稱開頭。
訊息
給定一個簡單的訊息宣告
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()
方法用作工廠。static Descriptor getDescriptor()
:傳回類型的描述子。它包含有關該類型的資訊,包括它有哪些欄位以及它們的類型。這可以與Message
的反射方法(例如getField()
)搭配使用。static Foo parseFrom(...)
:從給定的來源剖析Foo
類型的訊息並傳回它。parseFrom
方法與Message.Builder
介面中的每個mergeFrom()
變體都對應一個。請注意,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
,可用於建構 Foo
。Foo.Builder
會實作 Message.Builder
介面。它會擴充 GeneratedMessage.Builder
類別,但同樣地,這應該被視為實作細節。與 Foo
類似,Foo.Builder
可能會依賴 GeneratedMessage.Builder
中的一般方法實作,或者在使用 optimize_for
選項時,產生速度快得多的自訂程式碼。您可以使用靜態方法 Foo.newBuilder()
取得 Foo.Builder
。
Foo.Builder
沒有定義任何靜態方法。它的介面與 Message.Builder
介面定義完全相同,但傳回類型更具體:修改 builder 的 Foo.Builder
方法傳回類型為 Foo.Builder
,而 build()
傳回類型為 Foo
。
修改 builder 內容的方法(包括欄位設定器)總是傳回對 builder 的參考 (也就是說,它們會「return this;
」)。這允許在同一行中將多個方法呼叫鏈接在一起。例如:builder.mergeFrom(obj).setFoo(1).setBar("abc").clearBaz();
請注意,builder 不是執行緒安全的,因此當多個不同執行緒需要修改單一 builder 的內容時,應使用 Java 同步處理。
子建構器
對於包含子訊息的訊息,編譯器也會產生子 builder。這允許您重複修改深度巢狀的子訊息,而無需重新建構它們。例如
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
內部的內部類別。
欄位
除了上一節中描述的方法外,protocol buffer 編譯器還會為 .proto
檔案中定義的每個欄位產生一組存取方法。讀取欄位值的方法同時定義在訊息類別及其對應的 builder 中;修改值的方法僅定義在 builder 中。
請注意,方法名稱總是使用駝峰式命名法,即使 .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;
編譯器會在訊息類別及其 builder 中產生以下存取方法:
boolean hasFoo()
:如果已設定該欄位,則傳回true
。int getFoo()
:傳回該欄位的目前值。如果未設定該欄位,則傳回預設值。
編譯器只會在訊息的 builder 中產生以下方法:
Builder setFoo(int value)
:設定該欄位的值。在呼叫此方法後,hasFoo()
會傳回true
,而getFoo()
會傳回value
。Builder clearFoo()
:清除該欄位的值。在呼叫此方法後,hasFoo()
會傳回false
,而getFoo()
會傳回預設值。
對於其他簡單欄位類型,會根據純量值類型表選擇對應的 Java 類型。對於訊息和列舉類型,值類型會被訊息或列舉類別取代。
嵌入式訊息欄位
對於訊息類型,setFoo()
也會接受訊息的 builder 類型的實例作為參數。這只是一個快捷方式,相當於對 builder 呼叫 .build()
並將結果傳遞給該方法。
如果未設定該欄位,getFoo()
會傳回一個沒有任何欄位設定的 Foo 實例(可能是由 Foo.getDefaultInstance()
傳回的實例)。
此外,編譯器還會產生兩個存取方法,讓您可以存取訊息類型的相關子 builder。以下方法同時在訊息類別及其 builder 中產生:
FooOrBuilder getFooOrBuilder()
:傳回該欄位的 builder(如果已經存在),否則傳回訊息。在 builder 上呼叫此方法不會為該欄位建立子 builder。
編譯器只會在訊息的 builder 中產生以下方法。
Builder getFooBuilder()
:傳回該欄位的 builder。
單數欄位 (proto3)
對於這個欄位定義:
int32 foo = 1;
編譯器會在訊息類別及其 builder 中產生以下存取方法:
int getFoo()
:傳回該欄位的目前值。如果未設定該欄位,則傳回該欄位類型的預設值。
編譯器只會在訊息的 builder 中產生以下方法:
Builder setFoo(int value)
:設定該欄位的值。在呼叫此方法後,getFoo()
會傳回value
。Builder clearFoo()
:清除該欄位的值。在呼叫此方法後,getFoo()
會傳回該欄位類型的預設值。
對於其他簡單欄位類型,會根據純量值類型表選擇對應的 Java 類型。對於訊息和列舉類型,值類型會被訊息或列舉類別取代。
嵌入式訊息欄位
對於訊息欄位類型,訊息類別及其 builder 中會產生額外的存取方法:
boolean hasFoo()
:如果已設定該欄位,則傳回true
。
setFoo()
也會接受訊息的 builder 類型的實例作為參數。這只是一個快捷方式,相當於對 builder 呼叫 .build()
並將結果傳遞給該方法。
如果未設定該欄位,getFoo()
會傳回一個沒有任何欄位設定的 Foo 實例(可能是由 Foo.getDefaultInstance()
傳回的實例)。
此外,編譯器還會產生兩個存取方法,讓您可以存取訊息類型的相關子 builder。以下方法同時在訊息類別及其 builder 中產生:
FooOrBuilder getFooOrBuilder()
:傳回該欄位的 builder(如果已經存在),否則傳回訊息。在 builder 上呼叫此方法不會為該欄位建立子 builder。
編譯器只會在訊息的 builder 中產生以下方法。
Builder getFooBuilder()
:傳回該欄位的 builder。
列舉欄位
對於列舉欄位類型,訊息類別及其 builder 中會產生額外的存取方法:
int getFooValue()
:傳回列舉的整數值。
編譯器只會在訊息的 builder 中產生以下額外方法:
Builder setFooValue(int value)
:設定列舉的整數值。
此外,如果列舉值未知,getFoo()
會傳回 UNRECOGNIZED
— 這是 proto3 編譯器新增到產生的列舉類型的特殊額外值。
重複欄位
對於這個欄位定義:
repeated string foos = 1;
編譯器會在訊息類別及其 builder 中產生以下存取方法:
int getFoosCount()
:傳回欄位中目前元素的數量。String getFoos(int index)
:傳回給定從零開始的索引處的元素。ProtocolStringList getFoosList()
:將整個欄位傳回為ProtocolStringList
。如果未設定該欄位,則傳回空清單。
編譯器只會在訊息的 builder 中產生以下方法:
Builder setFoos(int index, String value)
:設定給定從零開始的索引處的元素的值。Builder addFoos(String value)
:將具有給定值的新元素附加到欄位。Builder addAllFoos(Iterable<? extends String> value)
:將給定Iterable
中的所有元素附加到欄位。Builder clearFoos()
:從欄位中移除所有元素。在呼叫此方法後,getFoosCount()
會傳回零。
對於其他簡單欄位類型,會根據純量值類型表選擇對應的 Java 類型。對於訊息和列舉類型,類型是訊息或列舉類別。
重複嵌入式訊息欄位
對於訊息類型,setFoos()
和 addFoos()
也會接受訊息的 builder 類型的實例作為參數。這只是一個快捷方式,相當於對 builder 呼叫 .build()
並將結果傳遞給該方法。還有一個額外產生的方法:
Builder addFoos(int index, Field value)
:在給定從零開始的索引處插入新元素。將目前位於該位置的元素(如果有的話)和任何後續元素向右移動(將其索引加一)。
此外,編譯器會在訊息類別及其 builder 中為訊息類型產生以下額外存取方法,讓您可以存取相關的子 builder:
FooOrBuilder getFoosOrBuilder(int index)
:傳回指定元素的 builder(如果已經存在),否則如果不存在則會擲回IndexOutOfBoundsException
。如果從訊息類別呼叫此方法,則它總是會傳回訊息(或擲回例外狀況),而不是 builder。在 builder 上呼叫此方法不會為該欄位建立子 builder。List<FooOrBuilder> getFoosOrBuilderList()
:將整個欄位傳回為不可修改的 builder 清單(如果可用),否則傳回訊息。如果從訊息類別呼叫此方法,則它總是會傳回訊息的不可變清單,而不是 builder 的不可修改清單。
編譯器只會在訊息的 builder 中產生以下方法:
Builder getFoosBuilder(int index)
:傳回指定索引處元素的 builder,如果索引超出範圍,則會擲回IndexOutOfBoundsException
。Builder addFoosBuilder(int index)
:插入並傳回重複訊息的預設訊息實例的 builder,並在指定的索引處。現有的項目會移到較高的索引,以便為插入的 builder 騰出空間。Builder addFoosBuilder()
:附加並傳回重複訊息的預設訊息實例的 builder。Builder removeFoos(int index)
:移除給定從零開始的索引處的元素。List<Builder> getFoosBuilderList()
:將整個欄位傳回為不可修改的 builder 清單。
重複列舉欄位 (僅限 proto3)
編譯器會在訊息類別及其 builder 中產生以下額外方法:
int getFoosValue(int index)
:傳回指定索引處列舉的整數值。List<java.lang.Integer> getFoosValueList()
:將整個欄位傳回為整數清單。
編譯器只會在訊息的 builder 中產生以下額外方法:
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
)。
編譯器會在訊息類別及其 builder 中產生以下存取方法:
boolean hasFooInt()
:如果 oneof 案例為FOO
,則傳回true
。int getFooInt()
:如果 oneof 案例為FOO
,則傳回foo
的目前值。否則,傳回此欄位的預設值。ChoiceCase getChoiceCase()
:傳回指出已設定哪個欄位的列舉。如果沒有設定任何欄位,則傳回CHOICE_NOT_SET
。
編譯器只會在訊息的 builder 中產生以下方法:
Builder setFooInt(int value)
:將Foo
設定為此值,並將 oneof 案例設定為FOO
。在呼叫此方法後,hasFooInt()
會傳回true
,getFooInt()
會傳回value
,而getChoiceCase()
會傳回FOO
。Builder clearFooInt()
:- 如果 oneof 案例不是
FOO
,則不會進行任何變更。 - 如果 oneof 案例為
FOO
,則將Foo
設定為 null,並將 oneof 案例設定為FOO_NOT_SET
。在呼叫此方法後,hasFooInt()
會傳回false
,getFooInt()
會傳回預設值,而getChoiceCase()
會傳回FOO_NOT_SET
。
- 如果 oneof 案例不是
Builder.clearChoice()
:重設choice
的值,並傳回 builder。
對於其他簡單欄位類型,會根據純量值類型表選擇對應的 Java 類型。對於訊息和列舉類型,值類型會被訊息或列舉類別取代。
Map 欄位
對於這個 map 欄位定義:
map<int32, int32> weight = 1;
編譯器會在訊息類別及其 builder 中產生以下存取方法:
Map<Integer, Integer> getWeightMap();
:傳回不可修改的Map
。int getWeightOrDefault(int key, int default);
:傳回 key 的值,如果不存在則傳回預設值。int getWeightOrThrow(int key);
: 傳回指定鍵 (key) 的值,如果該鍵不存在則拋出 IllegalArgumentException 例外。boolean containsWeight(int key);
: 指示此欄位中是否存在指定的鍵。int getWeightCount();
: 傳回此映射 (map) 中的元素數量。
編譯器只會在訊息的 builder 中產生以下方法:
Builder putWeight(int key, int value);
: 將權重 (weight) 新增至此欄位。Builder putAllWeight(Map<Integer, Integer> value);
: 將給定映射中的所有條目新增至此欄位。Builder removeWeight(int key);
: 從此欄位中移除權重。Builder clearWeight();
: 從此欄位中移除所有權重。@Deprecated Map<Integer, Integer> getMutableWeight();
: 傳回一個可變的Map
。請注意,多次呼叫此方法可能會傳回不同的映射實例。傳回的映射參考可能會因後續對 Builder 的方法呼叫而失效。
訊息值映射欄位
對於以訊息類型作為值的映射,編譯器會在訊息的建構器中產生一個額外的方法
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);
// 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 中,如果傳入未知的值描述符,則傳回UNRECOGNIZED
。EnumDescriptor getDescriptor()
: 傳回列舉類型的描述符,其中包含例如每個已定義值的相關資訊。(這與getDescriptorForType()
的不同之處僅在於它是靜態方法。)
也會為每個列舉值產生一個帶有後綴 _VALUE 的整數常數。
請注意,.proto
語言允許多個列舉符號具有相同的數值。具有相同數值的符號是同義詞。例如
enum Foo {
BAR = 0;
BAZ = 0;
}
在此情況下,BAZ
是 BAR
的同義詞。在 Java 中,BAZ
將被定義為靜態最終欄位,如下所示
static final Foo BAZ = BAR;
因此,BAR
和 BAZ
比較相等,而 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
被視為實作細節)。但是,這些超類別定義了許多額外的方法,您可以使用這些方法來操作擴充。
特別是,Foo
和 Foo.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;
然後,協議緩衝區編譯器將根據本節中描述的檔案中找到的服務定義產生程式碼。但是,產生的程式碼可能不太理想,因為它未綁定到任何特定的 RPC 系統,因此需要比針對一個系統客製化的程式碼更多的間接層級。如果您不希望產生此程式碼,請將此行新增至檔案
option java_generic_services = false;
如果未給出上述任何一行,則該選項預設為 false
,因為一般服務已被棄用。(請注意,在 2.4.0 之前,該選項預設為 true
)
基於 .proto
語言服務定義的 RPC 系統應提供 外掛程式,以產生適用於該系統的程式碼。這些外掛程式很可能需要停用抽象服務,以便它們可以產生其自己具有相同名稱的類別。外掛程式是 2.3.0 版(2010 年 1 月)中的新增功能。
本節的其餘部分說明在啟用抽象服務時,協議緩衝區編譯器產生什麼內容。
介面
給定一個服務定義
service Foo {
rpc Bar(FooRequest) returns(FooResponse);
}
協議緩衝區編譯器將產生一個抽象類別 Foo
來表示此服務。Foo
將為服務定義中定義的每個方法都有一個抽象方法。在此情況下,方法 Bar
定義為
abstract void bar(RpcController controller, FooRequest request,
RpcCallback<FooResponse> done);
參數等效於 Service.CallMethod()
的參數,但 method
引數是隱含的,而 request
和 done
指定了它們的確切類型。
Foo
子類別 Service
介面。協議緩衝區編譯器會自動產生 Service
方法的實作,如下所示
getDescriptorForType
: 傳回服務的ServiceDescriptor
。callMethod
: 根據提供的方法描述符確定正在呼叫哪個方法,並直接呼叫它,將請求訊息和回呼向下轉換為正確的類型。getRequestPrototype
和getResponsePrototype
: 傳回指定方法的正確類型的請求或回應的預設實例。
還會產生以下靜態方法
static ServiceDescriptor getServiceDescriptor()
: 傳回類型的描述符,其中包含有關此服務具有哪些方法及其輸入和輸出類型的資訊。
Foo
還將包含一個巢狀介面 Foo.Interface
。這是一個純介面,它再次包含與服務定義中每個方法相對應的方法。但是,此介面不會擴充 Service
介面。這是一個問題,因為 RPC 伺服器實作通常會寫入以使用抽象 Service
物件,而不是您的特定服務。為了解決這個問題,如果您有一個實作 Foo.Interface
的物件 impl
,您可以呼叫 Foo.newReflectiveService(impl)
來建構一個簡單地委派給 impl
並實作 Service
的 Foo
實例。
總而言之,在實作您自己的服務時,您有兩個選項
- 子類別
Foo
並根據需要實作其方法,然後將您的子類別的實例直接交給 RPC 伺服器實作。這通常是最簡單的,但有些人認為它不太「純」。 - 實作
Foo.Interface
並使用Foo.newReflectiveService(Foo.Interface)
來建構一個包裝它的Service
,然後將包裝器傳遞給您的 RPC 實作。
Stub
協議緩衝區編譯器還會產生每個服務介面的「存根」實作,客戶端可以使用它來將請求傳送至實作該服務的伺服器。對於 Foo
服務(如上所述),存根實作 Foo.Stub
將被定義為巢狀類別。
Foo.Stub
是 Foo
的子類別,它也實作以下方法
Foo.Stub(RpcChannel channel)
: 建構一個新的存根,該存根在給定的通道上傳送請求。RpcChannel getChannel()
: 傳回此存根的通道,如同傳遞至建構函式一樣。
存根另外將每個服務的方法實作為通道的包裝器。呼叫其中一個方法只是呼叫 channel.callMethod()
。
協議緩衝區程式庫不包含 RPC 實作。但是,它包含將產生的服務類別連接到您選擇的任何任意 RPC 實作所需的所有工具。您只需要提供 RpcChannel
和 RpcController
的實作即可。
阻擋介面
上面描述的 RPC 類別都具有非阻塞語意:當您呼叫一個方法時,您會提供一個回呼物件,該物件將在該方法完成後被叫用。通常,使用阻塞語意來編寫程式碼更容易(但可能較不具可擴展性),其中方法在完成之前不會傳回。為了容納這一點,協議緩衝區編譯器還會產生服務類別的阻塞版本。Foo.BlockingInterface
等效於 Foo.Interface
,只是每個方法只是傳回結果而不是呼叫回呼。因此,例如,bar
定義為
abstract FooResponse bar(RpcController controller, FooRequest request)
throws ServiceException;
與非阻塞服務類似,Foo.newReflectiveBlockingService(Foo.BlockingInterface)
會傳回一個包裝某些 Foo.BlockingInterface
的 BlockingService
。最後,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 訊息)。