プロトコルバッファの基本: Java
このチュートリアルでは、プロトコルバッファを扱うための基本的なJavaプログラマー向け入門を提供します。簡単なサンプルアプリケーションを作成する手順を通して、以下の方法を示します。
.proto
ファイルでメッセージ形式を定義する。- protocol buffer コンパイラを使用する。
- JavaプロトコルバッファAPIを使用してメッセージを書き込み、読み込む。
これは、Javaでプロトコルバッファを使用するための包括的なガイドではありません。より詳細なリファレンス情報については、プロトコルバッファ言語ガイド (proto2)、プロトコルバッファ言語ガイド (proto3)、Java APIリファレンス、Java生成コードガイド、およびエンコーディングリファレンスを参照してください。
問題領域
ここで使用する例は、人々の連絡先の詳細をファイルに読み書きできる、非常にシンプルな「アドレス帳」アプリケーションです。アドレス帳の各人物には、名前、ID、メールアドレス、連絡先の電話番号があります。
このような構造化データをどのようにシリアライズし、取得するのでしょうか? この問題を解決するにはいくつかの方法があります。
- Javaシリアライゼーションを使用する。これは言語に組み込まれているためデフォルトのアプローチですが、よく知られた問題が多数あります(Josh Bloch著「Effective Java」213ページを参照)。また、C++やPythonで書かれたアプリケーションとデータを共有する必要がある場合にも、あまりうまく機能しません。
- 「12:3:-23:67」のように4つの整数をエンコードするなど、データ項目を単一の文字列にエンコードするアドホックな方法を考案することもできます。これはシンプルで柔軟なアプローチですが、一度限りのエンコード/パースコードを記述する必要があり、パースにはわずかな実行時コストがかかります。これは、非常に単純なデータをエンコードする場合に最適です。
- データをXMLにシリアライズする。XMLは(ある意味)人間が読める形式であり、多くの言語でバインディングライブラリが存在するため、このアプローチは非常に魅力的です。他のアプリケーション/プロジェクトとデータを共有したい場合には良い選択肢となります。ただし、XMLは膨大なスペースを消費することで悪名高く、エンコード/デコードにはアプリケーションに多大なパフォーマンスペナルティを課す可能性があります。また、XML DOMツリーのナビゲートは、通常クラス内の単純なフィールドをナビゲートするよりもかなり複雑です。
これらのオプションの代わりに、プロトコルバッファを使用できます。プロトコルバッファは、まさにこの問題を解決するための柔軟で効率的な自動化ソリューションです。プロトコルバッファを使用すると、保存したいデータ構造の` .proto `記述を記述します。それから、プロトコルバッファコンパイラは、効率的なバイナリ形式でプロトコルバッファデータの自動エンコードと解析を実装するクラスを作成します。生成されたクラスは、プロトコルバッファを構成するフィールドのゲッターとセッターを提供し、プロトコルバッファを単位として読み書きする詳細を処理します。重要なことに、プロトコルバッファ形式は、コードが古い形式でエンコードされたデータを読み取ることができるように、時間をかけて形式を拡張するという考え方をサポートしています。
サンプルコードの場所
サンプルコードは、ソースコードパッケージの「examples」ディレクトリに含まれています。こちらからダウンロードできます。
プロトコルフォーマットの定義
アドレス帳アプリケーションを作成するには、まず` .proto `ファイルから始める必要があります。` .proto `ファイルの定義はシンプルです。シリアライズしたい各データ構造に対して_メッセージ_を追加し、そのメッセージ内の各フィールドに名前と型を指定します。以下は、メッセージ` addressbook.proto `を定義する` .proto `ファイルです。
syntax = "proto2";
package tutorial;
option java_multiple_files = true;
option java_package = "com.example.tutorial.protos";
option java_outer_classname = "AddressBookProtos";
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = PHONE_TYPE_HOME];
}
repeated PhoneNumber phones = 4;
}
message AddressBook {
repeated Person people = 1;
}
ご覧のとおり、構文は C++ や Java に似ています。ファイルの各部分を見て、それが何をするのかを見ていきましょう。
`.proto`ファイルはパッケージ宣言から始まります。これは、異なるプロジェクト間の名前の衝突を防ぐのに役立ちます。Javaでは、ここで`java_package`を明示的に指定していない限り、パッケージ名がJavaパッケージとして使用されます。`java_package`を提供している場合でも、Protocol Buffersの名前空間や非Java言語での名前の衝突を避けるために、通常の`package`も定義する必要があります。
パッケージ宣言の後には、Java固有の3つのオプション`java_multiple_files`、`java_package`、および`java_outer_classname`が表示されます。`java_package`は、生成されたクラスが配置されるJavaパッケージ名を指定します。これを明示的に指定しない場合、単に`package`宣言で指定されたパッケージ名と一致しますが、これらの名前は通常、適切なJavaパッケージ名ではありません(通常、ドメイン名で始まらないため)。`java_outer_classname`オプションは、このファイルを表すラッパークラスのクラス名を定義します。`java_outer_classname`を明示的に指定しない場合、ファイル名をアッパーキャメルケースに変換して生成されます。たとえば、「my_proto.proto」は、デフォルトで「MyProto」をラッパークラス名として使用します。`java_multiple_files = true`オプションは、生成された各クラスに対して個別の`.java`ファイルを生成することを有効にします(ラッパークラス用に単一の`.java`ファイルを生成し、ラッパークラスをアウタークラスとして使用し、他のすべてのクラスをラッパークラス内にネストする従来の動作とは異なります)。
次に、メッセージ定義があります。メッセージは、型付きフィールドのセットを含む単なる集約です。`bool`、`int32`、`float`、`double`、`string`など、多くの標準的な単純なデータ型がフィールドタイプとして利用可能です。他のメッセージタイプをフィールドタイプとして使用することで、メッセージにさらに構造を追加することもできます。上記の例では、`Person`メッセージは`PhoneNumber`メッセージを含み、`AddressBook`メッセージは`Person`メッセージを含んでいます。他のメッセージ内にメッセージタイプをネストすることもできます。ご覧のとおり、`PhoneNumber`タイプは`Person`内に定義されています。フィールドの1つに定義済み値のリストのいずれかを持たせたい場合は、`enum`タイプを定義することもできます。ここでは、電話番号が`PHONE_TYPE_MOBILE`、`PHONE_TYPE_HOME`、または`PHONE_TYPE_WORK`のいずれかの電話タイプであることを指定したいとします。
各要素の「= 1」、「= 2」というマーカーは、そのフィールドがバイナリエンコーディングで使用する一意の「タグ」を識別します。タグ番号1~15は、それよりも大きい番号よりも1バイト少なくエンコードできるため、最適化として、これらを頻繁に使用される要素や繰り返し要素に使い、タグ16以降をあまり使用されないオプション要素に残すことができます。繰り返しフィールドの各要素はタグ番号を再エンコードする必要があるため、繰り返しフィールドは特にこの最適化の良い候補です。
各フィールドには、以下のいずれかの修飾子を注釈付けする必要があります。
optional
: フィールドは設定されていても、設定されていなくてもよい。オプションフィールドの値が設定されていない場合、デフォルト値が使用される。単純な型の場合、例の電話番号`type`のように独自のデフォルト値を指定できる。そうでない場合、システムデフォルトが使用される。数値型の場合はゼロ、文字列の場合は空文字列、ブール型の場合はfalse。埋め込みメッセージの場合、デフォルト値は常にメッセージの「デフォルトインスタンス」または「プロトタイプ」であり、そのフィールドは何も設定されていない。明示的に設定されていないオプション(または必須)フィールドの値を取得するためにアクセサを呼び出すと、常にそのフィールドのデフォルト値が返される。repeated
: フィールドは任意の回数(0回を含む)繰り返されることがあります。繰り返し値の順序はプロトコルバッファ内で保持されます。繰り返しフィールドは動的なサイズの配列と考えることができます。required
: フィールドには値が提供されなければなりません。提供されない場合、メッセージは「未初期化」とみなされます。未初期化のメッセージをビルドしようとすると`RuntimeException`がスローされます。未初期化のメッセージをパースすると`IOException`がスローされます。これ以外は、必須フィールドはオプションフィールドとまったく同じように動作します。
重要
Requiredは永遠に フィールドを`required`としてマークすることには非常に注意が必要です。ある時点で必須フィールドの書き込みや送信を停止したい場合、そのフィールドをオプションフィールドに変更することは問題となります。古いリーダーは、このフィールドのないメッセージを不完全とみなし、意図せず拒否または破棄する可能性があります。代わりに、バッファに対してアプリケーション固有のカスタム検証ルーチンを記述することを検討すべきです。Google社内では、`required`フィールドは強く推奨されていません。proto2構文で定義されているほとんどのメッセージは、`optional`と`repeated`のみを使用しています。(Proto3は`required`フィールドをまったくサポートしていません。)すべての可能なフィールド型を含む` .proto `ファイルの作成に関する完全なガイドは、プロトコルバッファ言語ガイドに記載されています。ただし、クラス継承に似た機能を探さないでください。プロトコルバッファはそれを行いません。
Protocol Buffer のコンパイル
これで`.proto`ファイルができたので、次に必要なのは`AddressBook`(したがって`Person`と`PhoneNumber`)メッセージを読み書きするために必要なクラスを生成することです。これを行うには、`.proto`に対してプロトコルバッファコンパイラ`protoc`を実行する必要があります。
コンパイラをインストールしていない場合は、パッケージをダウンロードし、README の指示に従ってください。
次に、コンパイラを実行します。ソースディレクトリ(アプリケーションのソースコードが存在する場所。値を指定しない場合は現在のディレクトリが使用されます)、宛先ディレクトリ(生成されたコードを配置する場所。`$SRC_DIR`と同じである場合が多い)、および`.proto`へのパスを指定します。この場合、あなたは...
protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/addressbook.proto
Javaクラスを生成したいので、`--java_out`オプションを使用します。他のサポートされている言語にも同様のオプションが用意されています。
これにより、指定された宛先ディレクトリに`com/example/tutorial/protos/`サブディレクトリが生成され、いくつかの生成された`.java`ファイルが含まれます。
Protocol Buffer API
生成されたコードの一部を見て、コンパイラが作成したクラスとメソッドを確認しましょう。`com/example/tutorial/protos/`を見ると、`addressbook.proto`で指定した各メッセージのクラスを定義する`.java`ファイルが含まれていることがわかります。各クラスには、そのクラスのインスタンスを作成するために使用する独自の`Builder`クラスがあります。ビルダーの詳細については、以下のビルダーとメッセージのセクションで確認できます。
メッセージとビルダーの両方には、メッセージの各フィールドに対応する自動生成されたアクセサーメソッドがあります。メッセージにはゲッターのみがありますが、ビルダーにはゲッターとセッターの両方があります。`Person`クラスのアクセサーの一部を以下に示します(簡潔にするために実装は省略されています)。
// required string name = 1;
public boolean hasName();
public String getName();
// required int32 id = 2;
public boolean hasId();
public int getId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
一方、`Person.Builder`は同じゲッターとセッターを持っています。
// required string name = 1;
public boolean hasName();
public String getName();
public Builder setName(String value);
public Builder clearName();
// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();
// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();
// repeated .tutorial.Person.PhoneNumber phones = 4;
public List<PhoneNumber> getPhonesList();
public int getPhonesCount();
public PhoneNumber getPhones(int index);
public Builder setPhones(int index, PhoneNumber value);
public Builder addPhones(PhoneNumber value);
public Builder addAllPhones(Iterable<PhoneNumber> value);
public Builder clearPhones();
ご覧のように、各フィールドにはシンプルなJavaBeansスタイルのゲッターとセッターがあります。また、各単一フィールドには、そのフィールドが設定されている場合に`true`を返す`has`ゲッターもあります。最後に、各フィールドには、フィールドを空の状態に戻す`clear`メソッドがあります。
繰り返しフィールドにはいくつかの追加メソッドがあります。`Count`メソッド(リストのサイズを短縮したもの)、インデックスでリストの特定の要素を取得または設定するゲッターとセッター、リストに新しい要素を追加する`add`メソッド、およびリストに要素全体を追加する`addAll`メソッドです。
`.proto`ファイルでは小文字とアンダースコアを使用しているにもかかわらず、これらのアクセサメソッドがキャメルケース命名を使用していることに注目してください。この変換はプロトコルバッファコンパイラによって自動的に行われ、生成されたクラスが標準的なJavaスタイルの慣例に一致するようにします。`.proto`ファイルでは常にフィールド名に小文字とアンダースコアを使用すべきです。これにより、生成されるすべての言語で適切な命名規則が確保されます。良好な`.proto`スタイルに関する詳細については、スタイルガイドを参照してください。
特定のフィールド定義に対してプロトコルコンパイラが正確にどのようなメンバーを生成するかについての詳細は、Java生成コードリファレンスを参照してください。
列挙型とネストされたクラス
生成されたコードには、`Person`内にネストされた`PhoneType` Java 5 enumが含まれています。
public static enum PhoneType {
PHONE_TYPE_UNSPECIFIED(0, 0),
PHONE_TYPE_MOBILE(1, 1),
PHONE_TYPE_HOME(2, 2),
PHONE_TYPE_WORK(3, 3),
;
...
}
ネストされた型`Person.PhoneNumber`は、期待どおり、`Person`内のネストされたクラスとして生成されます。
ビルダーとメッセージ
プロトコルバッファコンパイラによって生成されるメッセージクラスはすべて_不変_です。メッセージオブジェクトが構築されると、Javaの`String`と同様に、変更することはできません。メッセージを構築するには、まずビルダーを構築し、設定したいフィールドに選択した値を設定し、その後、ビルダーの`build()`メソッドを呼び出す必要があります。
ビルダーのメッセージを変更する各メソッドが、別のビルダーを返すことに気づいたかもしれません。返されるオブジェクトは、実際にメソッドを呼び出したのと同じビルダーです。これは、コードの1行で複数のセッターを連結できるように、便宜上返されます。
`Person`のインスタンスを作成する方法の例を次に示します。
Person john =
Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.addPhones(
Person.PhoneNumber.newBuilder()
.setNumber("555-4321")
.setType(Person.PhoneType.PHONE_TYPE_HOME)
.build());
.build();
標準的なメッセージメソッド
各メッセージとビルダーのクラスには、メッセージ全体をチェックしたり操作したりするための、他にもいくつかのメソッドが含まれています。
isInitialized()
: すべての必須フィールドが設定されているかを確認します。toString()
: メッセージの人間が読める表現を返します。デバッグに特に役立ちます。mergeFrom(Message other)
: (ビルダーのみ) `other`の内容をこのメッセージにマージします。単一のスカラーフィールドを上書きし、複合フィールドをマージし、繰り返しフィールドを連結します。clear()
: (ビルダーのみ) すべてのフィールドを空の状態に戻します。
これらのメソッドは、すべてのJavaメッセージとビルダーが共有する`Message`および`Message.Builder`インターフェースを実装しています。詳細については、`Message`の完全なAPIドキュメントを参照してください。
パースとシリアライズ
最後に、各 protocol buffer クラスには、protocol buffer のバイナリフォーマットを使用して、選択した型のメッセージを書き込み、読み取るためのメソッドがあります。これらには以下が含まれます。
byte[] toByteArray();
: メッセージをシリアライズし、その生のバイトを含むバイト配列を返します。static Person parseFrom(byte[] data);
: 指定されたバイト配列からメッセージをパースします。void writeTo(OutputStream output);
: メッセージをシリアライズし、`OutputStream`に書き込みます。static Person parseFrom(InputStream input);
: `InputStream`からメッセージを読み込み、パースします。
これらは、解析とシリアライゼーションのために提供されるオプションのほんの一部です。完全なリストについては、`Message` APIリファレンスを参照してください。
重要
プロトコルバッファとオブジェクト指向設計 プロトコルバッファクラスは基本的に(Cの構造体のような)データホルダーであり、追加機能は提供しません。オブジェクトモデルにおいて、ファーストクラス市民としてはあまり適していません。生成されたクラスに豊富な動作を追加したい場合、最良の方法は、生成されたプロトコルバッファクラスをアプリケーション固有のクラスでラップすることです。プロトコルバッファのラッパー化は、`.proto`ファイルの設計を制御できない場合(例えば、他のプロジェクトから再利用している場合)にも良いアイデアです。その場合、ラッパークラスを使用して、アプリケーションの固有の環境により適したインターフェースを作成できます。一部のデータやメソッドを隠蔽したり、便利な機能を提供したりするなどです。生成されたクラスに継承によって動作を追加してはなりません。これは内部メカニズムを破壊し、いずれにしても良いオブジェクト指向のプラクティスではありません。メッセージの書き込み
それでは、プロトコルバッファクラスを使ってみましょう。まず、アドレス帳アプリケーションにアドレス帳ファイルに個人情報を書き込めるようにしたいとします。これを行うには、プロトコルバッファクラスのインスタンスを作成して値を設定し、それらをOutputStreamに書き込む必要があります。
以下に、ファイルから`AddressBook`を読み込み、ユーザー入力に基づいて新しい`Person`を1人追加し、新しい`AddressBook`を再びファイルに書き出すプログラムを示します。プロトコルコンパイラによって生成されたコードを直接呼び出すか参照する部分は強調表示されています。
import com.example.tutorial.protos.AddressBook;
import com.example.tutorial.protos.Person;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.PrintStream;
class AddPerson {
// This function fills in a Person message based on user input.
static Person PromptForAddress(BufferedReader stdin,
PrintStream stdout) throws IOException {
Person.Builder person = Person.newBuilder();
stdout.print("Enter person ID: ");
person.setId(Integer.valueOf(stdin.readLine()));
stdout.print("Enter name: ");
person.setName(stdin.readLine());
stdout.print("Enter email address (blank for none): ");
String email = stdin.readLine();
if (email.length() > 0) {
person.setEmail(email);
}
while (true) {
stdout.print("Enter a phone number (or leave blank to finish): ");
String number = stdin.readLine();
if (number.length() == 0) {
break;
}
Person.PhoneNumber.Builder phoneNumber =
Person.PhoneNumber.newBuilder().setNumber(number);
stdout.print("Is this a mobile, home, or work phone? ");
String type = stdin.readLine();
if (type.equals("mobile")) {
phoneNumber.setType(Person.PhoneType.PHONE_TYPE_MOBILE);
} else if (type.equals("home")) {
phoneNumber.setType(Person.PhoneType.PHONE_TYPE_HOME);
} else if (type.equals("work")) {
phoneNumber.setType(Person.PhoneType.PHONE_TYPE_WORK);
} else {
stdout.println("Unknown phone type. Using default.");
}
person.addPhones(phoneNumber);
}
return person.build();
}
// Main function: Reads the entire address book from a file,
// adds one person based on user input, then writes it back out to the same
// file.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: AddPerson ADDRESS_BOOK_FILE");
System.exit(-1);
}
AddressBook.Builder addressBook = AddressBook.newBuilder();
// Read the existing address book.
try {
addressBook.mergeFrom(new FileInputStream(args[0]));
} catch (FileNotFoundException e) {
System.out.println(args[0] + ": File not found. Creating a new file.");
}
// Add an address.
addressBook.addPerson(
PromptForAddress(new BufferedReader(new InputStreamReader(System.in)),
System.out));
// Write the new address book back to disk.
FileOutputStream output = new FileOutputStream(args[0]);
addressBook.build().writeTo(output);
output.close();
}
}
メッセージの読み取り
もちろん、アドレス帳から情報を取り出せなければ、あまり役に立ちません!この例では、上記の例で作成されたファイルを読み取り、そこに含まれるすべての情報を表示します。
import com.example.tutorial.protos.AddressBook;
import com.example.tutorial.protos.Person;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.PrintStream;
class ListPeople {
// Iterates though all people in the AddressBook and prints info about them.
static void Print(AddressBook addressBook) {
for (Person person: addressBook.getPeopleList()) {
System.out.println("Person ID: " + person.getId());
System.out.println(" Name: " + person.getName());
if (person.hasEmail()) {
System.out.println(" E-mail address: " + person.getEmail());
}
for (Person.PhoneNumber phoneNumber : person.getPhonesList()) {
switch (phoneNumber.getType()) {
case PHONE_TYPE_MOBILE:
System.out.print(" Mobile phone #: ");
break;
case PHONE_TYPE_HOME:
System.out.print(" Home phone #: ");
break;
case PHONE_TYPE_WORK:
System.out.print(" Work phone #: ");
break;
}
System.out.println(phoneNumber.getNumber());
}
}
}
// Main function: Reads the entire address book from a file and prints all
// the information inside.
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: ListPeople ADDRESS_BOOK_FILE");
System.exit(-1);
}
// Read the existing address book.
AddressBook addressBook =
AddressBook.parseFrom(new FileInputStream(args[0]));
Print(addressBook);
}
}
Protocol Buffer の拡張
プロトコルバッファを使用するコードをリリースした後、遅かれ早かれプロトコルバッファの定義を「改善」したくなることでしょう。新しいバッファが後方互換性があり、古いバッファが前方互換性があることを望むなら(そして、ほとんどの場合そう望むはずです)、従うべきルールがいくつかあります。プロトコルバッファの新しいバージョンでは
- 既存のフィールドのタグ番号を変更しては*いけません*。
- required フィールドを追加または削除しては*いけません*。
- optional または repeated フィールドを削除することは*できます*。
- 新しいオプションフィールドまたは繰り返しフィールドを追加することはできますが、新しいタグ番号を使用する必要があります(つまり、このプロトコルバッファでこれまで使用されていなかったタグ番号で、削除されたフィールドによっても使用されていないタグ番号です)。
(これらのルールにはいくつかの例外がありますが、めったに使用されません。)
これらの規則に従えば、古いコードは新しいメッセージを問題なく読み取り、新しいフィールドは単純に無視します。古いコードにとっては、削除されたオプションフィールドは単にデフォルト値になり、削除された繰り返しフィールドは空になります。新しいコードも古いメッセージを透過的に読み取ります。ただし、新しいオプションフィールドは古いメッセージには存在しないことに注意してください。そのため、`has_`で明示的に設定されているかどうかを確認するか、タグ番号の後に`[default = value]`を付けて`.proto`ファイルに適切なデフォルト値を指定する必要があります。オプション要素のデフォルト値が指定されていない場合、型固有のデフォルト値が代わりに使用されます。文字列の場合、デフォルト値は空の文字列です。ブール型の場合、デフォルト値はfalseです。数値型の場合、デフォルト値はゼロです。また、新しい繰り返しフィールドを追加した場合、それには`has_`フラグがないため、新しいコードでそれが(新しいコードによって)空にされたのか、まったく設定されなかったのか(古いコードによって)を区別できないことにも注意してください。
高度な使い方
プロトコルバッファには、単純なアクセサーとシリアライゼーションを超えた用途があります。それらを使って他に何ができるかについては、Java APIリファレンスをぜひ調べてみてください。
プロトコルメッセージクラスが提供する重要な機能の1つは_リフレクション_です。特定のメッセージタイプに対してコードを記述することなく、メッセージのフィールドを反復処理し、その値を操作できます。リフレクションの非常に便利な使い方の1つは、プロトコルメッセージをXMLやJSONなどの他のエンコーディング形式との間で変換することです。より高度なリフレクションの使用法としては、同じタイプの2つのメッセージ間の違いを見つけたり、特定のメッセージ内容に一致する式を記述できる「プロトコルメッセージの正規表現」のようなものを開発したりすることが考えられます。想像力を働かせれば、プロトコルバッファは当初予想されるよりもはるかに幅広い問題に適用できる可能性があります!
リフレクションは`Message`および`Message.Builder`インターフェースの一部として提供されます。