Gradle
Kotlin
Dependabot
概要
- Gradleの
dependencies
ブロックに書いているライブラリの定義をVersion Catalogに移行する - Gradle Kotlin DSLを併用するとIDEの補完も効いてラク
- Dependabotとも相性が良い
みたいな話。
Version Catalogとは
とりあえず公式のドキュメントはこちら。
https://docs.gradle.org/current/userguide/platforms.html
settings.gradle.kts
や libs.version.toml
に定義することで、使用するライブラリのバージョンを1箇所に集中させるというやり方。
1箇所で集中管理することで特にマルチプロジェクト構成のGradleプロジェクトの際に効力を発揮する。
本題
事前準備
まずはマルチプロジェクト構成のGradleプロジェクトを作りましょう。
先にも書きましたがシングルプロジェクトだとVersion Catalogを使う意味は正直ありません。
Version Catalogを導入する前のマルチプロジェクト構成だと皆さん大体こうしてますよね。
- sandbox-app
- gradle
- wrapper
- gradle-wrapper.jar
- gradle-wrapper.properties
- wrapper
- sandbox-api
- build.gradle.kts
- sandbox-database
- build.gradle.kts
- gradlew
- gradlew.bat
- build.gradle.kts
- settings.gradle.kts
- gradle
これをVersion Catalogを導入するということで以下のようにしましょう。
- sandbox-app
- buildSrc
- src/main/kotlin
- sandbox-app.java-common-dependencies-conventions.gradle.kts
- sandbox-app.java-conventions.gradle.kts
- build.gradle.kts
- settings.gradle.kts
- src/main/kotlin
- gradle
- wrapper
- gradle-wrapper.jar
- gradle-wrapper.properties
- libs.versions.toml
- wrapper
- sandbox-api
- build.gradle.kts
- sandbox-database
- build.gradle.kts
- gradlew
- gradlew.bat
- build.gradle.kts
- settings.gradle.kts
- buildSrc
ポイントとしては buildSrc
ディレクトリと libs.versions.toml
ファイルの2つになります。
libs.versions.toml
書き方はGradleの公式ドキュメントに書いてあるので読んでください。
https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
[versions]
にはバージョンだけ書き出して、 version.ref
で参照するようにすると良いことがあるので共通化しておきましょう。[bundles]
ブロックも上手いこと使うと依存関係を綺麗にまとめられます。
gradle/libs.versions.toml
[versions]
lombok = "1.18.30"
ulid = "8.3.0"
spring-boot = "3.1.5"
spring-dependency-management = "1.1.4"
guava = "32.1.3-jre"
commons-lang = "3.13.0"
commons-io = "2.15.0"
mysql = "8.2.0"
[libraries]
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
ulid = { module = "de.huxhorn.sulky:de.huxhorn.sulky.ulid", version.ref = "ulid" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang" }
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" }
spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "spring-boot" }
mysql = { module = "com.mysql:mysql-connector-j", version.ref = "mysql" }
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" }
[bundles]
apache-commons = [
"commons-lang3",
"commons-io"
]
buildSrc
ディレクトリの中身
Gradleにおいて buildSrc
ディレクトリは特殊な意味を持つディレクトリです。
ここにルートプロジェクト配下の全プロジェクトに適用する定義を落とし込んでいきます。
https://docs.gradle.org/current/userguide/organizing_gradle_projects.html#sec:build_sources
buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
gradlePluginPortal()
}
Version Catalogとしてライブラリのバージョンを管理する libs.versions.toml
を読み込んでおきましょう。
buildSrc/settings.gradle.kts
rootProject.name = "buildSrc"
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}
src/main/kotlin
配下には全プロジェクトに展開する定義を書いていきましょう。
地味にファイルの命名規則があるので要注意で {rootプロジェクト名}.xxxxx.gradle.kts
のように、ファイルの先頭にrootプロジェクトのプロジェクト名をつけるのを忘れないでください。
Javaプロジェクトとしての共通定義をここに落としていきましょう。 Gradleプラグインやリポジトリ、コンパイル周りの定義をしていきます。
src/main/kotlin/sandbox-app.java-conventions.gradle.kts
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.kotlin.dsl.eclipse
import org.gradle.kotlin.dsl.idea
import org.gradle.kotlin.dsl.java
import org.gradle.kotlin.dsl.repositories
import org.gradle.kotlin.dsl.withType
plugins {
java
idea
eclipse
}
repositories {
mavenCentral()
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
tasks.withType<Test> {
useJUnitPlatform()
testLogging {
events(
"SKIPPED",
"PASSED",
"FAILED",
"STANDARD_ERROR",
)
exceptionFormat = TestExceptionFormat.FULL
}
}
共通のライブラリの依存関係をここに落とし込んでいきましょう。
LombokやGuavaといった「どのプロジェクトでも使うな」みたいなライブラリの依存関係を定義していきます。
Version Catalogのことを調べると libs.xxx
みたいな書き方しか出てこないのですが、 src/main/kotlin
配下のファイルだとその書き方使えません。これマジで要注意。
ここで面白いのが findLibrary
で libs.versions.toml
の [libraries]
ブロックのキーを引っかけたり、 findBundle
で [bundles]
ブロックのキーを引っかけられるんですが、Kotlinを上手く使うことで書きっぷりを綺麗にまとめることができます。
src/main/kotlin/sandbox-app.java-common-dependencies-conventions.gradle.kts
plugins {
id("melinite.java-conventions")
}
val catalog = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
// Lombok
// => 4行に渡ってartifact:group:versionを書かなくてもよくなる
catalog.findLibrary("lombok").ifPresent {
implementation(it)
annotationProcessor(it)
testImplementation(it)
testAnnotationProcessor(it)
}
// ULID
catalog.findLibrary("ulid").ifPresent {
implementation(it)
}
// Guava
catalog.findLibrary("guava").ifPresent {
implementation(it)
}
// Apache Commons
// => commons-lang3とcommons-ioをバンドルしているので、これだけで2つのライブラリの依存関係が足される
catalog.findBundle("apache.commons").ifPresent {
implementation(it)
}
}
例えばLombokなんかをVersion Catalogを使わずにベタに定義するとこうなると思うんですが、綺麗にまとめられますね。
dependencies {
implementation("org.projectlombok:lombok:1.18.30")
annotationProcessor("org.projectlombok:lombok:1.18.30")
testImplementation("org.projectlombok:lombok:1.18.30")
testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
}
各プロジェクトの build.gradle.kts
Version Catalogを使うことで各プロジェクトの build.gradle.kts
を書く際には libs
から勝手にメソッドが生えてきます。
libs.versions.toml
の [libraries]
ブロックに書いたものは libs.xxx
のように参照できますし、 [plugins]
ブロックに書いたものは libs.plugins.xxx
のように参照できます。
注意点としてはtomlのキーはケバブケースで書くのですが、 build.gradle.kts
ではドット区切りで参照してくことになります。
sandbox-api/build.gradle.kts
import org.gradle.kotlin.dsl.application
plugins {
application
alias(libs.plugins.spring.boot)
alias(libs.plugins.spring.dependency.management)
id("melinite.java-common-dependencies-conventions")
}
dependencies {
implementation(libs.spring.boot.starter.web)
implementation(libs.spring.boot.starter.actuator)
implementation(project(":sandbox-database"))
testImplementation(libs.spring.boot.starter.test)
}
buildscript
ブロックでも同じように libs.xxx
で参照できるのがポイントです。
MyBatis GeneratorやDoma CodeGenといったジェネレーター系のGradleプラグインを使おうとすると、 dependencies
ブロックだけではなく buildscript
ブロックでもDBドライバーをクラスパスに通さないといけないシーンが多々あるので、これはまぁまぁ強力じゃないかなと。
sandbox-database/build.gradle.kts
import org.gradle.kotlin.dsl.`java-library`
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath(libs.mysql)
}
}
plugins {
`java-library`
id("melinite.java-common-dependencies-conventions")
}
dependencies {
implementation(libs.spring.boot.starter.data.jpa)
implementation(libs.mysql)
testImplementation(libs.spring.boot.starter.test)
}
Dependabotとの組み合わせ
Version Catalogが出てきた当初はDependabotが使えなかったんですが、今年の3月にはサポートされるようになっています。
そこでVersion Catalogを使用したGradleプロジェクトにDependabotを組み合わせた際に出てくる良い副産物を挙げていきましょう。
Gradleプラグインのバージョンも更新対象になる
Dependabot自体は前々から使っていたんですが、Gradleプロジェクトにおける1番の不満ポイントだった「Gradleプラグインは更新してくれない」という点を解消してくれます。
build.gradle.kts
plugins {
// こっちはDependabotが反応しない
id("org.springframework.boot") version "3.1.5"
}
dependencies {
// こっちはDependabotが反応する
implementation("org.springframework.boot:spring-boot-starter-web:3.1.5")
}
これがVersion Catalogを使うことでどちらもDependabotが更新対象として認知をしてくれるようになります。
libs.versions.toml
[versions]
spring-boot = "3.1.5"
[libraries]
# シンプルなライブラリの依存関係はDependabotが反応する(これまで通り)
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
[plugins]
# Gradleプラグインの依存関係もDependabotが反応するようになる
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
プルリクエストの数を抑えられる
あとは libs.versions.toml
の [versions]
ブロックでバージョンを共通化しているのでDependabotが作ってくるプルリクの数も抑えられるという利点もあります。
特にマルチプロジェクトな構成で同じライブラリを複数のプロジェクトに依存関係として定義していると、その数だけDependabotはプルリクエストを作ってきます。
つまりこういう定義の仕方をしているとDependabotは問答無用で2つプルリクエストを作ってきます。
projectA/build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:3.1.5")
}
projectB/build.gradle.kts
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web:3.1.5")
}
これがVersion Catalogによって依存関係のあるライブラリを1箇所に集中管理することでプルリクエストが1つになります。
libs.versions.toml
[versions]
# この行の変更分しかプルリクエストを作ってこない
spring-boot = "3.1.5"
[libraries]
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
Dependabotの定義がシンプルになる
Dependabotを使う上で最終的にはここに落ち着くのかなとは思うのですが、何度も言うようにライブラリの依存関係を1箇所に集中させているので、Dependabotの定義がとにかくシンプルになります。
マルチプロジェクト構成の際は dependencies
ブロックのある build.gradle.kts
を全部反応させようと思ったら、それらを全てDependabotに教えてあげないといけませんでした。
.github/dependabot.yml
version: 2
updates:
# build.gradle.ktsの分だけ書かないといけない
- package-ecosystem: gradle
directory: projectA
- package-ecosystem: gradle
directory: projectB
これが libs.versions.toml
の1ファイルに全てまとまっているので、Dependabotには1行分だけでOKです。
.github/dependabot.yml
version: 2
updates:
# rootディレクトリを指定しておけばlibs.versions.tomlを勝手に探してくれる
- package-ecosystem: gradle
directory: /
まとめ
ここ最近の開発においてはOSSのライブラリはなくてはならない存在になっていると思います。
ひと昔前はオレオレフレームワークみたいなのが流行りでしたけど、今となってはただの車輪の再発明でしかないのでただの無駄足にしかなりません。
使えるものは存分に使っていきましょう。
ということで良いVersion Catalogライフを!