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