タケハタのブログ

プログラマの生き方、働き方、技術について雑多に書いていくブログです。

サーバーサイドKotlin×gRPCコトハジメ 〜③インターセプターとメタデータ(前編)〜

かなりお久しぶりになってしまいました・・・ 「サーバーサイドKotlin×gRPCコトハジメ」、3回目です。 今回から、インターセプターとメタデータについて2回に分けて紹介する予定です。

前回、前々回のコードも実行しながら説明するので、こちらも参照してもらえればと思います(主に第1回の記事)。

第1回
https://blog.takehata-engineer.com/entry/server-side-kotlin-grpc-project-creation-to-start-confirmation

第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 で次のインターセプターへ処理が移ります。

ヘッダ、複数のインターセプターの設定については後述します。

参考:https://grpc.github.io/grpc-java/javadoc/io/grpc/ServerInterceptor.html#interceptCall-io.grpc.ServerCall-io.grpc.Metadata-io.grpc.ServerCallHandler-

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」の情報が出力されているのが確認できます。

次回はインターセプターの後処理とトレーラー

今回は簡単な処理ですが、インターセプターでの前処理、メタデータのヘッダを使った実装について紹介しました。
次回はインターセプターの後処理、メタデータのトレーラーを使った実装、またメタデータについてもう少し掘り下げようと思います。