前回の記事でgrpc-kotlinとgrpc-javaの実装を比較し、どこまでKotlin化できるかを紹介しました。
これと併せてgrpc-spring-boot-starterと組み合わせてSpring Bootでgrpc-kotlinを使うのも試していたので、今回はこちらを紹介します。
サンプルコード
以前にgrpc-spring-boot-starterを使ったKotlinのサンプルは公開していたのですが、今回はこちらをベースにgrpc-kotlinを導入してみます。
今回のサンプルも以下に公開してあります。
grpc-spring-boot-starterと組み合わせる
Gradleに必要な依存関係
build.gradleは下記です。
buildscript { ext.kotlin_version = "1.3.72" ext.coroutines_version = "1.3.3" ext.protobuf_version = "3.11.1" ext.grpc_version = "1.28.1" // CURRENT_GRPC_VERSION ext.grpc_kotlin_version = "0.1.1" // CURRENT_GRPC_KOTLIN_VERSION repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } plugins { id("org.springframework.boot") version "2.2.6.RELEASE" id("org.jetbrains.kotlin.jvm") version "1.3.61" id("org.jetbrains.kotlin.plugin.spring") version "1.3.61" id("io.spring.dependency-management") version "1.0.7.RELEASE" id("com.google.protobuf") version "0.8.9" id( "idea") id( "java") } group = "com.example.grpc.kotlin" version = "0.0.1-SNAPSHOT" sourceCompatibility = JavaVersion.VERSION_1_8 repositories { mavenCentral() } dependencies { implementation("org.springframework.boot:spring-boot-starter-webflux") // ①spring-boot-starter-webfluxを使う implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") // ②grpc関連の依存関係 implementation("io.github.lognet:grpc-spring-boot-starter:3.5.3") implementation("io.grpc:grpc-kotlin-stub:$grpc_kotlin_version") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$coroutines_version") testImplementation("org.springframework.boot:spring-boot-starter-test") } compileKotlin { kotlinOptions { freeCompilerArgs = ['-Xjsr305=strict'] jvmTarget = '1.8' } } protobuf { protoc { artifact = "com.google.protobuf:protoc:$protobuf_version" } plugins { grpc { artifact = "io.grpc:protoc-gen-grpc-java:$grpc_version" } grpckt { artifact = "io.grpc:protoc-gen-grpc-kotlin:$grpc_kotlin_version" // ② } } generateProtoTasks { all().each { task -> task.plugins { grpc { } grpckt { } // ③ } } } }
ポイントをいくつか説明します。
①spring-boot-starter-webfluxを使う
spring-boot-starter-webではなく、spring-boot-starter-webflux
の依存関係を追加しています。
implementation("org.springframework.boot:spring-boot-starter-webflux")
Spring WebFluxは、SpringでノンブロッキングI/Oを実現するためのWebフレームワークです。
直接的にgRPCと関係するわけではありませんが、クライアント側のgRPCでの通信処理がCoroutineを使って実装されおり、Controllerでスレッドをブロックせずに実行するために使用しています。
後ほどクライアント側の項目でもう少し説明します。
②gRPC関連の依存関係追加
implementation("io.github.lognet:grpc-spring-boot-starter:3.5.3") implementation("io.grpc:grpc-kotlin-stub:$grpc_kotlin_version")
grpc-spring-boot-starterとgrpc-kotlin-stub、そしてcoroutines関連の2つを追加しています。
grpc-spring-boot-starterはgrpc-stubなども内包しているため、grpc-kotlinが依存しているJava関連のライブラリはこれだけで対応できます。
サーバー
まずサーバー側の実装です。
もともとのgrpc-javaを使っての実装は下記。
@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() } }
そしてgrpc-kotlinに変えたものがこちらです。
@GRpcService(interceptors = [MessageInterceptor::class]) class ExampleGrpcService( private val exampleService: ExampleService ) : ExampleGrpcKt.ExampleCoroutineImplBase() { companion object { private val log = LoggerFactory.getLogger(ExampleGrpcService::class.java) } override suspend fun createMessage(request: CreateMessageRequest): 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") return responseBuilder.build() } }
サーバー側はSpringを使わずに実装している時と大きく変わりません。
Kotlinで生成されたExampleCoroutineImplBase
を継承し、それに伴いcreateMessage関数もサスペンド関数になっています。
また、引数からStreamObserverがなくなくなり、レスポンスの返却もオブジェクトをreturnするだけで良くなっています。
そして@GrpcService
を付ければ、gRPCのSerivceとして扱えます。
Serviceの実装をCoroutineImplBase
を使った形に変えるだけで、Spring Bootと組み合わせた中でも動かせました。
インターセプターの設定などもそのまま使えます。
クライアント
クライアント側の実装です。
もともとのgrpc-javaを使っていた時の実装は下記。
@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-kotlinに変えたものがこちらです。
@RestController class ExampleClientController { @RequestMapping("/createmessage") suspend fun createMessage(@RequestBody request: RestCreateMessageRequest): String { // ③Controllerの関数もサスペンド関数に val createMessageRequest = CreateMessageRequest.newBuilder().setName(request.name).build() val channel = ManagedChannelBuilder.forAddress("localhost", 6565) .intercept(MetadataClientInterceptor()) .usePlaintext().build() val stub = ExampleGrpcKt.ExampleCoroutineStub(channel) // ①KotlinのStubを使用 val response = coroutineScope { async { stub.createMessage(createMessageRequest) } // ②非同期実行 } return response.await().message.text } }
Channelの生成やInterceptorの設定などは同じです。
違いとして以下の部分があります。
①KotlinのStubを使用
gRPCのServiceの実行に使うStubが、Kotlinで生成されたExampleGrpcKt.ExampleCoroutineStub
に変わっています。
②非同期実行
また、これは必須ではありませんがasync
を使用して非同期で実行しています。
③Controllerの関数もサスペンド関数に
Stubを実行しているcreateMessage関数を、サスペンド関数として定義しています。
Controllerの関数をサスペンド関数として定義するのに、前述のspring-boot-starter-webflux
が必要になります。
これによりCoroutinesを使用したノンブロッキングなgRPCの非同期リクエスト処理が実現できます。
このExampleGrpcKtのStubの関数はサスペンド関数として定義されているため、もしspring-boot-starter-web
を使っていてController側が通常の関数で定義されていた場合、次のようにrunBlockingでブロックする必要があります。
val response = runBlocking { async { stub.createMessage(createMessageRequest) } }
補足
Spring WebFluxでは、RouterFunctionという実装方法も用意されていますが、ここでは比較のためControllerの形で定義しています。
まとめ
ということでgrpc-spring-boot-starterと組み合わせても問題なく動かすことができました。
基本的には従来のSpring Bootでの実装に、ServiceとStubの部分にKotlinで生成されたクラスを適用するだけです。
あとはクライアント側でSpringを使用する時はSpring WebFluxにした方が良い、ということ。
引き続き今後のアップデートにも期待です。