Kotlin Advent Calendar 2019 2日目の記事です。
過去4回に渡って「サーバー再度Kotlin×gRPCコトハジメ」というタイトルでKotlinを使ったgRPCのサーバー実装方法について紹介してきたのですが、ここまでバラバラと書いてきたものをまとめ、Githubに公開しました。
(時間がなくてREADMEとか全然書けてないのはご容赦ください・・・)
そちらを使いつつ、今回はその実装について通しで説明していきます。
過去の記事たちはこちら↓
サーバーサイドKotlin×gRPCコトハジメ 〜①プロジェクト作成から起動確認まで〜
サーバーサイドKotlin×gRPCコトハジメ 〜②Protocol Buffersの色々なオプション〜
サーバーサイドKotlin×gRPCコトハジメ 〜③インターセプターとメタデータ(前編)〜
サーバーサイドKotlin×gRPCコトハジメ 〜④インターセプターとメタデータ(後編)〜
この記事の各所にもリンクを貼りますが、詳細な部分はこちらも参考にしていただければと思います。
gRPCとは?
Kotlinの説明は不要かと思うので、一応gRPCの説明だけ。
gRPCはGoogle製のRPCフレームワークで、通信プロトコルとしてHTTP/2、IDLとしてProtocol Buffersを使用し、ハイパフォーマンスな通信を実現しています。
まだまだ国内での事例は少ないですが、GraphQLとともにRESTの次の通信の技術として注目されています。
詳しくは↓こちらの資料なども見ていただければと思います。
実行方法
先にサンプルの実行方法を簡単に説明します。
下記のリポジトリをcloneしてください。
https://github.com/n-takehata/kotlin-grpc-example
Intellij IDEAを使用する場合は、プロジェクトをインポートしてビルドが終わった後、Gradleビューから Tasks -> application -> bootrun
を実行すれば起動できます。
ターミナルから実行する場合は、プロジェクトのルートディレクトリで下記のコマンドを実行して、Gradleタスクを実行してください。
./gradlew bootRun
いずれも起動が成功すると、下記ようなログが出力されます。
INFO 59249 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path '' INFO 59249 --- [ main] c.e.g.k.k.KotlinGrpcExampleApplicationKt : Started KotlinGrpcExampleApplicationKt in 2.22 seconds (JVM running for 2.538) INFO 59249 --- [ main] o.l.springboot.grpc.GRpcServerRunner : Starting gRPC Server ... INFO 59249 --- [ main] o.l.springboot.grpc.GRpcServerRunner : 'com.example.grpc.kotlin.kotlingrpcexample.grpcservice.ExampleGrpcService' service has been registered. INFO 59249 --- [ main] o.l.springboot.grpc.GRpcServerRunner : gRPC Server started, listening on port 6565.
Tomcatが8080ポートで立ち上がり、gRPCサーバーが6565ポートでlisteningしている旨が表示されていますね。
そしてcurlコマンドでリクエストパラメータを渡し実行すると、下記のようにレスポンスが返ってきます。
$ curl -H 'Content-Type:application/json' -X POST -d '{"name":"sarina"}' http://localhost:8080/createmessage Hello sarina
アプリケーションのログを見ると、インターセプターなどの処理順にそって下記の内容が出力されます。
INFO 59021 --- [ault-executor-1] c.e.g.k.k.i.LoggingInterceptor : method name=Example/CreateMessage INFO 59021 --- [ault-executor-1] c.e.g.k.k.i.MessageInterceptor : call createMessage INFO 59021 --- [ault-executor-1] c.e.g.k.k.i.MetadataInterceptor : header=Example Header INFO 59021 --- [ault-executor-1] c.e.g.k.k.g.ExampleGrpcService : execute createMessage INFO 59021 --- [ault-executor-1] c.e.g.k.k.i.MetadataInterceptor : execute close INFO 59021 --- [nio-8080-exec-3] c.e.g.k.k.c.i.MetadataClientInterceptor : trailer=Example Trailer
それでは、ここから実行しているサンプルについての内容を説明していきます。
(この実行結果の意味は全て読み終わると理解できると思います)
プロジェクトの構成
簡単に書くと、下記のような構成、実行のフローになっています。
- ClientController → gRPCで通信するクライアント(REST API)
- GrpcService → gRPCの通信のインターフェースとなるメソッドが定義されたクラス
- Service → SpringでDIするビジネスロジック層のクラス
- ServerInterceptor → gRPC通信時のサーバー側のインターセプター
- ClientInterceptor → gRPC通信時のクライアント側のインターセプター
Springアプリケーションの中で通常のHTTP通信(8080ポート)とgRPC通信(6565ポート)の接続が両方できるようになっていて、「REST APIへのHTTP通信→gRPCのServiceを実行」という流れで動作確認できるようになっています。
(マイクロサービスアーキテクチャでよくあるやつです)
今回はサンプルなので同じアプリケーションに立てていますが、もちろんREST APIとgRPCのServiceで別アプリケーション、別サーバーで立てることができます。
それでは、それぞれのコードについても説明していきます。
grpc-spring-boot-starterの使用
build.gradleは下記になります。
build.gradle
plugins {
id("org.springframework.boot") version "2.1.6.RELEASE"
id("org.jetbrains.kotlin.jvm") version "1.3.21"
id("org.jetbrains.kotlin.plugin.spring") version "1.3.21"
id("io.spring.dependency-management") version "1.0.7.RELEASE"
id("com.google.protobuf") version "0.8.9" // Protocol Buffersを扱うためのプラグイン
id( "java")
}
sourceSets {
main {
proto {
srcDir 'src/main/protobuf'
}
java {
srcDirs 'src/main/generated'
}
}
}
group = "com.example.grpc.kotlin"
version = "0.0.1-SNAPSHOT"
sourceCompatibility = JavaVersion.VERSION_1_8
def grpcVersion = "1.10.0"
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
compile("io.github.lognet:grpc-spring-boot-starter:3.3.0") // gRPCをSpring Bootで扱うためのStarter
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
compileKotlin {
kotlinOptions {
freeCompilerArgs = ['-Xjsr305=strict']
jvmTarget = '1.8'
}
}
protobuf { // Protocol Buffersのコードジェネレータに関する設定
protoc {
artifact = "com.google.protobuf:protoc:3.5.1-1"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generateProtoTasks {
all().each { task ->
task.builtins {
// Protocol Buffers関連のファイル出力先
java {
outputSubDir = "generated"
}
}
task.plugins {
// gRPC関連のファイル出力先
grpc {
outputSubDir = "generated"
}
}
}
}
generatedFilesBaseDir = "$projectDir/src/"
}
clean {
delete "$protobuf.generatedFilesBaseDir/main/generated"
}
ポイントは grpc-spring-boot-starter
の依存関係の追加です。
こちらを使うことで、Spring BootでのgRPCサーバーの実装が簡単にできるようになります。
Protocol Buffersの定義
まずはProtocol Buffersの定義。
基本は第1回の記事で紹介しています。
また、各種オプションについては第2回で紹介しています。
message
リクエスト、レスポンスで使うオブジェクトの定義です。
下記のように設定します。
Message.proto
syntax = "proto3"; option java_package = "com.example.grpc.kotlingrpcexample.proto"; option java_outer_classname = "MessageProtobuf"; option java_multiple_files = true; message CreateMessageRequest { string name = 1; } message CreateMessageResponse { Message message = 1; MessageType type = 2; } message Message { string text = 1; int32 length = 2; } enum MessageType { NONE = 0; NORMAL = 1; SPECIAL = 2; }
service
こちらは通信のインターフェースとなるメソッドの定義をします。
messageで定義したオブジェクトを引数、戻り値として使います。
Example.proto
syntax = "proto3"; option java_package = "com.example.grpc.kotlingrpcexample.proto"; option java_outer_classname = "GreeterProtobuf"; option java_multiple_files = true; import "Messages.proto"; service Example { rpc CreateMessage (CreateMessageRequest) returns (CreateMessageResponse); }
ファイルの自動生成
プロジェクトのルートディレクトリで下記コマンドの実行、もしくはIDEからGradleの generateProto
タスクを実行してください。
./gradlew generateProto
src/main/generated
ディレクトリ配下に、生成されたファイルたちが出力されます。
また、 clean
タスクを実行することで生成したコードは全て削除されるので、生成の動きをもう一度見たい場合は削除後、再度 generateProto
タスクを実行してください。
gRPCの実装クラス
こちらも基本は第1回の記事の内容です。
gRPCで自動生成したファイルを使って、gRPCのサーバーサイド処理を実装します。
GrpcService
自動生成の「ExampleImplBase」を継承して実装します。
grpc-spring-boot-staterに含まれる @GrpcService
のアノテーションを付けることで、gRPCのServiceとして認識されます。
ExampleGrpcService.kt
@GRpcService(interceptors = [MessageInterceptor::class]) class ExampleGrpcService( private val exampleService: ExampleService ) : ExampleGrpc.ExampleImplBase() { companion object { private val log = LoggerFactory.getLogger(ExampleGrpcService::class.java) } override fun createMessage(request: CreateMessageRequest, responseObserver: StreamObserver<CreateMessageResponse>) { val text = exampleService.creteMessageText(request.name) val message = Message.newBuilder() .setText(text) .setLength(text.length) .build() val responseBuilder = CreateMessageResponse.newBuilder() .setMessage(message) .setType(MessageType.NORMAL) log.info("execute createMessage") responseObserver.onNext(responseBuilder.build()) responseObserver.onCompleted() } }
ServerInterceptor
サーバー側のgRPCのインターセプターです。
ServerInterceptor
というインターフェースを実装し、 interceptCall
というメソッドをオーバーライドすることでgRPCのInterceptorでの前処理を定義できます。
全ての処理で通したいインターセプターには、@GRpcGlobalInterceptor
のアノテーションを付けます。
LoggingInterceptor.kt
@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) } }
また、複数ある場合は、Springの Order
アノテーションで実行順を指定できます。
ヘッダ情報の取得などもこちらで書きます。
MetadataInterceptor.kt
@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 exampleHeader = headers.get(Metadata.Key.of("example_header", Metadata.ASCII_STRING_MARSHALLER)) log.info("header=$exampleHeader") return next.startCall(object : ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) { override fun close(status: Status?, trailers: Metadata) { log.info("execute close") trailers.put(Metadata.Key.of("example_trailer", Metadata.ASCII_STRING_MARSHALLER), "Example Trailer") super.close(status, trailers) } }, headers) } }
また、後処理に関しては SimpleForwardingServerCall
クラスの close
メソッドをオーバーライドし、 next.startCall
に渡すことで定義できます。
こちらではトレーラーを処理することができます。
前処理とヘッダに関して第3回、後処理とトレーラーに関しては第4回の記事で紹介しています。
Serviceクラス
GrpcServiceから呼び出しているServiceクラス(名前がややこしい・・・)のインターフェースと実装クラスです。
ここはSpringのDIのサンプルとして一応入れています。
ExampleServiceImpl.kt
@Service class ExampleServiceImpl : ExampleService { override fun creteMessageText(name: String): String { return "Hello $name" } }
サンプルでは名前の情報を受け取ってメッセージ文を作って返すだけの単純な処理にしていますが、いわゆるビジネスロジックの部分はここに記述するイメージです。
gRPCのクライアントになるREST API
最後に、実行確認のためクライアント側の実装をします。
これは第4回の記事の前半で紹介しています。
クライアントと言っても、実行のインターフェースとしては# REST APIなので、サーバー to サーバー間通信でのクライアントという意味ですね。
マイクロサービスアーキテクチャのゲートウェイ的なイメージです。
ちなみにまだ試せてないですが、gRPC通信の実行部分に関しては、AndroidのKotlinクライアントと接続する時も基本的には同じように使えると思います。
ClientController
実装にはProtocol Buffersの中にあるgRPCのstubを使います。 Channel(gRPCのConnectionのようなもの)を生成して、生成されたファイルにあるstubへ渡すことで、gRPCのServiceを実行できます。
@RestController class ExampleClientController { @RequestMapping("/createmessage") fun createMessage(@RequestBody request: RestCreateMessageRequest): String { val createMessageRequest = CreateMessageRequest.newBuilder().setName(request.name).build() val channel = ManagedChannelBuilder.forAddress("localhost", 6565) .intercept(MetadataClientInterceptor()) .usePlaintext().build() val stub = ExampleGrpc.newBlockingStub(channel) val response = stub.createMessage(createMessageRequest) return response.message.text } }
処理はシンプルで、名前の情報をリクエストを受け付け、gRPCのServiceのリクエストにそのまま設定して実行した結果から、メッセージのテキストを取り出しレスポンスとして返しているだけです。
intercept
で設定しているクラスは、後述するクライアント側のInterceptorです。
ClientInterceptor
こちらはクライアント側のgRPCのインターセプターです。
ClientInterceptor
というインターフェースを実装することで、サーバー側と同じようにgRPC通信時の前処理、後処理が定義できます。
class MetadataClientInterceptor : ClientInterceptor { companion object { private val log = LoggerFactory.getLogger(MetadataClientInterceptor::class.java) } override fun <ReqT, RespT> interceptCall(method: MethodDescriptor<ReqT, RespT>?, callOptions: CallOptions?, next: Channel): ClientCall<ReqT, RespT>? { return object : SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) { override fun start(responseListener: Listener<RespT>?, headers: Metadata) { headers.put(Metadata.Key.of("example_header", Metadata.ASCII_STRING_MARSHALLER), "Example Header") super.start(MetadataClientCallListener(responseListener), headers) } } } internal class MetadataClientCallListener<RespT>(responseListener: ClientCall.Listener<RespT>?) : SimpleForwardingClientCallListener<RespT>(responseListener) { override fun onClose(status: Status?, trailers: Metadata?) { try { val exampleTrailer = trailers?.get(Metadata.Key.of("example_trailer", Metadata.ASCII_STRING_MARSHALLER)) log.info("trailer=$exampleTrailer") } finally { super.onClose(status, trailers) } } } }
start
メソッドをオーバーライドして実装している処理が前処理で、ヘッダの送信などもここで書いています。
internal classで定義している「MetadataClientCallListener」で onClose
メソッドをオーバーライドして実装している処理が後処理で、トレーラーの受診もここで書いています。
ぜひKotlinとgRPCを使うキッカケに!
最近はサーバーサイドKotlinでマイクロサービスを構成するプロダクトも出てきているため、KotlinでgRPCを使う方も徐々に増えてきていると思います。
とは言え国内ではまだまだ事例が少なかったり、gRPCという技術に馴染みがない方も多いでしょう。
Android、iOSやWebのクライアントとの通信という意味ではもっと事例が少ないです。
しかし、いずれも次世代のサーバーサイド開発においてとても可能性がある技術スタックだと思います。
パフォーマンス面など様々なメリットもありますが、この記事がまずは一旦触ってみるキッカケになれば幸いです。
今後はORMでDB接続を入れたり、ストリーミング通信などgRPCのまだ紹介していない機能、さらにスマートフォンやWebのクライアントとの接続などサンプルも拡充して、引き続きこのブログで紹介していければと考えています。
そちらもお楽しみに!