Kotlin + SpringBootでのGradleマルチプロジェクト

# 目的
共通部品に依存するAPIやバッチを一つのプロジェクトにまとめるマルチプロジェクト化をしたい人のためにKotlin + Gradleでの方法を書きたいと思います
(せっかくなのでJarファイル作成の設定も記載しておきます)  

# 環境
* MacBookPro M1(zsh)
* IntelliJ IDEA Community版
* Gradle 7.0.2

成果物  
GitHub: https://github.com/seki-shinnosuke/kotlin-gradle-multi-project

# 内容
今回作るマルチプロジェクト構成はこんな感じです  
```
kotlin-gradle-multi-project
├── common
│   └── // 共通処理(UtilやComponent)を配置するためのサブプロジェクト
├── api
│   └── // SpringBootで作成したRestAPI
└── batch
    └── // SpringBootで作成したCommandLine
```

まずは「settings.gradle.kts」  
kotlin-gradle-multi-projectをrootプロジェクトとし配下の各プロジェクトはサブプロジェクトとしてincludeします  
```kotlin:settings.gradle.kts
rootProject.name = "kotlin-gradle-multi-project"
include("common", "api", "batch")
```

そして今回の肝となる「build.gradle.kts」  
```kotlin:build.gradle.kts
import io.spring.gradle.dependencymanagement.dsl.DependencyManagementExtension
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.springframework.boot.gradle.tasks.bundling.BootJar

object Versions {
    const val jdk = "17"
}

plugins {
    application
    idea
    kotlin("jvm") version "1.6.10"
    kotlin("plugin.spring") version "1.6.10"
    id("org.springframework.boot") version "2.6.6"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
}

allprojects {
    group = "com.example"

    repositories {
        mavenCentral()
        maven("https://plugins.gradle.org/m2/")
    }

    tasks {
        // JSR 305チェックを明示的に有効にする
        withType<KotlinCompile>().configureEach {
            kotlinOptions.freeCompilerArgs = listOf("-Xjsr305=strict", "-java-parameters")
            kotlinOptions.jvmTarget = Versions.jdk
        }
        withType<Test>().configureEach {
            useJUnitPlatform()
            val javaToolchains = project.extensions.getByType<JavaToolchainService>()
            javaLauncher.set(
                javaToolchains.launcherFor {
                    languageVersion.set(JavaLanguageVersion.of(Versions.jdk.toInt()))
                }
            )
        }

        // 共通部品を入れるプロジェクト以外はBootJarを生成可能にする
        // Jarファイル名: kotlin-gradle-multi-project-[api|batch].jar
        // 実行クラス名: com.example.[api|batch].[Api|Batch]Application
        withType<BootJar>().configureEach {
            if (this.project == rootProject || this.project.name == "common") {
                enabled = false
            } else {
                mainClass.set("${rootProject.group}.${this.project.name}.${this.project.name.capitalize()}Application")
            }
        }
        withType<Jar>().configureEach {
            if (this.project == rootProject) {
                enabled = false
            } else {
                enabled = true
                archiveBaseName.set("${rootProject.name}-${this.project.name}")
            }
        }
    }
}

subprojects {
    apply {
        plugin("kotlin")
        plugin("kotlin-spring")
        plugin("org.springframework.boot")
        plugin("io.spring.dependency-management")
    }

    dependencies {
        implementation("org.jetbrains.kotlin:kotlin-stdlib")
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-gradle-plugin")
        implementation("org.springframework.boot:spring-boot-gradle-plugin:2.6.6")

        testImplementation("org.springframework.boot:spring-boot-starter-test") {
            exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
        }
    }

    configure<DependencyManagementExtension> {
        imports {
            mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        }
    }
}

project(":common") {
    dependencies {
        implementation("org.springframework:spring-web")
    }

    tasks.bootJar {
        enabled = false
    }
    tasks.bootRun {
        enabled = false
    }
    tasks.jar {
        enabled = true
    }

    // commonに作成したテストクラスを共通部品として他のプロジェクトでも取り込みたい場合
    val testCompile by configurations.creating
    configurations.create("testArtifacts") {
        extendsFrom(testCompile)
    }
    tasks.register("testJar", Jar::class.java) {
        archiveClassifier.set("test")
        from(sourceSets["test"].output)
    }
    artifacts {
        add("testArtifacts", tasks.named<Jar>("testJar"))
    }
}

project(":api") {
    dependencies {
        // 共通プロジェクトへの依存を追加
        implementation(project(":common"))
        implementation("org.springframework.boot:spring-boot-starter-web")
    }

    springBoot {
        buildInfo()
    }
}

project(":batch") {
    dependencies {
        // 共通プロジェクトへの依存を追加
        implementation(project(":common"))
        implementation("org.springframework.boot:spring-boot-starter-web")
    }

    springBoot {
        buildInfo()
    }
}
```
ポイントは以下
```kotlin
allprojects {
  // ルートを含めた全プロジェクトに対しての設定
}
subprojects {
  // 全サブプロジェクトに対しての設定
}
project(":common") {
  // 共通プロジェクト
}
project(":xxxxxx") {
    dependencies {
        // 共通プロジェクトへの依存を追加
        implementation(project(":common"))
    }
}
```

マルチプロジェクトで共通化した際にプロダクトコードは依存関係を追加できるがテストコードでも共通のテストクラスを利用したいということも多々あると思います(DBの接続設定やテスト用のアノテーションクラスなど)  
そういった時はテストクラスをJar生成時に含める形で共通プロジェクトに以下のような設定を加えると読み込んでくれます  
```kotlin
    val testCompile by configurations.creating
    configurations.create("testArtifacts") {
        extendsFrom(testCompile)
    }
    tasks.register("testJar", Jar::class.java) {
        archiveClassifier.set("test")
        from(sourceSets["test"].output)
    }
    artifacts {
        add("testArtifacts", tasks.named<Jar>("testJar"))
    }
```

# まとめ
Java + Gradle(Groovy)でのマルチプロジェクト化は結構記事がありますがKotlinでの記事があまり見られなかったので書いてみました  
プロジェクト内で共通化するならMavenリポジトリを立ててそこにパッケージとしてPublishした方がいいのではと思いますが利用規模やGit管理時に1リポジトリで完結するプロジェクトにしたい場合は参考にしていただければと思います