すきま風

勉強したことのメモとか

Multi Module ProjectのSpring Boot × Kotlin ApplicationにgRPCを導入する

Clean Architecture (Multi Module Project) のSpring BootでgRPCを使えるようにします。 仕事で使いそうなので勉強しました。本当はGoで作りたかったけど、誰も賛同してくれなかった。人望/Zero 😉

ソフトウェアバージョン

software version
OS MacOS Catalina
Spring Boot 2.3.4
Java Corretto-11.0.6
Kotlin 1.4.10
Gradle 6.6.1

モジュール構成

module description
adapters api, persistence, web
application use case
configuration 設定ファイルと起動アプリケーション
domain domain層

domain layer, application layerにはまだ何も実装していません 😌

プロジェクト構成

clean
├── adapters
│   └── web
│       ├── build.gradle.kts
│       └── src
│           └── main
│               ├── kotlin.com.example.clean.web.service.HelloWorldService.kt
│               └── proto.hello_world.proto
├── application
├── build.gradle.kts
├── configuration
│   ├── build.gradle.kts
│   └── src.main.kotlin.com.example.clean.CleanApplication.kt
└── domain

gradle.kts

build.gradle.kts

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

// use kotlin 1.4.10 at dependecyManagement
// https://github.com/spring-gradle-plugins/dependency-management-plugin/issues/235
extra["kotlin.version"] = "1.4.10"

plugins {
    id("org.springframework.boot") version "2.3.4.RELEASE" apply false
    id("io.spring.dependency-management") version "1.0.10.RELEASE"
    id("com.google.protobuf") version "0.8.13" apply false
    id("java")
    kotlin("jvm") version "1.4.10" apply false
    kotlin("plugin.spring") version "1.4.10" apply false
}

repositories {
    mavenCentral()
}

allprojects {
    group = "com.example"
    version = "0.0.1-SNAPSHOT"
}

subprojects {
    val versions by extra {
        mapOf(
            "grpcSpringBootStarter" to "4.2.0",
            "grpc" to "1.33.0",
            "protobuf" to "3.13.0",
            "grpcKotlin" to "0.1.1"
        )
    }

    apply {
        plugin("org.jetbrains.kotlin.jvm")
        plugin("org.jetbrains.kotlin.plugin.spring")
    }

    repositories {
        mavenCentral()
        google()
    }

    java.sourceCompatibility = JavaVersion.VERSION_11
    java.targetCompatibility = JavaVersion.VERSION_11

    // https://github.com/gradle/kotlin-dsl-samples/issues/843
    // https://medium.com/grandcentrix/a-deeper-look-into-gradles-kotlin-dsl-3498ecf80026
    val implementation by configurations
    val testImplementation by configurations

    dependencies {
        // https://docs.gradle.org/current/userguide/platforms.html
        // maven bom適用の書き方が変わった
        implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES))

        implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
        implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
        testImplementation("org.springframework.boot:spring-boot-starter-test") {
            exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
        }
        testImplementation("io.projectreactor:reactor-test")

    }

    tasks {
        withType<Test> {
            useJUnitPlatform()
        }

        // https://github.com/gradle/kotlin-dsl-samples/blob/master/samples/multi-kotlin-project-config-injection/build.gradle.kts
        withType<KotlinCompile>().configureEach {
            kotlinOptions {
                freeCompilerArgs = listOf("-Xjsr305=strict")
                jvmTarget = "11"
            }
        }
    }
}

settings.gradle.kts

rootProject.name = "clean"

// include(":domain", ":configuration", ":adapters:web", ":application")
include(":configuration", ":adapters:web")

configuration/build.gradle.kts

plugins {
    id("org.springframework.boot")
}

dependencies {
    // implementation(project(":application"))
    // implementation(project(":domain"))
    // implementation(project(":adapters:api"))
    // implementation(project(":adapters:persistence"))
    implementation(project(":adapters:web"))

    // grpc 起動だけなら spring-boot-starterでOK (なはず)
    // actuatorを使う場合はweb or webfluxが必要
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("org.springframework.boot:spring-boot-starter-actuator")
}

// https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/gradle-plugin/reference/html/#packaging-layered-jars
// layered() を指定しなくてもbootBuildImageで作ったDocker Imageが動作するようになっていた
tasks.getByName<org.springframework.boot.gradle.tasks.bundling.BootJar>("bootJar") {
    mainClassName = "com.example.clean.CleanApplicationKt"
}

adapters/web/build.gradle.kts

import com.google.protobuf.gradle.generateProtoTasks
import com.google.protobuf.gradle.protobuf
import com.google.protobuf.gradle.protoc
import com.google.protobuf.gradle.plugins
import com.google.protobuf.gradle.id

plugins {
    id("com.google.protobuf")
    id("idea")
}

val versions: Map<String, String> by extra

dependencies {
    // implementation(project(":application"))
    // implementation(project(":domain"))

    // change kotlin.serialization if spring boot 2.4 released
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")

    // https://github.com/LogNet/grpc-spring-boot-starter#2-setup
    // 2020/10/27 現在 version matrixは正しかったので特別な対応不要
    implementation("io.github.lognet:grpc-spring-boot-starter:${versions["grpcSpringBootStarter"]}")

    // https://grpc.io/docs/languages/kotlin/quickstart/
    // https://github.com/GoogleCloudPlatform/kotlin-samples/blob/master/run/grpc-hello-world-gradle/build.gradle.kts
    implementation("io.grpc:grpc-kotlin-stub:${versions["grpcKotlin"]}")
}

// https://cloud.google.com/blog/ja/products/application-development/use-grpc-with-kotlin
// https://github.com/grpc/grpc-kotlin/tree/master/examples
protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:${versions["protobuf"]}"
    }

    plugins {
        id("grpc") {
            artifact = "io.grpc:protoc-gen-grpc-java:${versions["grpc"]}"
        }
        // https://speakerdeck.com/n_takehata/grpc-kotlinhatokomatekotlinhua-tekirufalseka
        id("grpckt") {
            artifact = "io.grpc:protoc-gen-grpc-kotlin:${versions["grpcKotlin"]}"
        }
    }
    generateProtoTasks {
        all().forEach {
            it.plugins {
                id("grpc")
                id("grpckt")
            }
        }
    }
}

// for IntelliJ Idea
// https://stackoverflow.com/questions/52058851/using-protobuf-with-gradle-intellij
idea {
    module {
        listOf("java", "grpc", "grpckt").forEach { dir ->
            sourceSets.getByName("main").java { srcDir("$buildDir/generated/source/proto/main/$dir") }
        }
    }
}

proto

adapters/web/src/main/proto/hello_world.proto

syntax = "proto3";

package com.example.clean.proto;

option java_multiple_files = true;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

kotlin

configuration/src/main/kotlin/com/example/clean/CleanApplication.kt

@SpringBootApplication
class CleanApplication

fun main(args: Array<String>) {
    runApplication<CleanApplication>(*args)
}

adapters/web/src/main/kotlin/com/example/clean/web/HelloWorldService.kt

@GRpcService
class HelloWorldService : GreeterGrpcKt.GreeterCoroutineImplBase() {
    // grpcktでreactiveに書ける
    override suspend fun sayHello(request: HelloRequest): HelloReply = HelloReply
        .newBuilder()
        .setMessage("Hello ${request.name}")
        .build()
}

docker image

$ ./gradlew bootBuildImage
$ docker run -it -p 6565:6565 -p 8080:8080 --rm docker.io/library/configuration:0.0.1-SNAPSHOT

動作確認

evans でテストします

$ evans --host localhost --port 6565 --proto adapters/web/src/main/proto/hello_world.proto repl
> service Greeter
> call SayHello
name (TYPE_STRING) => foobar
{
  "message": "Hello foobar"
}
> exit

参考

できるだけソースにコメントにして入れておきました 😌

Sharing dependency versions between projects

GitHub - gradle/kotlin-dsl-samples: Samples builds using the Gradle Kotlin DSL

Google Cloud Blog - News, Features and Announcements

Quick start – gRPC

GitHub - GoogleCloudPlatform/kotlin-samples

GitHub - grpc/grpc-kotlin

grpc-kotlinはどこまでKotlin化できるのか? - Speaker Deck