本节将指导您如何将基于 Groovy 的 Gradle 构建脚本转换为 Kotlin。

Gradle 较新的 Kotlin DSL 在受支持的 IDE 中提供了愉快的编辑体验:内容辅助、重构、文档等。

IntelliJ IDEA and Android Studio

另请阅读Gradle Kotlin DSL 入门,了解 Gradle Kotlin DSL 的特性、限制和用法。

用户手册的其余部分包含构建脚本片段,演示了 Groovy DSL 和 Kotlin DSL。这是查找如何使用每种 DSL 以及了解其用途的最佳地点;它涵盖了所有 Gradle 功能,从使用插件自定义依赖项解析行为

开始迁移之前

请阅读:迁移之前理解以下重要信息会有帮助

  • 使用最新版本的 Gradle、应用的插件和您的 IDE 应该是您的第一步。

  • Kotlin DSL 在 Intellij IDEA 和 Android Studio 中得到全面支持。其他 IDE,例如 Eclipse 或 NetBeans,目前尚未提供用于编辑 Gradle Kotlin DSL 文件的实用工具,但导入基于 Kotlin DSL 的构建并与其一起工作是正常的。

  • 在 IntelliJ IDEA 中,您必须从 Gradle 模型导入项目,以获得 Kotlin DSL 脚本的内容辅助和重构工具。

  • 在某些情况下,Kotlin DSL 会较慢。例如,首次使用、干净检出或临时 CI Agent 已知会较慢。这同样适用于 buildSrc 目录中的某些内容发生更改,从而使构建脚本缓存失效的场景。配置时间较慢的构建可能会影响 IDE 的响应速度,请查阅有关 Gradle 性能的文档

  • 您必须使用 Java 8 或更高版本运行 Gradle。不支持 Java 7。

  • 嵌入式 Kotlin 编译器已知可在 x86-64 架构的 Linux、macOS、Windows、Cygwin、FreeBSD 和 Solaris 上工作。

  • 了解 Kotlin 语法和基本语言特性非常有帮助。Kotlin 参考文档Kotlin Koans应该对您有所帮助。

  • 使用 plugins {} 块来声明 Gradle 插件可以显著改善编辑体验,并且强烈推荐。在将 Groovy 构建脚本转换为 Kotlin 之前,考虑在其中采用它。

  • Kotlin DSL 将不支持 model {} 元素。这是已停用的 Gradle 软件模型的一部分。

  • 不建议启用孵化中的按需配置功能,因为它可能导致非常难以诊断的问题。

更多信息请阅读Gradle Kotlin DSL 入门

如果您遇到问题或疑似 bug,请利用 gradle/gradle 问题跟踪器

您无需一次性全部迁移!基于 Groovy 和基于 Kotlin 的构建脚本都可以 apply 其他语言的脚本。您可以在Kotlin DSL 示例中找到未涵盖的任何 Gradle 功能的启发。

准备 Groovy 脚本

Kotlin 和 Groovy 之间一些简单的语言差异可能会使转换脚本变得繁琐

  • Groovy 字符串可以使用单引号 'string' 或双引号 "string" 引起来,而 Kotlin 要求使用双引号 "string"

  • Groovy 允许在调用函数时省略括号,而 Kotlin 总是要求使用括号。

  • Gradle Groovy DSL 允许在赋值属性时省略 = 赋值运算符,而 Kotlin 总是要求使用赋值运算符。

作为第一步迁移,建议通过以下方式准备您的 Groovy 构建脚本

  • 统一引号,使用双引号,

  • 明确函数调用和属性赋值(分别使用括号和赋值运算符)。

前者可以通过搜索 ' 并替换为 " 轻松完成。例如,

group = 'com.acme'
dependencies {
    implementation 'com.acme:example:1.0'
}

变为

group "com.acme"
dependencies {
    implementation "com.acme:example:1.0"
}

下一步会稍微复杂一些,因为在 Groovy 脚本中区分函数调用和属性赋值可能并非易事。一个好的策略是首先将所有模糊的语句视为属性赋值,然后通过将失败的语句改为函数调用来修复构建。

例如,

group "com.acme"
dependencies {
    implementation "com.acme:example:1.0"
}

变为

group = "com.acme"                          (1)
dependencies {
    implementation("com.acme:example:1.0")  (2)
}
1 属性赋值
2 函数调用

在保持 Groovy 有效语法的同时,它现在变得明确并且接近 Kotlin 语法,从而更容易将脚本重命名为 Gradle Kotlin DSL 脚本。

需要注意的是,虽然 Groovy 的 extra 属性可以使用对象的 ext 属性修改,但在 Kotlin 中,它们使用 extra 属性修改。查看每个对象并相应地更新构建脚本非常重要。

您可以在用户指南中找到一个示例。

脚本文件命名

Groovy DSL 脚本文件使用 .gradle 文件扩展名。Kotlin DSL 脚本文件使用 .gradle.kts 文件扩展名。

要使用 Kotlin DSL,只需将文件命名为 build.gradle.kts 而不是 build.gradle

Settings 文件,即 settings.gradle,也可以重命名为 settings.gradle.kts

在多项目构建中,您可以使用 Groovy DSL(使用 build.gradle)的模块,也可以使用 Kotlin DSL(使用 build.gradle.kts)的模块。

除此之外,应用以下约定以获得更好的 IDE 支持

  • 根据模式 *.settings.gradle.kts 命名应用于 Settings 的脚本,

  • 根据模式 *.init.gradle.kts 命名初始化脚本

应用插件

与 Groovy DSL 一样,有两种方法可以应用 Gradle 插件

这是一个使用声明式 plugins {} 块的示例

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

Kotlin DSL 为所有Gradle 核心插件提供了属性扩展,如上所示通过 javajacocomaven-publish 声明。

第三方插件可以像 Groovy DSL 一样应用。除了双引号和括号。您也可以使用这种风格应用核心插件。但是,推荐使用静态类型访问器,因为它们是类型安全的,并且将由您的 IDE 自动完成。

您也可以使用命令式的 apply 语法,但此时非核心插件必须包含在构建脚本的类路径中

build.gradle.kts
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:2.7.8")
    }
}

apply(plugin = "java")
apply(plugin = "jacoco")
apply(plugin = "org.springframework.boot")
build.gradle
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath('org.springframework.boot:spring-boot-gradle-plugin:2.7.8')
    }
}

apply plugin: 'java'
apply plugin: 'jacoco'
apply plugin: 'org.springframework.boot'

我们强烈建议您优先使用 plugins {} 块而不是 apply() 函数。

plugins {} 块的声明性质使 Kotlin DSL 能够为应用插件贡献的扩展、配置和其他功能提供类型安全的访问器,这使得 IDE 易于发现插件模型的详细信息并易于配置它们。
有关更多信息,请参阅 Gradle 用户手册中plugins {} 块的文档

配置插件

许多插件都带有用于配置它们的扩展。如果这些插件使用声明式 plugins {} 块应用,则会提供 Kotlin 扩展函数来配置它们的扩展,其方式与 Groovy 中相同。以下示例展示了 Jacoco 插件的工作原理。

build.gradle.kts
plugins {
    jacoco
}

jacoco {
    toolVersion = "0.8.1"
}
build.gradle
plugins {
    id 'jacoco'
}

jacoco {
    toolVersion = '0.8.1'
}

相比之下,如果您使用命令式 apply() 函数应用插件,则必须使用 configure<T>() 函数来配置该插件。以下示例通过在 configure<T>() 函数中显式声明插件的扩展类(即 CheckstyleExtension)来展示 Checkstyle 插件的工作原理。

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

configure<CheckstyleExtension> {
    maxErrors = 10
}
build.gradle
apply plugin: "checkstyle"

checkstyle {
    maxErrors = 10
}

再次强调,我们强烈建议您通过 plugins {} 块声明式地应用插件。

了解插件提供的扩展有哪些

因为您的 IDE 知道插件提供的配置元素,所以在您请求 IDE 建议时,它会包含这些元素。这既会在构建脚本的顶级发生(大多数插件扩展都添加到 Project 对象),也会在扩展的配置块内部发生。

您还可以运行 :kotlinDslAccessorsReport Task,了解所有已应用插件贡献的扩展。它会打印出用于访问这些扩展的 Kotlin 代码,并提供访问器方法的名称和类型。

如果您想配置的插件在其方法签名中依赖于 groovy.lang.Closure 或使用其他动态 Groovy 语义,则需要更多工作才能从 Kotlin DSL 构建脚本配置该插件。有关如何从 Kotlin 代码调用 Groovy 代码或将该插件的配置保留在 Groovy 脚本中,请参阅Gradle Kotlin DSL 文档的互操作性部分

插件也贡献 Task,您可能希望直接配置它们。此主题在下面的配置 Task 部分中介绍。

保持构建脚本的声明性

要获得 Gradle Kotlin DSL 的最大优势,您应该努力使您的构建脚本保持声明性。这里要记住的主要一点是,为了获得类型安全的访问器,必须在构建脚本主体之前应用插件。

强烈建议您阅读 Gradle 用户手册中关于使用 Gradle Kotlin DSL 配置插件的内容。

如果您的构建是多项目构建,例如绝大多数 Android 构建,请同时阅读关于多项目构建的后续部分。

最后,有一些策略可以plugins {} 块与未发布正确元数据的插件一起使用,例如 Android Gradle 插件。

配置规避

Gradle 4.9 引入了一个新的 API,用于在构建脚本和插件中创建和配置 Task。其目的是让这个新 API 最终取代现有 API。

现有 Gradle Task API 与新 API 之间的主要区别之一在于 Gradle 是否会花费时间创建 Task 实例并运行配置代码。新 API 允许 Gradle 延迟或完全避免配置在构建中永远不会执行的 Task。例如,在编译代码时,Gradle 不需要配置运行测试的 Task。

有关更多信息,请参阅博客文章演进 Gradle API 以减少配置时间以及用户手册中的Task 配置规避章节。

Gradle Kotlin DSL 通过使类型安全的模型访问器利用新 API 并提供 DSL 构造来使其更易于使用,从而拥抱配置规避。请放心,整个 Gradle API 仍然可用。

配置 Task

配置 Task 的语法是 Groovy 和 Kotlin DSL 开始显著不同的地方。

build.gradle.kts
tasks.jar {
    archiveFileName = "foo.jar"
}
build.gradle
tasks.jar {
    archiveFileName = 'foo.jar'
}

注意,在 Kotlin 中,tasks.jar {} 表示法利用了配置规避 API 并延迟了 jar Task 的配置。

如果类型安全的 Task 访问器 tasks.jar 不可用(请参阅上面配置插件部分),您可以回退到使用 tasks 容器 API。以下示例的 Kotlin 版本与上面使用类型安全访问器的版本严格等效

build.gradle.kts
tasks.named<Jar>("jar") {
    archiveFileName = "foo.jar"
}
build.gradle
tasks.named('jar') {
    archiveFileName = 'foo.jar'
}

请注意,由于 Kotlin 是一种静态类型语言,因此需要显式指定 Task 的类型。否则,脚本将无法编译,因为推断的类型将是 Task 而不是 Jar,并且 archiveName 属性特定于 Jar Task 类型。

如果配置规避在迁移过程中阻碍了您,并且您想像 Groovy 一样急切地配置 Task,可以通过在 tasks 容器上使用急切配置 API 来实现

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

Gradle Kotlin DSL 中容器的使用在此处详细记录

了解 Task 的类型

如果您不知道 Task 是什么类型,可以通过内置的 help Task 找到该信息。只需使用 --task 选项传递您感兴趣的 Task 名称,如下所示

❯ ./gradlew help --task jar
...
Type
     Jar (org.gradle.api.tasks.bundling.Jar)

让我们通过一个快速的工作示例将所有这些整合起来,该示例配置 Spring Boot 项目的 bootJarbootRun Task

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

tasks.bootJar {
    archiveFileName = "app.jar"
    mainClass = "com.example.demo.Demo"
}

tasks.bootRun {
    mainClass = "com.example.demo.Demo"
    args("--spring.profiles.active=demo")
}
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.8'
}

tasks.bootJar {
    archiveFileName = 'app.jar'
    mainClass = 'com.example.demo.Demo'
}

tasks.bootRun {
    mainClass = 'com.example.demo.Demo'
    args '--spring.profiles.active=demo'
}

这非常容易理解。主要区别在于使用 Kotlin DSL 访问器时,Task 配置会自动变为延迟配置。

现在,为了示例起见,让我们看看使用 API 而不是类型安全访问器应用相同的配置,类型安全访问器可能因构建逻辑结构而不可用,有关更多信息,请参阅 Gradle 用户手册中相应的文档

我们首先通过 help Task 确定 bootJarbootRun Task 的类型

❯ ./gradlew help --task bootJar
...
Type
     BootJar (org.springframework.boot.gradle.tasks.bundling.BootJar)
❯ ./gradlew help --task bootRun
...
Type
     BootRun (org.springframework.boot.gradle.tasks.run.BootRun)

现在我们知道这两个 Task 的类型了,我们可以导入相关的类型(即 BootJarBootRun),并根据需要配置 Task。请注意,IDE 可以帮助我们完成所需的导入,因此我们只需要简单的名称,即不带完整包名。以下是最终的构建脚本,包含导入内容

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

// TODO:Finalize Upload Removal - Issue #21439
plugins {
    java
    id("org.springframework.boot") version "2.7.8"
}

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

tasks.named<BootRun>("bootRun") {
    mainClass = "com.example.demo.Demo"
    args("--spring.profiles.active=demo")
}
build.gradle
plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.8'
}

tasks.named('bootJar') {
    archiveFileName = 'app.jar'
    mainClass = 'com.example.demo.Demo'
}

tasks.named('bootRun') {
    mainClass = 'com.example.demo.Demo'
    args '--spring.profiles.active=demo'
}

创建 Task

可以使用名为 task(…​) 的脚本顶级函数来创建 Task

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

请注意,上述示例使用 Groovy 和 Kotlin DSL 都会急切地配置创建的 Task。

也可以在 tasks 容器上注册或创建 Task,分别使用 register(…​)create(…​) 函数,如下所示

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

上面的示例创建了无类型的临时 Task,但您更常见地希望创建特定类型的 Task。这也可以使用相同的 register()create() 方法完成。这是一个创建类型为 Zip 的新 Task 的示例

build.gradle.kts
tasks.register<Zip>("docZip") {
    archiveFileName = "doc.zip"
    from("doc")
}
build.gradle
tasks.register('docZip', Zip) {
    archiveFileName = 'doc.zip'
    from 'doc'
}
build.gradle.kts
tasks.create<Zip>("docZip") {
    archiveFileName = "doc.zip"
    from("doc")
}
build.gradle
tasks.create(name: 'docZip', type: Zip) {
    archiveFileName = 'doc.zip'
    from 'doc'
}

配置和依赖项

在现有配置中声明依赖项与在 Groovy 构建脚本中的方式类似,您可以在此示例中看到

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")
}
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'
}

应用插件贡献的每个配置也可以作为 configurations 容器的成员使用,因此您可以像引用任何其他配置一样引用它。

了解有哪些配置可用

了解有哪些配置可用的最简单方法是在 configurations 容器中请求 IDE 建议。

您还可以使用 :kotlinDslAccessorsReport Task,它会打印用于访问已应用插件贡献的配置的 Kotlin 代码,并提供所有这些访问器的名称。

请注意,如果您不使用 plugins {} 块来应用插件,则将无法以通常的方式配置这些插件提供的依赖项配置。相反,您必须使用字符串字面量作为配置名称,这意味着您将无法获得 IDE 支持

build.gradle.kts
apply(plugin = "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")
}
build.gradle
apply plugin: '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'
}

这只是又一个理由说明为什么您应该尽可能使用 plugins {} 块!

自定义配置和依赖项

有时您需要创建自己的配置并向其附加依赖项。以下示例声明了两个新配置

  • db,我们向其添加一个 PostgreSQL 依赖项

  • integTestImplementation,它配置为扩展 testImplementation 配置,并向其添加不同的依赖项

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")
}
build.gradle
configurations {
    db
    integTestImplementation {
        extendsFrom testImplementation
    }
}

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

请注意,在上面的示例中,我们只能在 dependencies {} 块内使用 db(…​)integTestImplementation(…​) 表示法,因为这两个配置都事先通过 creating() 方法声明为委托属性。如果配置在其他地方定义,您只能通过首先通过 configurations 创建委托属性(与 configurations.creating() 不同),或者在 dependencies {} 块内使用字符串字面量来引用它们。以下示例演示了这两种方法

build.gradle.kts
// get the existing 'testRuntimeOnly' configuration
val testRuntimeOnly by configurations

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

迁移策略

如上所述,使用 Kotlin DSL 的脚本和使用 Groovy DSL 的脚本可以参与同一个构建。此外,来自 buildSrc 目录、包含构建或外部位置的 Gradle 插件可以使用任何 JVM 语言实现。这使得逐步迁移构建成为可能,而不会阻塞您的团队。

两种迁移方法比较突出

  • 将现有构建语法一点一点地迁移到 Kotlin,同时保留结构——我们称之为机械迁移

  • 将您的构建逻辑按照 Gradle 最佳实践进行重构,并将切换到 Kotlin DSL 作为这项工作的一部分

这两种方法都可行。对于简单的构建,机械迁移就足够了。复杂且高度动态的构建可能无论如何都需要进行一些重构,因此在这种情况下,重新实现构建逻辑以遵循 Gradle 最佳实践是有意义的。

由于应用 Gradle 最佳实践将使您的构建更易于使用和更快,我们建议您最终以这种方式迁移所有项目,但专注于首先需要重构的项目以及最能从改进中获益的项目是明智的。

另请考虑,构建逻辑中越依赖于 Groovy 动态特性的部分,就越难以从 Kotlin DSL 中使用。无论动态 Groovy 构建逻辑位于何处,您都可以在Gradle Kotlin DSL 文档的互操作性部分找到如何从静态 Kotlin 跨越动态边界的方法。

有两项关键最佳实践可以更容易地在 Kotlin DSL 的静态上下文中使用

  • 使用 plugins {}

  • 将本地构建逻辑放入构建的 buildSrc 目录

plugins {}是为了保持构建脚本的声明性,以便充分利用 Kotlin DSL。

利用buildSrc 项目是将您的构建逻辑组织成易于测试并提供良好 IDE 支持的共享本地插件和约定。

Kotlin DSL 构建结构示例

根据您的构建结构,您可能对以下用户手册章节感兴趣

互操作性

在您的构建逻辑中混合使用语言时,您可能需要跨越语言边界。一个极端的例子是使用用 Java、Groovy 和 Kotlin 实现的 Task 和插件,同时还使用 Kotlin DSL 和 Groovy DSL 构建脚本的构建。

引用 Kotlin 参考文档

Kotlin 的设计考虑了 Java 互操作性。现有的 Java 代码可以自然地从 Kotlin 调用,并且 Kotlin 代码也可以相当流畅地从 Java 使用。

关于从 Kotlin 调用 Java从 Java 调用 Kotlin 的内容在 Kotlin 参考文档中都有很好的介绍。

这在很大程度上也适用于与 Groovy 代码的互操作性。此外,Kotlin DSL 提供了几种方法来选择性地使用 Groovy 语义。

关于 Gradle Kotlin DSL 和互操作性