かなりお久しぶりになってしまいました・・・ 「サーバーサイドKotlin×gRPCコトハジメ」、3回目です。 今回から、インターセプターとメタデータについて2回に分けて紹介する予定です。
前回、前々回のコードも実行しながら説明するので、こちらも参照してもらえればと思います(主に第1回の記事)。
第2回
https://blog.takehata-engineer.com/entry/server-side-kotlin-grpc-protocol-buffers-options
インターセプター(前処理)
Kotlin(Java)のGrpcでは、 ServerInterceptor
というインターフェースを実装することで、サーバーのインターセプターを実装することができます。
インターセプターではメインの処理の共通の前処理、後処理などを書くのに使うことが多いと思いますが、今回の前編では「前処理」の方に触れていきます。
Globalなインターセプターの実装
まずはGlobalなインターセプターの実装です。
これは、全てのAPIへのリクエストに対して、必ず実行されるものになります。
まず、下記のクラスを実装します。
@GRpcGlobalInterceptor class LogInterceptor: ServerInterceptor { companion object{ private val log = LoggerFactory.getLogger(LogInterceptor::class.java) } override fun <ReqT : Any, RespT : Any> interceptCall(call: ServerCall<ReqT, RespT>, headers: Metadata, next: ServerCallHandler<ReqT, RespT>): ServerCall.Listener<ReqT> { log.info("method name:${call.methodDescriptor.fullMethodName}") return next.startCall(call, headers) } }
前述の通り、 ServerInterceptor
というインターフェースを実装しています。
そして interceptCall
というメソッドをオーバーライドし、インターセプターで実装したい処理を書いています。
ここでは呼び出されているgRPCのメソッド名をログとして出力しています。
そして、 @GRpcGlobalInterceptor
というアノテーションを付けると、Globalなインターセプターの扱いになります。
これはgrpc-spring-boot-starterで用意されているアノテーションです。
call
という引数は、リクエストのBody情報や呼び出しメソッドの情報などを持ち、レスポンスの情報を受信するオブジェクトです。
保持している情報は色々あるのですが、この例では call.methodDescriptor.fullMethodName
で実行されるメソッドの名前を取得しています。
下記で実行します。
$ grpcc --proto ./src/main/proto/Greeter.proto --address 127.0.0.1:6565 -i Greeter@127.0.0.1:6565> client.sayHello({name:"takehata"}, printReply) EventEmitter {} Greeter@127.0.0.1:6565> { "message": "Hello takehata", "type": "NORMAL" }
レスポンスの内容は変わりませんが、アプリケーション側のログを見ると、下記が出力されます。
INFO 6401 --- [ault-executor-0] c.e.g.k.k.interceptor.LogInterceptor : method name:Greeter/SayHello
また、interceptCall
他の2つの引数の意味ですが、
- headers リクエストのヘッダ情報(メタデータ)
- next 次のインターセプターへつなぐためのプロセッサ
となっています。
headers
はリクエストのヘッダ情報が入っています。
next
は複数インターセプターを設定した場合の次のインターセプターへ処理をつなげるためのプロセッサで、 next.startCall
で次のインターセプターへ処理が移ります。
ヘッダ、複数のインターセプターの設定については後述します。
Service固有のインターセプターの実装
インターセプターはGlobalだけでなく、gRPCの特定のServiceクラスに対して設定することもできます。
まず、下記のインターセプターを実装します。
class MessageInterceptor: ServerInterceptor { companion object{ private val log = LoggerFactory.getLogger(MessageInterceptor::class.java) } override fun <ReqT : Any, RespT : Any> interceptCall(call: ServerCall<ReqT, RespT>, headers: Metadata, next: ServerCallHandler<ReqT, RespT>): ServerCall.Listener<ReqT> { log.info("call sendMessage") return next.startCall(call, headers) } }
「call sendMessage」とログに出力するだけの処理です。
こちらでは GRpcGlobalInterceptor
のアノテーションがありません。
そして、下記の新しいServiceクラスを作成します。
syntax = "proto3"; option java_package = "com.example.grpc.kotlingrpcexample.proto"; option java_outer_classname = "MessageProtobuf"; option java_multiple_files = true; service Message { rpc sendMessage (SendRequest) returns (SendResponse); } message SendRequest { string message = 1; } message SendResponse { string status = 1; }
@GRpcService(interceptors = [MessageInterceptor::class]) class MessageService: MessageGrpc.MessageImplBase() { override fun sendMessage(request: SendRequest, responseObserver: StreamObserver<SendResponse>) { println("send message: ${request.message}") val messageBuilder = SendResponse.newBuilder() .setStatus("succsess") responseObserver.onNext(messageBuilder.build()) responseObserver.onCompleted() } }
処理としては、リクエストで受け取ったメッセージを標準出力し、「success」のステータスを返却するだけのものになります。
@GrpcService
のパラメータで、 interceptors = [MessageInterceptor::class]
と先ほど作成したインターセプターのクラスを設定しています。
これで「MessageService」のメソッドを実行した時に、「MessageInterceptor」が呼び出されるようになります。
下記で実行します。
$ grpcc --proto ./src/main/proto/Message.proto --address 127.0.0.1:6565 -i Message@127.0.0.1:6565> client.sendMessage({message:"Flustration"}, printReply) EventEmitter {} Message@127.0.0.1:6565> { "status": "succsess" }
そしてアプリケーションのログを見ると、「call sendMessage」のメッセージが出力され、その後GlobalのLoginInterceptorの処理でメソッド名が出力されています。
INFO 7567 --- [ault-executor-0] c.e.g.k.k.i.MessageInterceptor : call sendMessage INFO 7567 --- [ault-executor-0] c.e.g.k.k.i.LoggingInterceptor : method name:Message/sendMessage send message: Flustration
また、MessageInterceptorの対象になっていないGreeterServiceのメソッドを実行すると、
$ grpcc --proto ./src/main/proto/Greeter.proto --address 127.0.0.1:6565 -i Greeter@127.0.0.1:6565> client.sayHello({name:"takehata"}, printReply)
INFO 7567 --- [ault-executor-0] c.e.g.k.k.i.LoggingInterceptor : method name:Greeter/SayHello
LoggingInterceptorの処理だけが実行され、「call sendMessage」のメッセージが出力されないことが確認できます。
インターセプターの実行順序の指定
複数のインターセプターを設定した場合、その実行順序を制御することができます。
やり方は簡単で、下記のように @Order
というSpring Frameworkのアノテーション付けることで、設定されます。
@GRpcGlobalInterceptor @Order(10) class LoggingInterceptor: ServerInterceptor { companion object{ private val log = LoggerFactory.getLogger(LoggingInterceptor::class.java) } override fun <ReqT : Any, RespT : Any> interceptCall(call: ServerCall<ReqT, RespT>, headers: Metadata, next: ServerCallHandler<ReqT, RespT>): ServerCall.Listener<ReqT> { log.info("method name:${call.methodDescriptor.fullMethodName}") return next.startCall(call, headers) } }
@Order(20) class MessageInterceptor: ServerInterceptor { companion object{ private val log = LoggerFactory.getLogger(MessageInterceptor::class.java) } override fun <ReqT : Any, RespT : Any> interceptCall(call: ServerCall<ReqT, RespT>, headers: Metadata, next: ServerCallHandler<ReqT, RespT>): ServerCall.Listener<ReqT> { log.info("call sendMessage") return next.startCall(call, headers) } }
引数で渡している数値の小さいインターセプターから、先に実行されます。
今回はGlobalなLoggingInterceptorが先に実行され、その次にMessageInterceptorが実行されるように指定しました。
再びMessageServiceのメソッドを実行すると
$ grpcc --proto ./src/main/proto/Message.proto --address 127.0.0.1:6565 -i Message@127.0.0.1:6565> client.sendMessage({message:"Flustration"}, printReply)
INFO 7732 --- [ault-executor-0] c.e.g.k.k.i.LoggingInterceptor : method name:Message/sendMessage 7732 --- [ault-executor-0] c.e.g.k.k.i.MessageInterceptor : call sendMessage send message: Flustration
指定した通りの順序でインターセプターが実行され、メッセージがログ出力されています。
メタデータ(Header)を付与してInterceptorで処理する
最後に、メタデータ(Header)を使った処理も紹介します。
まず、下記のインターセプターを実装してください。
@GRpcGlobalInterceptor @Order(30) class MetadataInterceptor: ServerInterceptor { companion object{ private val log = LoggerFactory.getLogger(MetadataInterceptor::class.java) } override fun <ReqT : Any, RespT : Any> interceptCall(call: ServerCall<ReqT, RespT>, headers: Metadata, next: ServerCallHandler<ReqT, RespT>): ServerCall.Listener<ReqT> { val deviceName = headers.get(Metadata.Key.of("deviceName", Metadata.ASCII_STRING_MARSHALLER)) log.info("deviceName=$deviceName") return next.startCall(call, headers) } }
headers.get(Metadata.Key.of("deviceName", Metadata.ASCII_STRING_MARSHALLER))
で、ヘッダ情報から「deviceName」という名前の情報を取得してログに出力しています。
Metadata
クラスから値を取得するには Metadata.Key<T>
クラスを指定する必要があり、 Metadata.Key<T>
で作成しています。
この引数で指定しているのは、取得したいメタデータの名前と、取得するヘッダ情報の変換方法を指定しています。
gRPCのメタデータにはシリアル文字列、バイナリのどちらかを指定することができ、今回は文字列を使用しているため ASCII_STRING_MARSHALLER
を指定しています。
(ここの部分に関しては、次回バイナリの場合と併せて別途説明します)
そして、grpccからの実行は下記のようにします。
$ grpcc --proto ./src/main/proto/Message.proto --address 127.0.0.1:6565 -i Message@127.0.0.1:6565> client.sendMessage({message:"Flustration"}, createMetadata({"deviceName":"Galaxy S9+"}), printReply) EventEmitter {} Message@127.0.0.1:6565> { "status": "succsess" }
createMetadata
という関数を引数に渡し、JSON形式で渡したいメタデータの情報をしていすることで、ヘッダに値を設定した上でgRPCのメソッドが実行されます。
引数の順番も printReply
より前に書かないと正しく渡されないようなので、注意してください。
INFO 73486 --- [ault-executor-0] c.e.g.k.k.i.LoggingInterceptor : method name:Message/sendMessage INFO 73486 --- [ault-executor-0] c.e.g.k.k.i.MessageInterceptor : call sendMessage INFO 73486 --- [ault-executor-0] c.e.g.k.k.i.MetadataInterceptor : deviceName=Galaxy S9+ send message: Flustration
アプリケーションのログにヘッダとして渡した「deviceName」の情報が出力されているのが確認できます。
次回はインターセプターの後処理とトレーラー
今回は簡単な処理ですが、インターセプターでの前処理、メタデータのヘッダを使った実装について紹介しました。
次回はインターセプターの後処理、メタデータのトレーラーを使った実装、またメタデータについてもう少し掘り下げようと思います。