タケハタのブログ

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

grpc-kotlinをgrpc-spring-boot-starterと組み合わせて使う

前回の記事でgrpc-kotlinとgrpc-javaの実装を比較し、どこまでKotlin化できるかを紹介しました。
これと併せてgrpc-spring-boot-starterと組み合わせてSpring Bootでgrpc-kotlinを使うのも試していたので、今回はこちらを紹介します。

サンプルコード

以前にgrpc-spring-boot-starterを使ったKotlinのサンプルは公開していたのですが、今回はこちらをベースにgrpc-kotlinを導入してみます。

github.com

今回のサンプルも以下に公開してあります。

github.com

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フレームワークです。

spring.pleiades.io

直接的に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にした方が良い、ということ。

引き続き今後のアップデートにも期待です。