protocでコンパイルし、コードを自動生成する

protoファイルによってデータ構造を記述したら、そのデータ構造を取り扱うためのプログラムコードを自動生成することができる。その際に用いるコマンドがprotocだ。

protocコマンドの使い方は以下の通りだ(コマンドを実際に実行する際には一行で指定する)。

protoc
  --proto_path=<import命令の探索先ディレクトリ>
  --cpp_out=<C++ソースコードの出力先ディレクトリ>
  --java_out=<Javaソースコードの出力先ディレクトリ>
  --python_out=<Pythonソースコードの出力先ディレクトリ>
  protoファイルのパス

この記事ではJava以外使用しないので、以下のようなコマンドを用いて、先ほどのPersons.protoファイルからソースコードをjavaディレクトリに自動生成した。

> mkdir java
> protoc --java_out=java Persons.proto

すると、指定したディレクトリ内にPersons.javaというファイルができあがり、中にはPersons.protoで定義したデータ構造を操作するためのクラスが複数記述されていた。

自動生成されたコードを用いてプログラミング

生成されたPersons.javaファイルの構造を大まかに表すと、以下のようになる。

class Persons {

  static class Person {

    static class Builder {
    }
  }
}

これらのクラス(内部クラス)について、以下簡単に説明しておこう。

Personsは、protoファイルの名前に対応して作られたクラス。.javaファイルの名前とも一致しているpublicなクラスだ。「一つのprotoファイル内に、複数のメッセージタイプが格納される」というprotoファイルの構造に対応しており、このクラスの内部にはメッセージタイプに対応した複数の入れ子クラスが定義されている。

「Persons.Person」クラスと、さらにその入れ子クラスである「Persons.Person.Builder」クラスは(以降の説明では頭の「Persons」を省略する)、メッセージタイプ「Person」に対応したクラスである。これらには、メッセージタイプの各フィールド(nameやage)に対応したアクセッサメソッド(setName(), getName()とか)が定義されている。

Personクラスがイミュータブル(変更不可)だということをおさえれば、これらのクラスの関係を理解するのは容易だ。つまりPersonクラスには、getterメソッドはあるけれどもsetterメソッドはなく、一度生成されたインスタンスの状態を変更することができないのだ。このおかげで、Personのインスタンスは複数スレッドから安全にアクセスすることができる。対してPerson.BuilderクラスはPersonクラスのインスタンスを生成するという役割を担い、setter/getterをともに保持している。

さて、これらのクラスを用いて、Javaオブジェクトをシリアライズしてファイルに保存し、そのファイルからオブジェクトを復元する、というサンプルを作成してみよう。

プログラムは以下のようになる。

リスト:ReadWritePerson
import java.io.*;

public class ReadWritePerson {
    private static final String FILE_NAME = "person.data";

    public static void main(String[] args) throws Exception {
    // (1) Builderの生成
    Persons.Person.Builder shiraishiBuilder = Persons.Person.newBuilder();
    // (2) setterを使って値をセットしたあと、Personの生成
    Persons.Person shiraishi = shiraishiBuilder
        .setName("白石")
        .setAge(30)
        .build();
    // (3) ファイルへの書き出し
    OutputStream out = new FileOutputStream(FILE_NAME);
    shiraishi.writeTo(out);
    out.close();

    // (4) ファイルからの読み込み
    InputStream in = new FileInputStream(FILE_NAME);
    shiraishi = Persons.Person.parseFrom(in);
    in.close();
    System.out.printf("名前:%s 年齢:%d%n",
              shiraishi.getName(), shiraishi.getAge());
    }
}

上記プログラムのポイントは以下のとおり。

(1) Persons.Person.Builderのインスタンスを得るには、メッセージタイプに対応したクラスのnewBuilder()を用いる

// (1) Builderの生成
Persons.Person.Builder shiraishiBuilder = Persons.Person.newBuilder();

(2) Builderにはフィールドに対するsetter/getterが宣言されている。setterメソッドは戻り値としてthisを返すので、ここに示すように連続してメソッド呼び出しを行うことができる。Builderから、メッセージタイプのインスタンスを生成するにはbuild()を使用する

// (2) setterを使って値をセットしたあと、Personの生成
Persons.Person shiraishi = shiraishiBuilder
    .setName("白石")
    .setAge(30)
    .build();

(3) データのシリアライズを行うには、メッセージタイプのwriteTo()メソッドを使用すれば良い。同メソッドはjava.io.OutputStreamを引数にとる。ここでは、FileOutputStreamを用いることによりファイルへの書き出しを行っている

// (3) ファイルへの書き出し
OutputStream out = new FileOutputStream(FILE_NAME);
shiraishi.writeTo(out);
out.close();

(4) シリアライズされたデータからオブジェクトを復元するには、メッセージタイプのparseFrom()メソッドを使用する

// (4) ファイルからの読み込み
InputStream in = new FileInputStream(FILE_NAME);
shiraishi = Persons.Person.parseFrom(in);
in.close();
System.out.printf("名前:%s 年齢:%d%n",
          shiraishi.getName(), shiraishi.getAge());
}

このコードをコンパイルして実行するには、Protocol Bufferのランタイムライブラリに対してクラスパスを通す必要がある。ライブラリJARファイルは、Maven2のローカルリポジトリ内(デフォルトでは「ユーザのホームディレクトリ/.m2/repository」)にインストールされているはずなので、それを利用する(もちろん、Maven2の流儀に従ってpom.xmlを作成しても良い)。

> javac -classpath ~/.m2/repository/com/google/protobuf/protobuf-java/2.0.0beta/protobuf-java-2.0.0beta.jar:. *.java
> java -classpath ~/.m2/repository/com/google/protobuf/protobuf-java/2.0.0beta/protobuf-java-2.0.0beta.jar:. ReadWritePerson
名前:白石 年齢:30

javacでコンパイルし、javaコマンドで実行した結果、プログラム内でPersonにセットした値が正しく表示されているようだ。

以上で、Protocol Bufferの基本的な使用方法についての説明は完了だ。