Gradle Groovy DSL から Kotlin DSL への乗り換え方

f:id:Naotsugu:20200726205049p:plain


はじめに

Gradle 5.0 よりGradle Kotlin DSL が提供され、groovy によるビルドスクリプトを Kotlin で記載できるようになりました。

Kotlin でビルドスクリプトを書くことで、IDEによるサジェストやコードジャンプなどの恩恵を十分に受けることができますし、タイプセーフでありリファクタリングなども容易となるので、これから Groovy のビルドスクリプトを書くのであれば Gradle Kotlin DSL を使うべきです。

ここでは、Groovy DSL と Kotlin DSL の違いと Kotlin DSL でビルドスクリプトを書くための基礎知識について説明します。


スクリプトファイルの命名

Kotlin DSL を使うには、ビルドスクリプトの拡張子を .gradle から .gradle.kts に変更します。

  • Groovy DSL スクリプトでは build.gradle の拡張子を使用します
  • Kotlin DSL スクリプトでは build.gradle.kts の拡張子を使用します

設定ファイルについても同様で、settings.gradlesettings.gradle.kts のような拡張子とします。

initスクリプトなどについても同様に init.gradleinit.gradle.kts のように定義します。


Kotlin と Groovy の言語上の考慮事項

Kotlin と Groovy の言語上の違いにより以下を考慮する必要があります。

  • Groovy による文字列は 'string' のように1重引用符または二重引用符で指定できますが、Kotlinでは二重引用符"string" が必要になります
  • Groovyでは、関数を呼び出すときに括弧を省略できますが、Kotlin では括弧が必要になります
  • Gradle Groovy DSLでは、プロパティの割り当てに = を省略できますが、Kotlinでは常に代入演算子が必要になります

build.gradle の以下の定義は、

// build.gradle
group 'com.acme'
dependencies {
    implementation 'com.acme:example:1.0'
}

build.gradle.kts のように記載することになります。

// build.gradle.kts
group = "com.acme"
dependencies {
    implementation("com.acme:example:1.0")
}


Gradle init によるプロジェクト生成

gradle init タスクにより Java アプリケーションプロジェクトを Groovy DSL を使う設定とした場合には以下のようなビルドスクリプトが生成されます。

settings.gradle

rootProject.name = 'grs'
include('app')

build.gradle

// build.gradle
plugins {
    id 'application'
}

repositories {
    jcenter()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.2'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
    implementation 'com.google.guava:guava:29.0-jre'
}

application {
    mainClass = 'grs.App'
}

tasks.named('test') {
    useJUnitPlatform()
}


一方 Kotlin DSL を使う設定とした場合には以下のようなビルドスクリプトが生成されます。

settings.gradle.kts

rootProject.name = "kts"
include("app")

build.gradle.kts

// build.gradle.kts
plugins {
    application
}

repositories {
    jcenter()
}

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.2")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
    implementation("com.google.guava:guava:29.0-jre")
}

application {
    mainClass.set("kts.App")
}

tasks.test {
    useJUnitPlatform()
}

プラグインの指定では id 'application'application のようになっています。

また、mainClass = 'grs.App'mainClass.set("kts.App") となっています。

その他はほとんど同等な形で記載できることがわかります。


プラグインの適用

プラグインの適用は plugins {} ブロックで宣言的に記載することができます。

Groovy DSL での以下の記載は、

// build.gradle
plugins {
    id 'java'
    id 'jacoco'
    id 'maven-publish'
    id 'org.springframework.boot' version '2.0.2.RELEASE'
}

Kotlin DSL では以下のように書きます。

// build.gradle.kts
plugins {
    java
    jacoco
    `maven-publish`
    id("org.springframework.boot") version "2.0.2.RELEASE"
}

Kotlin DSL では javajacoco maven-publishといった Gradle コアプラグインはプロパティ拡張が提供されるため、直接指定できます。

Kotlin ではプロパティ名として - を使用できないため maven-publish はバッククオートで囲む必要があります。

サードパーティのプラグインは、Groovy DSL と同じ方法で適用できます(ただし id() のような形でプラグインIDを指定します)。


プラグインの設定

プラグインの拡張設定は Groovy DSL と同様に設定することができます。

例えば jacoco プラグインの設定は以下のようにすることができます。

// build.gradle.kts
plugins {
    jacoco
}

jacoco {
    toolVersion = "0.8.1"
}

プラグインを命令的に apply で適用する場合は以下のように記載します。

// build.gradle.kts
apply(plugin = "checkstyle")

configure<CheckstyleExtension> {
    maxErrors = 10
}

ただし、こちらの書き方は推奨されていません。plugins {} ブロックを使って宣言的にプラグインを適用することでタイプセーフなアクセサーを利用できるようになります。


タスクの構成

build.gradle によるタスクの構成は以下のようになります。

// build.gradle
tasks.jar {
    archiveFileName = 'foo.jar'
}

Kotlin DSL では以下のようになります。

// build.gradle.kts
tasks.jar {
    archiveFileName.set("foo.jar")
}

Groovy DSL と Kotlin DSL では記載内容は同様ですが、その動作は異なっており、Kotlin DSL では configuration avoidance API を利用するものになっています。 これは、対象のタスクが実行されるまで その構成を延期することで、タスクの構成のコストを回避します。ビルドの過程で必要の無いコード品質、テスト、公開タスクといった無関係のタスクの構成を実行しません。

Groovy DSL と同様に、eager な構成を行う場合は以下のようにすることができます。

// build.gradle.kts
tasks.getByName<Jar>("jar") {
    archiveFileName.set("foo.jar")
}


タスク構成の例として Spring Boot の構成は以下のようになります。

// build.gradle.kts
plugins {
    java
    id("org.springframework.boot") version "2.3.4.RELEASE"
}

tasks.bootJar {
    archiveFileName.set("app.jar")
    mainClassName = "com.example.demo.Demo"
}

tasks.bootRun {
    main = "com.example.demo.Demo"
    args("--spring.profiles.active=demo")
}

タスクの構成は以下のように書くこともできます。

// build.gradle.kts
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

plugins {
    java
    id("org.springframework.boot") version "2.3.4.RELEASE"
}

tasks.named<BootJar>("bootJar") {
    archiveFileName.set("app.jar")
    mainClassName = "com.example.demo.Demo"
}

tasks.named<BootRun>("bootRun") {
    main = "com.example.demo.Demo"
    args("--spring.profiles.active=demo")
}

Kotlin はタイプセーフなので、対象の構成の型を明示する必要があります。


タスクの作成

Groovy DSL によるタスクの作成は以下のようになります。

// build.gradle
task greeting {
    doLast { println 'Hello, World!' }
}

Kotlin DSL では以下のように書くことができます。

// build.gradle.kts
task("greeting") {
    doLast { println("Hello, World!") }
}

この場合、eager な構成となり、configuration avoidance は適用されません。

以下のように書いても同様となります。

// build.gradle.kts
tasks.create("greeting") {
    doLast { println("Hello, World!") }
}


型指定されたタスクは以下のようになります。

// build.gradle.kts
tasks.create<Zip>("docZip") {
    archiveFileName.set("doc.zip")
    from("doc")
}

create() ではなく register() とすることで configuration avoidance となり、タスク実行時までその構成が遅延されます。

// build.gradle.kts
tasks.register<Zip>("docZip") {
    archiveFileName.set("doc.zip")
    from("doc")
}


依存の指定

依存関係の宣言は、Groovy DSL と同じように指定できます。

Groovy DSL による以下の宣言は、

// build.gradle
plugins {
    id 'java-library'
}
dependencies {
    implementation 'com.example:lib:1.1'
    runtimeOnly 'com.example:runtime:1.0'
    testImplementation('com.example:test-support:1.3') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'com.example:test-junit-jupiter-runtime:1.3'
}

Kotlin DSL では以下のようになります。

// build.gradle.kts
plugins {
    `java-library`
}
dependencies {
    implementation("com.example:lib:1.1")
    runtimeOnly("com.example:runtime:1.0")
    testImplementation("com.example:test-support:1.3") {
        exclude(module = "junit")
    }
    testRuntimeOnly("com.example:test-junit-jupiter-runtime:1.3")
}


依存のカスタム構成を使う場合、Groovy DSL の以下の宣言は、

// build.gradle

configurations {
    db
    integTestImplementation {
        extendsFrom testImplementation
    }
}

dependencies {
    db 'org.postgresql:postgresql'
    integTestImplementation 'com.example:integ-test-support:1.3'
}

Kotlin DSL では以下のようになります。

// build.gradle.kts
val db by configurations.creating
val integTestImplementation by configurations.creating {
    extendsFrom(configurations["testImplementation"])
}

dependencies {
    db("org.postgresql:postgresql")
    integTestImplementation("com.example:integ-test-support:1.3")
}

ただし、上記設定を行った場合、dependencies { } ブロックでは、dbintegTestImplementation しか使えなくなります。

他の構成と合わせて利用する場合は、configurations により委譲プロパティを定義します。

// build.gradle.kts
val testRuntimeOnly by configurations
val db by configurations.creating
val integTestImplementation by configurations.creating {
    extendsFrom(configurations["testImplementation"])
}

dependencies {
    testRuntimeOnly("com.example:test-junit-jupiter-runtime:1.3")
    "db"("org.postgresql:postgresql")
    "integTestImplementation"("com.example:integ-test-support:1.3")
}

または文字列リテラルとして指定します。

dependencies {
    "db"("org.postgresql:postgresql")
    "integTestImplementation"("com.example:integ-test-support:1.3")
}


よくある設定

Java バージョン指定

java {                                      
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

文字エンコーディング指定

tasks {
    withType<JavaCompile> {
        options.encoding = Charsets.UTF_8.name()
    }
    // ...
}