Java と gRPC 界隈のこれまでの現状
Java, Kotlin ユーザーの皆様、gRPC は使っているでしょうか?
JVM 言語の上で gRPC を使う際、grpc-java が提供されていますが、これは Java 上で netty を使って単体で動くもので、Spring Boot との親和性が皆無でした。
Google 社内では Spring Boot を使っていないであろうから良いのでしょうが、我々一般の Java 開発者にとっては不自由なものです。
これまでは、有志からいくつかの Spring Boot の上で grpc-java を動かすライブラリが提供されていましたが、どれも個人が開発しているもの (LogNet/grpc-spring-boot-starter など) で、プロジェクトが放置気味だったり品質が微妙だったり、良い選択肢があまりありませんでした。
grpc-ecosystem/grpc-spring 爆誕
そんな中、少し前に grpc-ecosystem organization に grpc-spring が追加されました。
これからは、この Spring Boot 実装が gRPC 公認のコミュニティプロジェクトとしてメンテされることになる様です。
この実装は元々、yidongnan/grpc-spring-boot-starter として開発されていたもので、中々対応されていなかった Spring Boot 3 対応を機に grpc-ecosystem へ移管された模様。
きっとこれからは、Java + Spring Boot で gRPC というと grpc-spring が標準的になっていくのでしょう。
英語のドキュメントも整備されています: gRPC-Spring-Boot-Starter Documentation
サンプルコードを動かしてみる
早速、grpc-spring がどんな感じで使えるか見ていきましょう。
サンプルコードに沿って、以下のようなシンプルな gRPC サービス MyService
を実装していきます。
syntax = "proto3";
package net.devh.boot.grpc.example;
option java_multiple_files = true;
option java_package = "net.devh.boot.grpc.examples.lib";
option java_outer_classname = "HelloWorldProto";
// gRPC サービスの定義
service MyService {
rpc SayHello (HelloRequest) returns (HelloReply) {
}
}
// リクエストメッセージの定義
message HelloRequest {
string name = 1;
}
// レスポンスメッセージの定義
message HelloReply {
string message = 1;
}
gRPC Server
まずサーバー側のコードですが、Gradle の設定や protoc plugin などは、公式ドキュメントの Getting started に詳しく書かれているので割愛して、肝となる部分を見ていきましょう。
実際のサンプルコードは、grpc-spring/examples/local-grpc-server にあります。
基本的にやることは4つで
- protoc が生成した
MyServiceGrpc.MyServiceImplBase
を継承したクラスMyServiceImpl
を定義する -
@GrpcService
アノテーションをMyServiceImpl
に追加する -
MyServiceImpl
が、Spring に Bean としてスキャンされる様にする - 実際の gRPC のサービスメソッドを実装する
つまり、proto をコンパイルして生成されたクラスを継承したクラスを書いて、そこに @GrpcService
アノテーションをつけるだけです。
import example.HelloReply;
import example.HelloRequest;
import example.MyServiceGrpc;
import io.grpc.stub.StreamObserver;
import net.devh.boot.grpc.server.service.GrpcService;
@GrpcService
public class MyServiceImpl extends MyServiceGrpc.MyServiceImplBase {
@Override
public void sayHello(HelloRequest request, StreamObserver<HelloReply> responseObserver) {
HelloReply reply = HelloReply.newBuilder()
.setMessage("Hello ==> " + request.getName())
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
}
}
この状態で Spring Boot を立ち上げると、デフォルトでは 9090 ポートに gRPC サーバーが立ち上がっています。
ポートを変更したい場合には、Spring Boot のプロパティで以下の様に設定を変更できます。
ただし、他の Spring が利用しているポートと同じポートを利用することはできません。同じアプリケーションで、Spring MVC や Webflux を利用している場合でも、gRPC は別のポートで立ち上げる必要があります。
grpc:
server:
port: 9898
実際に、gRPCurl などの gRPC デバッグツールを利用して API を叩いてみましょう。
$ grpcurl --plaintext -d '{"name": "test"}' localhost:9090 net.devh.boot.grpc.example.MyService/sayHello
{
"message": "Hello ==> test"
}
Exception handler
@GrpcAdvice
アノテーションを付与したクラスを定義して、その中に @GrpcExceptionHandler
アノテーションを付与したメソッドを定義すると、引数の型に対応した例外ハンドラが書ける様です。
gRPC は、独自のステータスコードを元にエラーを返すので、アプリケーションが定義する例外から Status
または StatusException
への変換を書くことになります。
@GrpcAdvice
public class GrpcExceptionAdvice {
@GrpcExceptionHandler
public Status handleInvalidArgument(IllegalArgumentException e) {
return Status.INVALID_ARGUMENT.withDescription("Your description").withCause(e);
}
@GrpcExceptionHandler(ResourceNotFoundException.class)
public StatusException handleResourceNotFoundException(ResourceNotFoundException e) {
Status status = Status.NOT_FOUND.withDescription("Your description").withCause(e);
Metadata metadata = ...
return status.asException(metadata);
}
}
その他
Spring Security にも対応している様です。詳しくは、Server Security あたりを参照。
また、grpc-java にはいくつかの Flavor が用意されていて、ここではブロッキングする通常のクラスを使っていますが、Reactor, RxJava, Kotlin Coroutines に対応した非同期の Flavor も用意されています。
grpc-spring はこれらにも対応してくれているので、非同期の gRPC サーバーも簡単に書く事ができます。詳しくは gRPC-Java Flavors を参照してください。
gRPC Client
では、クライアント側の実装も、Getting started に書かれている手順を元に抜粋して見ていきましょう。
実際のサンプルコードは、grpc-spring/examples/local-grpc-client にあります。
クライアントも手順はとても簡単で、以下の様な手順になるでしょう
- protoc が生成した gRPC サービスのクライアントスタブ、ここでは
MyServiceBlockingStub
を、利用するサービスクラスにフィールドとして定義する - 定義したスタブのフィールドに
@GrpcClient
アノテーションを付与する - アノテーションで定義した名前を元に、Spring Boot のプロパティで接続先等を設定する
他にも、@GrpcClientBean
を利用してクライアントスタブを注入する方法がある様ですが、ここでは割愛します。
利用するサービスクラスは以下の様な形で、クライアントスタブを利用します。
import example.HelloRequest;
import example.MyServiceGrpc.MyServiceBlockingStub;
import net.devh.boot.grpc.client.inject.GrpcClient;
import org.springframework.stereotype.Service;
@Service
public class FoobarService {
@GrpcClient("myService")
private MyServiceBlockingStub myServiceStub;
public String receiveGreeting(String name) {
HelloRequest request = HelloRequest.newBuilder()
.setName(name)
.build();
return myServiceStub.sayHello(request).getMessage();
}
}
さらに、以下のように、接続先等の Spring Boot のプロパティを設定します。
myService
の部分が、@GrpcClient
のアノテーションで定義した名前と対応することになります。
grpc:
client:
myService:
address: 'static://127.0.0.1:9898'
enableKeepAlive: true
keepAliveWithoutCalls: true
negotiationType: plaintext
address の static://
の部分は、DNS や様々なサービスディスカバリなど、他の様々なターゲットを利用できる様になっているためのようです。詳しくは、Choosing the Target を参照。
その他
クライアントも、非同期のクライアントスタブに一部標準で対応している様ですが、Reactive 対応や Coroutines 対応のクライアントスタブを利用するためは、StubFactory
の実装が必要の様です。
まとめ
Java で gRPC 公認の Spring Boot 実装が出てきて、gRPC を実装するライブラリに迷わなくて済む時代になりました。実装も簡単です。
今後メンテされなくなるという点も、公式コミュニティプロジェクト化に伴いある程度は払拭されたので、今後は機会があれば grpc-spring も使っていきたい所です。
ここから宣伝
じゃあ、お前はお仕事で gRPC を実装するためにどんなライブラリ使ってるの?と言われると、LINE が開発している Armeria を使っています。
だって、gRPC だって Swagger のように Web UI でドキュメントを生成してデバッグしたいじゃん?REST API と gRPC を同じポートで共存させたいじゃん?堅牢なマイクロサービスを構築するためには多機能なデコレーターが必要じゃん?といった要望には、既存の grpc-java をベースとしたライブラリではいろいろ不足しているからです。
grpc-spring より高機能なので、こちらも使ってみてくださいね。