Gradle 的 Kotlin DSL 提供了传统 Groovy DSL 的替代方案,在受支持的 IDE 中提供增强的编辑体验,包括更好的内容辅助、重构和文档等功能。

本章探讨了关键的 Kotlin DSL 结构,并演示了如何使用它们与 Gradle API 交互。

如果您有兴趣将现有 Gradle 构建迁移到 Kotlin DSL,请参阅专门的迁移页面

先决条件

  • 嵌入式 Kotlin 编译器适用于 Linux、macOS、Windows、Cygwin、FreeBSD 和 Solaris 的 x86-64 架构。

  • 建议熟悉 Kotlin 语法和基本语言特性。请参阅Kotlin 文档Kotlin Koans以学习基础知识。

  • 强烈建议使用plugins {}块声明 Gradle 插件,因为它能显著改善编辑体验。

IDE 支持

Kotlin DSL 完全受 IntelliJ IDEA 和 Android Studio 支持。虽然其他 IDE 缺乏编辑 Kotlin DSL 文件的高级工具,但您仍然可以导入基于 Kotlin-DSL 的构建并像往常一样使用它们。

构建导入 语法高亮 1 语义编辑器 2

IntelliJ IDEA

Android Studio

Eclipse IDE

CLion

Apache NetBeans

Visual Studio Code (LSP)

Visual Studio

1 Gradle Kotlin DSL 脚本中的 Kotlin 语法 高亮
2 Gradle Kotlin DSL 脚本中的 代码 补全、 跳转 源、 文档、 重构 等…​

如限制中所述,您必须使用 Gradle 模型导入您的项目,才能在 IntelliJ IDEA 中为 Kotlin DSL 脚本启用内容辅助和重构工具。

配置时间较长的构建可能会影响 IDE 的响应速度,因此请查看性能部分以帮助解决此类问题。

自动构建导入与脚本依赖的自动重新加载

IntelliJ IDEA 和 Android Studio 都会在您更改构建逻辑时检测到并提供两个建议

  1. 再次导入整个构建

    IntelliJ IDEA
    IntelliJ IDEA
  2. 编辑构建脚本时重新加载脚本依赖项

    Reload script dependencies

我们建议禁用自动构建导入,同时启用脚本依赖项的自动重新加载。这种方法在编辑 Gradle 脚本时提供早期反馈,同时让您可以控制何时将整个构建设置与 IDE 同步。

请参阅故障排除部分了解更多信息。

Kotlin DSL 脚本

与基于 Groovy 的对应物一样,Kotlin DSL 基于 Gradle 的 Java API 构建。Kotlin DSL 脚本中的所有内容都是 Kotlin 代码,由 Gradle 编译和执行。构建脚本中的许多对象、函数和属性都来自 Gradle API 和已应用插件的 API。

使用Kotlin DSL 参考搜索可用成员。

脚本文件名

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

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

要激活 Kotlin DSL,请为您的构建脚本使用 .gradle.kts 扩展名而不是 .gradle。这也适用于设置文件(例如,settings.gradle.kts)和初始化脚本

您可以在同一个构建中混合使用 Groovy DSL 和 Kotlin DSL 脚本。例如,Kotlin DSL 构建脚本可以应用 Groovy DSL 脚本,多项目构建中的不同项目可以使用其中任何一个。

为了改进 IDE 支持,我们建议遵循以下约定

  • 使用模式 *.settings.gradle.kts 命名设置脚本(或任何由 Gradle Settings 对象支持的脚本)。这包括从设置脚本应用的脚本插件。

  • 使用模式 *.init.gradle.kts 或简单地 init.gradle.kts 命名初始化脚本

这有助于 IDE 识别脚本“支持”的对象,无论是 ProjectSettings 还是 Gradle

隐式导入

所有 Kotlin DSL 构建脚本都附带隐式导入,包括

  • 默认的 Gradle API 导入

  • Kotlin DSL API,其中包括来自以下包的类型

    • org.gradle.kotlin.dsl

    • org.gradle.kotlin.dsl.plugins.dsl

    • org.gradle.kotlin.dsl.precompile

    • java.util.concurrent.Callable

    • java.util.concurrent.TimeUnit

    • java.math.BigDecimal

    • java.math.BigInteger

    • java.io.File

    • javax.inject.Inject

避免使用内部 Kotlin DSL API

在插件和构建脚本中使用内部 Kotlin DSL API 可能会在 Gradle 或插件更新时导致构建中断。

Kotlin DSL API 通过上述包(但不在其子包中)中列出的相应 API 文档中的类型扩展了公共 Gradle API。

编译警告

Gradle Kotlin DSL 脚本在构建的配置阶段由 Gradle 编译。

Kotlin 编译器发现的弃用警告在编译脚本时会在控制台报告

> Configure project :
w: build.gradle.kts:4:5: 'getter for uploadTaskName: String!' is deprecated. Deprecated in Java

可以通过将 org.gradle.kotlin.dsl.allWarningsAsErrors Gradle 属性设置为 true 来配置构建以在脚本编译期间发出的任何警告时失败

gradle.properties
org.gradle.kotlin.dsl.allWarningsAsErrors=true

类型安全模型访问器

Groovy DSL 允许您按名称引用许多构建模型元素,即使它们是在运行时定义的,例如命名配置或源集。

例如,当应用 Java 插件时,您可以通过 configurations.implementation 访问 implementation 配置。

Kotlin DSL 用类型安全模型访问器取代了这种动态解析,这些访问器与插件贡献的模型元素一起工作。

理解类型安全模型访问器何时可用

Kotlin DSL 当前提供各种类型安全模型访问器集,每个都适用于不同的范围。

对于主项目构建脚本和预编译项目脚本插件

类型安全模型访问器 示例

依赖项和构件配置

implementationruntimeOnly(由 Java 插件贡献)

项目扩展和约定,以及其上的扩展

sourceSets

dependenciesrepositories 容器上的扩展,以及其上的扩展

testImplementation(由 Java 插件贡献)、mavenCentral

tasksconfigurations 容器中的元素

compileJava(由 Java 插件贡献)、test

项目扩展容器中的元素

由 Java 插件贡献并添加到 sourceSets 容器的源集:sourceSets.main.java { setSrcDirs(listOf("src/main/java")) }

对于主项目设置脚本和预编译设置脚本插件

类型安全模型访问器 示例

Settings 插件贡献的项目扩展和约定,以及其上的扩展

pluginManagementdependencyResolutionManagement

初始化脚本和脚本插件不具有类型安全模型访问器。这些限制将在未来的 Gradle 版本中移除。

可用的类型安全模型访问器集在评估脚本主体之前,紧接着 plugins {} 块之后确定。此后贡献的模型元素,例如在构建脚本中定义的配置,将不适用于类型安全模型访问器

build.gradle.kts
// Applies the Java plugin
plugins {
    id("java")
}

repositories {
    mavenCentral()
}

// Access to 'implementation' (contributed by the Java plugin) works here:
dependencies {
    implementation("org.apache.commons:commons-lang3:3.12.0")
    testImplementation("org.junit.jupiter:junit-jupiter:5.10.0")
    testRuntimeOnly("org.junit.platform:junit-platform-launcher") // Add this if needed for runtime
}

// Add a custom configuration
configurations.create("customConfiguration")
// Type-safe accessors for 'customConfiguration' will NOT be available because it was created after the plugins block
dependencies {
    customConfiguration("com.google.guava:guava:32.1.2-jre") // ❌ Error: No type-safe accessor for 'customConfiguration'
}

然而,这意味着您可以对由父项目应用的插件贡献的任何模型元素使用类型安全访问器。

以下项目构建脚本演示了如何使用类型安全访问器访问各种配置、扩展和其他元素

build.gradle.kts
plugins {
    `java-library`
}

dependencies {                              (1)
    api("junit:junit:4.13")
    implementation("junit:junit:4.13")
    testImplementation("junit:junit:4.13")
}

configurations {                            (1)
    implementation {
        resolutionStrategy.failOnVersionConflict()
    }
}

sourceSets {                                (2)
    main {                                  (3)
        java.srcDir("src/core/java")
    }
}

java {                                      (4)
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

tasks {
    test {                                  (5)
        testLogging.showExceptions = true
        useJUnit()
    }
}
1 使用由 Java 库插件贡献的 apiimplementationtestImplementation 依赖配置的类型安全访问器
2 使用访问器配置 sourceSets 项目扩展
3 使用访问器配置 main 源集
4 使用访问器配置 main 源集的 java
5 使用访问器配置 test 任务

您的 IDE 知道类型安全访问器,并会将它们包含在建议中。

这适用于构建脚本的顶层(大多数插件扩展都添加到 Project 对象中),以及配置扩展的块内。

请注意,对于 configurationstaskssourceSets 等容器元素的访问器利用了 Gradle 的配置规避 API。例如,在 tasks 上,访问器的类型是 TaskProvider<T>,并提供底层任务的惰性引用和惰性配置。

以下是一些说明配置规避何时适用的示例

build.gradle.kts
tasks.test {
    // lazy configuration
    useJUnitPlatform()
}

// Lazy reference
val testProvider: TaskProvider<Test> = tasks.test

testProvider {
    // lazy configuration
}

// Eagerly realized Test task, defeats configuration avoidance if done out of a lazy context
val test: Test = tasks.test.get()

对于所有其他容器,元素访问器的类型为 NamedDomainObjectProvider<T>,提供相同的行为

build.gradle.kts
val mainSourceSetProvider: NamedDomainObjectProvider<SourceSet> = sourceSets.named("main")

了解类型安全模型访问器不可用时该怎么办

考虑上面所示的示例构建脚本,它演示了类型安全访问器的使用。以下示例是相同的,只是它使用 apply() 方法应用插件。

在这种情况下,构建脚本无法使用类型安全访问器,因为 apply() 调用发生在构建脚本的主体中。您必须改用其他技术,如下所示

build.gradle.kts
apply(plugin = "java-library")

dependencies {
    "api"("junit:junit:4.13")
    "implementation"("junit:junit:4.13")
    "testImplementation"("junit:junit:4.13")
}

configurations {
    "implementation" {
        resolutionStrategy.failOnVersionConflict()
    }
}

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

configure<JavaPluginExtension> {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

tasks {
    named<Test>("test") {
        testLogging.showExceptions = true
    }
}

对于以下贡献的模型元素,类型安全访问器不可用

  • 通过 apply(plugin = "id") 方法应用的插件。

  • 项目构建脚本。

  • 脚本插件,通过 apply(from = "script-plugin.gradle.kts")

  • 通过跨项目配置应用的插件。

您不能在用 Kotlin 实现的二进制 Gradle 插件中使用类型安全访问器。

如果找不到类型安全访问器,请回退到使用相应类型的普通 API。为此,您需要知道所配置模型元素的名称和/或类型。我们现在将向您展示如何通过详细检查脚本来发现这些信息。

构件配置

以下示例演示了如何在没有类型安全访问器的情况下引用和配置构件配置

build.gradle.kts
apply(plugin = "java-library")

dependencies {
    "api"("junit:junit:4.13")
    "implementation"("junit:junit:4.13")
    "testImplementation"("junit:junit:4.13")
}

configurations {
    "implementation" {
        resolutionStrategy.failOnVersionConflict()
    }
}

代码看起来与类型安全访问器相似,只是配置名称是字符串字面量。您可以在依赖项声明和 configurations {} 块中使用字符串字面量作为配置名称。

虽然 IDE 无法帮助您发现可用配置,但您可以在相应插件的文档中查找它们,或者通过运行 ./gradlew dependencies

项目扩展

项目扩展既有名称也有唯一的类型。但是,Kotlin DSL 只需要知道类型即可配置它们。

以下示例显示了原始示例构建脚本中的 sourceSets {}java {} 块。 configure<T>() 函数与相应类型一起使用

build.gradle.kts
apply(plugin = "java-library")

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

configure<JavaPluginExtension> {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

请注意,sourceSetsProject 上的 Gradle 扩展,类型为 SourceSetContainer,而 javaProject 上的扩展,类型为 JavaPluginExtension

您可以通过查看已应用插件的文档或运行 ./gradlew kotlinDslAccessorsReport 来发现可用的扩展。该报告生成访问已应用插件贡献的模型元素所需的 Kotlin 代码,提供名称和类型。

作为最后手段,您可以检查插件的源代码,尽管在大多数情况下这应该是不必要的。

如果您只需要引用扩展而无需配置它,或者您想进行一行配置,您还可以使用 the<T>() 函数

build.gradle.kts
the<SourceSetContainer>()["main"].java.srcDir("src/main/java")

上面的代码片段还演示了配置作为容器的项目扩展元素的一种方法。

项目扩展容器中的元素

基于容器的项目扩展,例如 SourceSetContainer,允许您配置它们所持有的元素。

在我们的示例构建脚本中,我们希望在源集容器中配置一个名为 main 的源集。我们可以通过使用 named() 方法而不是访问器来实现这一点

build.gradle.kts
apply(plugin = "java-library")

configure<SourceSetContainer> {
    named("main") {
        java.srcDir("src/core/java")
    }
}

基于容器的项目扩展中的所有元素都有一个名称,因此您可以在所有此类情况下使用此技术。

对于项目扩展和约定,您可以通过检查已应用插件的文档或运行 ./gradlew kotlinDslAccessorsReport 来发现任何容器中存在的元素。

作为最后手段,您还可以查看插件的源代码以找出其作用。

任务

任务不是通过基于容器的项目扩展来管理的,但它们是行为相似的容器的一部分。

这意味着您可以像配置源集一样配置任务。以下示例说明了这种方法

build.gradle.kts
apply(plugin = "java-library")

tasks {
    named<Test>("test") {
        testLogging.showExceptions = true
    }
}

我们使用 Gradle API 按名称和类型引用任务,而不是使用访问器。

请注意,需要显式指定任务的类型。如果您不这样做,脚本将无法编译,因为推断的类型将是 Task,而不是 Test,并且 testLogging 属性是 Test 任务类型特有的。

但是,如果您只需要配置所有任务通用的属性或调用所有任务通用的方法(即在 Task 接口上声明的那些),则可以省略类型。

您可以通过运行 ./gradlew tasks 来发现可用的任务。

要找出给定任务的类型,请运行 ./gradlew help --task <taskName>,如下所示

❯ ./gradlew help --task test
...
Type
     Test (org.gradle.api.tasks.testing.Test)

IDE 可以协助您进行所需的导入,因此您只需要类型的简单名称,而无需包名部分。在这种情况下,无需导入 Test 任务类型,因为它属于 Gradle API,因此是隐式导入的。

使用容器对象

Gradle 构建模型广泛使用容器对象(或简称“容器”)。

例如,configurationstasks 是分别持有 ConfigurationTask 对象的容器。社区插件也贡献容器,例如 Android 插件贡献的 android.buildTypes 容器。

Kotlin DSL 为构建作者提供了多种与容器交互的方式。我们将以 tasks 容器为例,探讨这些方法中的每一种。

在配置受支持容器上的现有元素时,您可以利用另一节中描述的类型安全访问器。该节还解释了哪些容器支持类型安全访问器。

使用容器 API

Gradle 中的所有容器都实现了 NamedDomainObjectContainer<DomainObjectType>。有些容器可以持有不同类型的对象,并实现了 PolymorphicDomainObjectContainer<BaseType>。与容器交互的最简单方法是通过这些接口。

以下示例演示了如何使用 named() 方法配置现有任务,以及使用 register() 方法创建新任务

build.gradle.kts
tasks.named("check")                    (1)
tasks.register("myTask1")               (2)

tasks.named<JavaCompile>("compileJava") (3)
tasks.register<Copy>("myCopy1")         (4)

tasks.named("assemble") {               (5)
    dependsOn(":myTask1")
}
tasks.register("myTask2") {             (6)
    description = "Some meaningful words"
}

tasks.named<Test>("test") {             (7)
    testLogging.showStackTraces = true
}
tasks.register<Copy>("myCopy2") {       (8)
    from("source")
    into("destination")
}
1 获取名为 check 的现有任务的 Task 类型引用
2 注册一个名为 myTask1 的新的无类型任务
3 获取名为 compileJava 的现有任务的 JavaCompile 类型引用
4 注册一个名为 myCopy1 的新的 Copy 类型任务
5 获取名为 assemble 的现有(无类型)任务的引用并配置它——使用此语法只能配置 Task 上可用的属性和方法
6 注册一个名为 myTask2 的新的无类型任务并配置它——在这种情况下,您只能配置 Task 上可用的属性和方法
7 获取名为 test 的现有 Test 类型任务的引用并配置它——在这种情况下,您可以访问指定类型的属性和方法
8 注册一个名为 myCopy2 的新的 Copy 类型任务并配置它
上述示例依赖于配置规避 API。如果您需要或想要急切地配置或注册容器元素,只需将 named() 替换为 getByName(),将 register() 替换为 create()

使用 Kotlin 委托属性

与容器交互的另一种方式是通过 Kotlin 委托属性。如果您需要容器元素的引用,以便在构建中的其他地方使用,则这些属性特别有用。此外,Kotlin 委托属性可以通过 IDE 重构轻松重命名。

以下示例实现了与上一节相同的结果,但它使用委托属性并重用这些引用而不是字符串字面量任务路径

build.gradle.kts
val check by tasks.existing
val myTask1 by tasks.registering

val compileJava by tasks.existing(JavaCompile::class)
val myCopy1 by tasks.registering(Copy::class)

val assemble by tasks.existing {
    dependsOn(myTask1)  (1)
}
val myTask2 by tasks.registering {
    description = "Some meaningful words"
}

val test by tasks.existing(Test::class) {
    testLogging.showStackTraces = true
}
val myCopy2 by tasks.registering(Copy::class) {
    from("source")
    into("destination")
}
1 使用 myTask1 任务的引用而不是任务路径
上述示例依赖于配置规避 API。如果您需要或想要急切地配置或注册容器元素,只需将 existing() 替换为 getting(),将 registering() 替换为 creating()

同时配置多个容器元素

配置容器的多个元素时,您可以将交互分组到一个块中,以避免在每次交互中重复容器名称。

以下示例演示了类型安全访问器、容器 API 和 Kotlin 委托属性的组合

build.gradle.kts
tasks {
    test {
        testLogging.showStackTraces = true
    }
    val myCheck by registering {
        doLast { /* assert on something meaningful */ }
    }
    check {
        dependsOn(myCheck)
    }
    register("myHelp") {
        doLast { /* do something helpful */ }
    }
}

使用运行时属性

Gradle 在运行时定义了两种主要的属性来源:项目属性额外属性

Kotlin DSL 为处理这些属性类型提供了特定的语法,我们将在以下部分中探讨。

项目属性

Kotlin DSL 允许您通过 Kotlin 委托属性绑定项目属性来访问它们。

以下代码片段演示了此技术对于几个项目属性的应用,其中一个属性必须被定义

build.gradle.kts
val myProperty: String by project  (1)
val myNullableProperty: String? by project (2)
1 通过 myProperty 委托属性使 myProperty 项目属性可用——在这种情况下,项目属性必须存在,否则当构建脚本尝试使用 myProperty 值时,构建将失败
2 myNullableProperty 项目属性执行相同的操作,但是只要您检查 null 值(Kotlin 的 null 安全标准规则适用),构建就不会在使用 myNullableProperty 值时失败

相同的方法适用于设置脚本和初始化脚本,只是您分别使用 by settingsby gradle 代替 by project

额外属性

额外属性在任何实现 ExtensionAware 接口的对象上都可用。

在 Kotlin DSL 中,您可以通过委托属性访问和创建额外属性,使用 by extra 语法,如下例所示

build.gradle.kts
val myNewProperty by extra("initial value")  (1)
val myOtherNewProperty by extra { "calculated initial value" }  (2)

val myExtraProperty: String by extra  (3)
val myExtraNullableProperty: String? by extra  (4)
1 在当前上下文(本例中为项目)中创建一个名为 myNewProperty 的新额外属性,并将其初始化为值 "initial value",这也决定了属性的类型
2 创建一个新的额外属性,其初始值由提供的 lambda 计算
3 将当前上下文(本例中为项目)中现有额外属性绑定到 myProperty 引用
4 与上一行相同,但允许属性具有 null 值

此方法适用于所有 Gradle 脚本:项目构建脚本、脚本插件、设置脚本和初始化脚本。

您还可以使用以下语法从子项目访问根项目上的额外属性

my-sub-project/build.gradle.kts
val myNewProperty: String by rootProject.extra  (1)
1 将根项目的 myNewProperty 额外属性绑定到同名引用

额外属性不仅限于项目。例如,Task 扩展了 ExtensionAware,因此您也可以将额外属性附加到任务上。

以下示例定义了一个新的 myNewTaskPropertytest 任务上,然后使用该属性初始化另一个任务

build.gradle.kts
tasks {
    test {
        val reportType by extra("dev")  (1)
        doLast {
            // Use 'suffix' for post-processing of reports
        }
    }

    register<Zip>("archiveTestReports") {
        val reportType: String by test.get().extra  (2)
        archiveAppendix = reportType
        from(test.get().reports.html.outputLocation)
    }
}
1 test 任务上创建一个新的 reportType 额外属性
2 使 test 任务的 reportType 额外属性可用于配置 archiveTestReports 任务

如果您乐意使用急切配置而不是配置规避 API,您可以使用一个“全局”属性作为报告类型,如下所示

build.gradle.kts
tasks.test {
    doLast { /* ... */ }
}

val testReportType by tasks.test.get().extra("dev")  (1)

tasks.create<Zip>("archiveTestsReports") {
    archiveAppendix = testReportType  (2)
    from(test.reports.html.outputLocation)
}
1 test 任务上创建并初始化一个额外属性,并将其绑定到“全局”属性
2 使用“全局”属性初始化 archiveTestReports 任务

额外属性还有最后一种语法,它将 extra 视为一个 map。我们通常不建议使用这种方法,因为它绕过了 Kotlin 的类型检查并限制了 IDE 支持。但是,它比委托属性语法更简洁,如果您只需要设置额外属性而不需要稍后引用它,则可以使用它。

这是一个简单的示例,演示如何使用 map 语法设置和读取额外属性

build.gradle.kts
extra["myNewProperty"] = "initial value"  (1)

tasks.register("myTask") {
    doLast {
        println("Property: ${project.extra["myNewProperty"]}")  (2)
    }
}
1 创建一个新的项目额外属性 myNewProperty 并设置其值
2 从我们创建的项目额外属性中读取值——注意 extra[…​] 上的 project. 限定符,否则 Gradle 会假定我们想从任务中读取额外属性

使用 Gradle 类型

PropertyProviderNamedDomainObjectProvider 是代表值和对象的延迟和惰性评估的类型。Kotlin DSL 为使用这些类型提供了专门的语法。

使用 Property

属性表示一个可以惰性设置和读取的值

  • 设置值:property.set(value)property = value

  • 访问值:property.get()

  • 使用委托语法:val propValue: String by property

build.gradle.kts
val myProperty: Property<String> = project.objects.property(String::class.java)

myProperty.set("Hello, Gradle!") // Set the value
println(myProperty.get())        // Access the value

// Using delegate syntax
val propValue: String by myProperty
println(propValue)

// Using lazy syntax
myProperty = "Hi, Gradle!" // Set the value
println(myProperty.get())  // Access the value

使用 Provider

Provider 表示一个只读、惰性求值的值

  • 访问值:provider.get()

  • 链式调用:provider.map { transform(it) }

build.gradle.kts
val versionProvider: Provider<String> = project.provider { "1.0.0" }

println(versionProvider.get()) // Access the value

// Chaining transformations
val majorVersion: Provider<String> = versionProvider.map { it.split(".")[0] }
println(majorVersion.get()) // Prints: "1"

使用 NamedDomainObjectProvider

命名域对象提供程序表示来自 Gradle 容器(如任务或扩展)的惰性求值命名对象

  • 访问对象:namedObjectProvider.get()

  • 配置对象:namedObjectProvider.configure { …​ }

build.gradle.kts
val myTaskProvider: NamedDomainObjectProvider<Task> = tasks.named("build")

// Configuring the task
myTaskProvider.configure {
    doLast {
        println("Build task completed!")
    }
}

// Accessing the task
val myTask: Task = myTaskProvider.get()

惰性属性赋值

Gradle 的 Kotlin DSL 支持使用 = 运算符进行惰性属性赋值。

惰性属性赋值在使用惰性属性时减少了冗余。它适用于公开为 final(没有 setter)且类型为 PropertyConfigurableFileCollection 的属性。由于属性必须是 final,我们通常建议避免为惰性类型属性使用自定义 setter,如果可能,通过抽象 getter 实现此类属性。

在 Kotlin DSL 中,使用 = 运算符是调用 set() 的首选方式

build.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

abstract class WriteJavaVersionTask : DefaultTask() {
    @get:Input
    abstract val javaVersion: Property<String>
    @get:OutputFile
    abstract val output: RegularFileProperty

    @TaskAction
    fun execute() {
        output.get().asFile.writeText("Java version: ${javaVersion.get()}")
    }
}

tasks.register<WriteJavaVersionTask>("writeJavaVersion") {
    javaVersion.set("17") (1)
    javaVersion = "17" (2)
    javaVersion = java.toolchain.languageVersion.map { it.toString() } (3)
    output = layout.buildDirectory.file("writeJavaVersion/javaVersion.txt")
}
1 使用 .set() 方法设置值
2 使用 = 运算符进行惰性属性赋值来设置值
3 = 运算符也可以用于赋值惰性值

IDE 支持

IntelliJ 2022.3 及 Android Studio Giraffe 开始支持惰性属性赋值。

Kotlin DSL 插件

Kotlin DSL 插件提供了一种方便的方式来开发贡献构建逻辑的基于 Kotlin 的项目。这包括buildSrc 项目包含构建Gradle 插件

该插件通过以下方式实现此目的

  • 应用Kotlin 插件,该插件添加了对编译 Kotlin 源文件的支持。

  • kotlin-stdlibkotlin-reflectgradleKotlinDsl() 依赖项添加到 compileOnlytestImplementation 配置中,从而在 Kotlin 代码中启用这些 Kotlin 库和 Gradle API 的使用。

  • 配置 Kotlin 编译器,使用与 Kotlin DSL 脚本相同的设置,确保构建逻辑与这些脚本之间的一致性

  • 启用对预编译脚本插件的支持。

每个 Gradle 版本都旨在与特定版本的 kotlin-dsl 插件一起使用。任意 Gradle 版本和 kotlin-dsl 插件版本之间的兼容性不作保证。使用意外版本的 kotlin-dsl 插件将发出警告,并可能导致难以诊断的问题。

这是您需要使用插件的基本配置

buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    // The org.jetbrains.kotlin.jvm plugin requires a repository
    // where to download the Kotlin compiler dependencies from.
    mavenCentral()
}

Kotlin DSL 插件利用了Java 工具链。默认情况下,代码将针对 Java 8。您可以通过定义项目使用的 Java 工具链来更改它

buildSrc/src/main/kotlin/myproject.java-conventions.gradle.kts
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

嵌入式 Kotlin

Gradle 嵌入 Kotlin 以提供对基于 Kotlin 的脚本的支持。

Kotlin 版本

Gradle 附带 kotlin-compiler-embeddable 以及匹配版本的 kotlin-stdlibkotlin-reflect 库。有关详细信息,请参阅 Gradle 的兼容性矩阵的 Kotlin 部分。这些模块中的 kotlin 包通过 Gradle 类路径可见。

Kotlin 提供的兼容性保证适用于向后和向前兼容性。

向后兼容性

我们的方法是只在 Gradle 主要版本发布时进行向后不兼容的 Kotlin 升级。我们清楚地记录了每个版本附带的 Kotlin 版本,并在主要版本发布前宣布升级计划。

旨在与旧版 Gradle 保持兼容性的插件作者必须将其 API 使用限制为这些版本支持的范围。这与使用 Gradle 中的任何新 API 没有什么不同。例如,如果引入了用于依赖项解析的新 API,插件必须要么放弃对旧版 Gradle 的支持,要么组织其代码以有条件地在兼容版本上执行新代码路径。

向前兼容性

主要的兼容性问题在于外部 kotlin-gradle-plugin 版本与 Gradle 附带的 kotlin-stdlib 版本之间。更广泛地说,这适用于任何传递性地依赖于 kotlin-stdlib 及其由 Gradle 提供的版本的插件。只要版本兼容,一切都应该按预期工作。随着 Kotlin 语言的成熟,这个问题将逐渐减少。

Kotlin 编译器参数

以下 Kotlin 编译器参数用于编译 Kotlin DSL 脚本,以及应用了 kotlin-dsl 插件的项目的 Kotlin 源和脚本

-java-parameters

为 Java >= 1.8 方法参数的反射生成元数据。有关更多信息,请参阅 Kotlin 文档中的Kotlin/JVM 编译器选项

-Xjvm-default=all

使 Kotlin 接口的所有非抽象成员成为实现它们的 Java 类的默认成员。这是为了为用 Kotlin 编写的插件提供更好的与 Java 和 Groovy 的互操作性。有关更多信息,请参阅 Kotlin 文档中的接口中的默认方法

-Xsam-conversions=class

将 SAM(单一抽象方法)转换的实现策略设置为始终生成匿名类,而不是使用 invokedynamic JVM 指令。这是为了更好地支持配置缓存和增量构建。有关更多信息,请参阅 Kotlin 问题跟踪器中的KT-44912

-Xjsr305=strict & -Xjspecify-annotations=strict

设置 Kotlin 的 Java 互操作性以严格遵循 JSR-305 和 JSpecify 注解,以提高空安全。有关更多信息,请参阅 Kotlin 文档中的从 Kotlin 调用 Java 代码

互操作性

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

Kotlin 在设计时就考虑到了 Java 互操作性。现有的 Java 代码可以很自然地从 Kotlin 调用,Kotlin 代码也可以很顺畅地从 Java 使用。
—— Kotlin 参考文档

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

同样的,与 Groovy 代码的互操作性也大多如此。此外,Kotlin DSL 提供了几种选择加入 Groovy 语义的方式,我们接下来将讨论。

静态扩展

Groovy 和 Kotlin 语言都支持通过 Groovy 扩展模块Kotlin 扩展来扩展现有类。

要从 Groovy 调用 Kotlin 扩展函数,请将其作为静态函数调用,并将接收者作为第一个参数传递

build.gradle
TheTargetTypeKt.kotlinExtensionFunction(receiver, "parameters", 42, aReference)

Kotlin 扩展函数是包级函数。您可以在 Kotlin 参考文档的包级函数部分了解如何定位声明给定 Kotlin 扩展的类型名称。

要从 Kotlin 调用 Groovy 扩展方法,也适用相同的方法:将其作为静态函数调用,并将接收者作为第一个参数传递

build.gradle.kts
TheTargetTypeGroovyExtension.groovyExtensionMethod(receiver, "parameters", 42, aReference)

命名参数和默认参数

Groovy 和 Kotlin 语言都支持命名函数参数和默认参数,尽管它们的实现方式非常不同。Kotlin 对两者都提供了完整的功能支持,如 Kotlin 语言参考中命名参数默认参数所述。Groovy 以非类型安全的方式实现命名参数,基于 Map<String, ?> 参数,这意味着它们不能与默认参数结合使用。换句话说,对于任何给定方法,在 Groovy 中您只能使用其中一种。

从 Groovy 调用 Kotlin

要从 Groovy 调用具有命名参数的 Kotlin 函数,只需使用带有位置参数的普通方法调用

build.gradle
kotlinFunction("value1", "value2", 42)

无法通过参数名称提供值。

要从 Groovy 调用具有默认参数的 Kotlin 函数,请始终为所有函数参数传递值。

从 Kotlin 调用 Groovy

要从 Kotlin 调用带有命名参数的 Groovy 函数,您需要传递一个 Map<String, ?>,如下例所示

build.gradle.kts
groovyNamedArgumentTakingMethod(mapOf(
    "parameterName" to "value",
    "other" to 42,
    "and" to aReference))

要从 Kotlin 调用带有默认参数的 Groovy 函数,请始终为所有参数传递值。

Kotlin 中的 Groovy 闭包

有时您可能需要从 Kotlin 代码调用接受 Closure 参数的 Groovy 方法。例如,一些用 Groovy 编写的第三方插件期望闭包参数。

用任何语言编写的 Gradle 插件都应该优先使用 Action<T> 类型来代替闭包。Groovy 闭包和 Kotlin lambda 会自动映射到该类型的参数。

为了提供一种在保留 Kotlin 强类型的同时构造闭包的方法,存在两个辅助方法

  • closureOf<T> {}

  • delegateClosureOf<T> {}

这两种方法在不同情况下都很有用,并且取决于您将 Closure 实例传递给的方法。

有些插件期望简单的闭包,就像 Bintray 插件一样

build.gradle.kts
bintray {
    pkg(closureOf<PackageConfig> {
        // Config for the package here
    })
}

在其他情况下,例如在使用 Gretty Plugin 配置农场时,插件期望一个委托闭包

build.gradle.kts
farms {
    farm("OldCoreWar", delegateClosureOf<FarmExtension> {
        // Config for the war here
    })
}

有时从源代码中无法很好地判断应该使用哪个版本。通常,如果您在使用 closureOf<T> {} 时遇到 NullPointerException,使用 delegateClosureOf<T> {} 将解决问题。

这两个实用函数对于配置闭包很有用,但有些插件可能期望 Groovy 闭包用于其他目的。 KotlinClosure0KotlinClosure2 类型允许以更灵活的方式将 Kotlin 函数转换为 Groovy 闭包

build.gradle.kts
somePlugin {

    // Adapt parameter-less function
    takingParameterLessClosure(KotlinClosure0({
        "result"
    }))

    // Adapt unary function
    takingUnaryClosure(KotlinClosure1<String, String>({
        "result from single parameter $this"
    }))

    // Adapt binary function
    takingBinaryClosure(KotlinClosure2<String, String, String>({ a, b ->
        "result from parameters $a and $b"
    }))
}

Kotlin DSL Groovy 构建器

如果某些插件大量使用 Groovy 元编程,那么从 Kotlin 或 Java 或任何静态编译语言中使用它可能会非常麻烦。

Kotlin DSL 提供了一个 withGroovyBuilder {} 实用扩展,它将 Groovy 元编程语义附加到 Any 类型的对象上。

以下示例演示了 target 对象上该方法的几个功能

build.gradle.kts
target.withGroovyBuilder {                                          (1)

    // GroovyObject methods available                               (2)
    if (hasProperty("foo")) { /*...*/ }
    val foo = getProperty("foo")
    setProperty("foo", "bar")
    invokeMethod("name", arrayOf("parameters", 42, aReference))

    // Kotlin DSL utilities
    "name"("parameters", 42, aReference)                            (3)
        "blockName" {                                               (4)
            // Same Groovy Builder semantics on `blockName`
        }
    "another"("name" to "example", "url" to "https://example.com/") (5)
}
1 接收者是一个 GroovyObject 并提供 Kotlin 助手
2 GroovyObject API 可用
3 调用 methodName 方法,传递一些参数
4 配置 blockName 属性,映射到接受方法调用的 Closure
5 调用接受命名参数的 another 方法,映射到接受 Groovy 命名参数 Map<String, ?> 的方法调用

使用 Groovy 脚本

处理假定 Groovy DSL 构建脚本的问题插件的另一个选择是,在 Groovy DSL 构建脚本中配置它们,然后从主 Kotlin DSL 构建脚本应用该 Groovy DSL 构建脚本

dynamic-groovy-plugin-configuration.gradle
native {    (1)
    dynamic {
        groovy as Usual
    }
}
build.gradle.kts
plugins {
    id("dynamic-groovy-plugin") version "1.0"   (2)
}
apply(from = "dynamic-groovy-plugin-configuration.gradle")  (3)
1 Groovy 脚本使用动态 Groovy 配置插件
2 Kotlin 构建脚本请求并应用插件
3 Kotlin 构建脚本应用 Groovy 脚本

故障排除

IDE 支持由两个组件提供

  1. Kotlin 插件(由 IntelliJ IDEA/Android Studio 使用)。

  2. Gradle。

支持级别因每个版本的不同而异。

如果遇到问题,请首先从命令行运行 ./gradlew tasks 以确定问题是否特定于 IDE。如果问题在命令行上仍然存在,则可能源于构建本身而非 IDE 集成。

但是,如果构建在命令行上成功运行,但您的脚本编辑器报告错误,请尝试重新启动 IDE 并清除其缓存。

如果问题仍然存在,并且您怀疑 Kotlin DSL 脚本编辑器有问题,请尝试以下操作

  • 运行 ./gradlew tasks 以收集更多详细信息。

  • 检查以下位置的日志

    • macOS 上的 $HOME/Library/Logs/gradle-kotlin-dsl

    • Linux 上的 $HOME/.gradle-kotlin-dsl/log

    • Windows 上的 $HOME/AppData/Local/gradle-kotlin-dsl/log

  • Gradle 问题跟踪器上报告问题,并尽可能包含详细信息。

从 5.1 版本开始,日志目录会自动清理。日志会定期检查(最多每 24 小时一次),如果 7 天未使用,文件将被删除。

如果这不能帮助查明问题,您可以在 IDE 中启用 org.gradle.kotlin.dsl.logging.tapi 系统属性。这将导致 Gradle Daemon 在其位于 $HOME/.gradle/daemon 的日志文件中记录更多详细信息。

在 IntelliJ IDEA 中,通过导航到 Help > Edit Custom VM Options…​ 并添加:-Dorg.gradle.kotlin.dsl.logging.tapi=true 来启用此属性。

对于 Kotlin DSL 脚本编辑器之外的 IDE 问题,请在相应的 IDE 问题跟踪器中提出问题

最后,如果您遇到 Gradle 本身或 Kotlin DSL 的问题,请在Gradle 问题跟踪器上报告问题。

限制

  • Kotlin DSL 在首次使用时(例如,在干净的检出或临时持续集成代理上)已知比 Groovy DSL 慢。更改 buildSrc 目录中的内容也会产生影响,因为它会使构建脚本缓存失效。造成此现象的主要原因是 Kotlin DSL 脚本编译速度较慢。

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

  • Kotlin DSL 脚本编译规避存在已知问题。如果您遇到问题,可以通过将 org.gradle.kotlin.dsl.scriptCompilationAvoidance 系统属性设置为 false 来禁用它。

  • Kotlin DSL 不支持 model {} 块,该块是已停用的 Gradle 软件模型的一部分。

如果您遇到麻烦或发现疑似错误,请在Gradle 问题跟踪器中报告问题。