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=
的參數、套件名稱(將 .
替換為 /
)和 .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 類別檔案。
套件
產生的類別會根據 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
,可用於建構 Foo
。Foo.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()
將傳回true
,getFooInt()
將傳回value
,而getChoiceCase()
將傳回FOO_INT
。Builder clearFooInt()
:- 如果 oneof case 不是
FOO_INT
,則不會變更任何內容。 - 如果 oneof case 為
FOO_INT
,則將Foo
設定為 null,並將 oneof case 設定為CHOICE_NOT_SET
。在調用此方法之後,hasFooInt()
將傳回false
,getFooInt()
將傳回預設值,而getChoiceCase()
將傳回CHOICE_NOT_SET
。
- 如果 oneof case 不是
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;
}
在這種情況下,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;
那麼 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
引數是隱含的,而 request
和 done
則指定它們的確切型別。
Foo
是 Service
介面的子類別。Protocol Buffer 編譯器會自動產生 Service
方法的實作,如下所示
getDescriptorForType
:傳回服務的ServiceDescriptor
。callMethod
:根據提供的方法描述元判斷要呼叫哪個方法,並直接呼叫它,將請求訊息和回呼向下轉換為正確的型別。getRequestPrototype
和getResponsePrototype
:傳回指定方法之正確型別的請求或回應的預設執行個體。
也會產生以下靜態方法
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.Stub
是 Foo
的子類別,它也實作了以下方法
Foo.Stub(RpcChannel channel)
:建構一個新的 stub,它會在給定的通道上傳送請求。RpcChannel getChannel()
:傳回此 stub 的通道,如同傳遞至建構函式一樣。
stub 另外將每個服務的方法實作為通道周圍的包裝器。呼叫其中一個方法只會呼叫 channel.callMethod()
。
Protocol Buffer 程式庫不包含 RPC 實作。但是,它包含將產生的服務類別連接到您選擇的任何任意 RPC 實作所需的所有工具。您只需要提供 RpcChannel
和 RpcController
的實作即可。
封鎖介面
以上描述的 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 訊息,用於常見的使用案例)。