gRPCのJava版のチュートリアルには、Gradleを使ったビルド方法だけが説明されている。Java界隈ではMavenやGradleがビルドツールとしてよく使われているのは知っているが、私はmakeが使いたい。よって、まずはビルドツールを使わずに個々の手順を進める方法を明らかにした上で、それをMakefileにまとめる。
まずは依存パッケージをインストールしておく。gRPCのチュートリアルにあるようにGitHubからリポジトリを撮ってきてcmakeでビルドしてもよいが、使っているOSにバイナリパッケージがあるならば、それを使うのが楽だ。Ubuntu(21.10)の場合、aptで以下のパッケージを入れる。
- libprotobuf-java
- libgrpc-java
- protobuf-compiler-grpc-java-plugin
- libgoogle-common-protos-java
- libguava-java
- libnetty-java
- libperfmark-java
テスト用に、RPCのインターフェイスをprotoファイルとして定義しておく。任意の文字列を送るとそれをそのままエコーバックするサービスだ。lucky_star.protoとかいう名前で保存しておこう。
syntax = "proto3"; package lucky_star; message EchoRequest { string message = 1; } message EchoResponse { string echo = 1; } service HappyService { rpc Echo(EchoRequest) returns (EchoResponse); }
このprotoファイルから、GRPCのJavaコードを生成する。Protocol Buffersのコンパイラドライバであるprotocコマンドと、そのGRPC-Java用プラグインであるgrpc_java_pluginを用いる。プラグインの方はなぜか絶対パスで指定しなければいけないらしい。
$ protoc --plugin=protoc-gen-grpc_java=/usr/bin/grpc_java_plugin \ --proto_path=. --java_out=. --grpc_java_out=. lucky_star.proto
パッケージ名と同名のlucky_starというディレクトリが作られ、その中にパッケージ名をキャメルケースにしたLuckyStar.javaファイルと、サービス名にGrpcをつけたHappyServiceGrpc.javaファイルが作られる。それをカレントディレクトリに移動させる。
$ mv lucky_star/*.java .
私がバイナリパッケージとして入れたバージョンのgrpc_java_pluginは、Java8以前のパッケージを想定したコードを生成するが、最新のJDKを使っていると互換性の問題がある。そのままビルドしようとすると、以下のエラーが出てしまうのだ。
HappyServiceGrpc.java:23: error: cannot find symbol @javax.annotation.Generated( ^ symbol: class Generated location: package javax.annotation
この報告にあるように、Java8まではjavax.annotation.GeneratedであったクラスがJava9からはjavax.annotation.processing.Generatedに変わっているのが原因だ。いずれgrpc_java_plugin側で新しい構造をデフォルトにしてくれると期待しつつ、今は生成されたコードを書き換えるという回避策を取る。
$ sed -e 's/javax\.annotation\.Generated/javax.annotation.processing.Generated/g' \ HappyServiceGrpc.java > ~HappyServiceGrpc.java $ mv -f ~HappyServiceGrpc.java HappyServiceGrpc.java
実際のアプリケーションのコードを書く準備が整ったので、サーバから実装していこう。以下のようなコードになる。SampleServer.javaなどとして保存されたい。
package lucky_star; import io.grpc.Server; import io.grpc.ServerBuilder; import io.grpc.stub.StreamObserver; public class SampleServer { public static void main(String[] args) throws Exception { // サーバを作って、サービスを登録する Server server = ServerBuilder.forPort(1978) .addService(new HappyServiceImpl()).build(); // サーバを開始する server.start(); System.out.println("The server is running."); System.out.flush(); // サーバが終了するまで待つ server.awaitTermination(); } // サービスの実装クラス static class HappyServiceImpl extends HappyServiceGrpc.HappyServiceImplBase { // Echoの実装 @Override public void echo(LuckyStar.EchoRequest request, StreamObserver<LuckyStar.EchoResponse> observer) { System.out.println("Server Echo: " + request.getMessage()); System.out.flush(); // レスポンスを生成する LuckyStar.EchoResponse.Builder response = LuckyStar.EchoResponse.newBuilder(); response.setEcho(request.getMessage()); // レスポンスを出力する observer.onNext(response.build()); // 出力処理が完了するまで待つ observer.onCompleted(); } } }
クライアントは以下のようなコードになる。SampleClient.javaなどとして保存されたい。
package lucky_star; import io.grpc.Channel; import io.grpc.ManagedChannel; import io.grpc.ManagedChannelBuilder; import io.grpc.StatusRuntimeException; import java.util.concurrent.TimeUnit; public class SampleClient { public static void main(String[] args) throws Exception { ManagedChannel channel = null; try { // gRPCのチャンネルを作る channel = ManagedChannelBuilder.forTarget("localhost:1978") .usePlaintext().build(); // サービスのスタブを作る HappyServiceGrpc.HappyServiceBlockingStub stub = HappyServiceGrpc.newBlockingStub(channel); // Echoメソッドのリクエストを作る LuckyStar.EchoRequest.Builder request = LuckyStar.EchoRequest.newBuilder(); request.setMessage("Hello World"); // Echoメソッドを実行して結果を表示する LuckyStar.EchoResponse response; try { response = stub.echo(request.build()); } catch (StatusRuntimeException e) { System.err.println("RPC failed: " + e.getStatus()); return; } System.out.println("Client: Echo: " + response.getEcho()); } finally { if (channel != null) { // チャンネルの接続を切る channel.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); } } } }
アプリケーションをビルドしたり実行したりする際には、gRPCのライブラリをクラスパスに含める必要がある。gRPC-Javaをバイナリパッケージで入れた場合、/usr/share/javaの下に多数のjarファイルがあるはずだ。それをワイルドカードで全部指定する。
$ javac -d . -cp .:/usr/share/java/* HappyServiceGrpc.java LuckyStar.java SampleServer.java SampleClient.java
オプションで "-d ." を指定したので、パッケージと同じ名前のディレクトリlucky_starが作られて、その中に大量のクラスファイルが作られているはずだ。あとは普通に、javaコマンドでサーバやクライアントのmainのあるクラスを指定すれば実行できる。サーバ側は以下のようにする。
$ java -cp .:/usr/share/java/* lucky_star.SampleServer The server is running.
別の端末でクライアントを起動すると、無事にエコーバックの出力が得られるはずだ。
$ java -cp .:/usr/share/java/* lucky_star.SampleClient Client: Echo: Hello World
JARファイルにまとめて扱いやすくする。また、JARファイルだけで動くか確認もする
$ jar cvf lucky_star/*.class ... $ rm -rf lucky_star
$ java -cp lucky_star.jar:/usr/share/java/* lucky_star.SampleServer The server is running.
$ java -cp lucky_star.jar:/usr/share/java/* lucky_star.SampleClient Client: Echo: Hello World
以上で、一通りのビルド手順が網羅できている。MavenやGradleがなくても生きていける(あった方が便利なのは言うまでもないが)。さて、私は古き良きmake派なので、Makefileを書こう。本当はconfigureから書くのだが、説明のためにMakefileをいきなり書く。コピペするときは先頭のインデントはタブに置換する必要がある。
SHELL = /bin/bash JARFILES = lucky_star.jar JAVAFILES = HappyServiceGrpc.java LuckyStar.java SampleServer.java SampleClient.java JAVAC = /usr/java/bin/javac GRPCJARDIR = /usr/share/java JAVACFLAGS = -d . -cp .:$(GRPCJARDIR)/* JAR = /usr/java/bin/jar JAVARUN = /usr/java/bin/java JAVARUNFLAGS = -cp lucky_star.jar:$(GRPCJARDIR)/* PROTOC = /usr/bin/protoc GRPCJAVAPLUGIN = /usr/bin/grpc_java_plugin all : $(JARFILES) clean : rm -rf $(JARFILES) lucky_star *~ check : $(JAVARUN) $(JAVARUNFLAGS) lucky_star.SampleServer & SERVER_PID="$$!" ; sleep 2 ;\ $(JAVARUN) $(JAVARUNFLAGS) lucky_star.SampleClient ;\ kill $$SERVER_PID proto2java : lucky_star.proto rm -rf lucky_star $(PROTOC) --plugin=protoc-gen-grpc_java=$(GRPCJAVAPLUGIN) \ --proto_path=. --java_out=. --grpc_java_out=. lucky_star.proto mv lucky_star/*.java . rm -rf lucky_star sed -e 's/javax\.annotation\.Generated/javax.annotation.processing.Generated/g' \ HappyServiceGrpc.java > ~HappyServiceGrpc.java mv -f ~HappyServiceGrpc.java HappyServiceGrpc.java lucky_star.jar : $(JAVAFILES) $(JAVAC) $(JAVACFLAGS) $(JAVAFILES) $(JAR) cvf $@ lucky_star/*.class rm -rf lucky_star
protoファイルから生成したjavaファイルは、予めパッケージやリポジトリに入れておく方針にした。そうすれば、利用者はprotocやprotoc-gen-grpc_javaをインストールする必要がなくなるからだ。なので、開発時にprotoファイルを更新した場合には、明示的に make prot2java を実行する。利用者側では、make && make check を実行するだけでビルドと動作確認ができる。make checkの結果はこんな感じになる。
$ make check /usr/java/bin/java -cp lucky_star.jar:/usr/share/java/* lucky_star.SampleServer & SERVER_PID="$!" ; sleep 2 ;\ /usr/java/bin/java -cp lucky_star.jar:/usr/share/java/* lucky_star.SampleClient ;\ kill $SERVER_PID The server is running. Server Echo: Hello World Client: Echo: Hello World
まとめ。gRPCのJava版のアプリケーションを手動でビルドする手順を紹介した。また、それをMakefileにまとめた。ビルド手順に躓いてgRPCを回避していた人も少なからずいると思うので、この記事を見て戻ってきてくれる人がいたなら嬉しい。
Tkrzw-RPCのJava版クライアントの開発が後回しになっていたのは、この手順を調べるのがやたら面倒だったからだ。というか、Gradleをビルドするためにprotobufのバージョンを上げなきゃならなかったのだが、そうすると現状のシステムと非互換が発生するので作業できなかったのだ。一昨日、システムをUbuntu 21.10にアップグレードしたので、いろいろ捗るようになった。gRPC周りは依存コンポーネントのバージョンの違いに敏感なので、バイナリパッケージで入れないときつい。Gradle使えよと言われそうだが、C++とPythonとRubyとGoもメンテせにゃならんもんで。