Gradle 的 Kotlin DSL 为传统的 Groovy DSL 提供了一种替代语法,在支持的 IDE 中提供增强的编辑体验,具有更出色的内容辅助、重构、文档等功能。本章详细介绍了主要的 Kotlin DSL 结构以及如何使用它与 Gradle API 交互。

如果您有兴趣将现有的 Gradle 构建迁移到 Kotlin DSL,请查看专门的 迁移部分

先决条件

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

  • 了解 Kotlin 语法和基本语言特性非常有帮助。 Kotlin 参考文档Kotlin Koans 将帮助您学习基础知识。

  • 使用 plugins {} 块声明 Gradle 插件可以显著改善编辑体验,强烈推荐。

IDE 支持

IntelliJ IDEA 和 Android Studio 完全支持 Kotlin DSL。其他 IDE 尚未提供用于编辑 Kotlin DSL 文件的有用工具,但您仍然可以导入基于 Kotlin-DSL 的构建并照常使用它们。

表 1. IDE 支持矩阵
构建导入 语法高亮 1 语义编辑器 2

IntelliJ IDEA

Android Studio

Eclipse IDE

CLion

Apache NetBeans

Visual Studio Code (LSP)

Visual Studio

1 Kotlin 语法 高亮 Gradle Kotlin DSL 脚本中
2 代码 补全, 导航 源代码, 文档, 重构 等…​ Gradle Kotlin DSL 脚本中

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

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

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

IntelliJ IDEA 和 Android Studio(源自 IntelliJ IDEA)都会检测到您对构建逻辑的更改,并提供两种建议

  1. 再次导入整个构建

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

    Reload script dependencies

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

故障排除

IDE 支持由两个组件提供

  • IntelliJ IDEA/Android Studio 使用的 Kotlin 插件

  • Gradle

支持级别因每个组件的版本而异。

如果您遇到问题,您应该尝试的第一件事是从命令行运行 ./gradlew tasks,以查看您的问题是否仅限于 IDE。如果您在命令行中遇到相同的问题,则问题出在构建而不是 IDE 集成。

如果您可以在命令行中成功运行构建,但您的脚本编辑器出现错误,则您应该尝试重新启动 IDE 并使缓存失效。

如果上述方法不起作用,并且您怀疑 Kotlin DSL 脚本编辑器存在问题,您可以

  • 运行 ./gradle tasks 以获取更多详细信息

  • 检查以下位置之一的日志

    • $HOME/Library/Logs/gradle-kotlin-dsl 在 Mac OS X 上

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

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

  • 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 的等效项类似,Kotlin DSL 在 Gradle 的 Java API 之上实现。您在 Kotlin DSL 脚本中读取的所有内容都是由 Gradle 编译和执行的 Kotlin 代码。您在构建脚本中使用的许多对象、函数和属性都来自 Gradle API 和应用插件的 API。

您可以使用 Kotlin DSL 参考 搜索功能来浏览可用的成员。

脚本文件名

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

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

要激活 Kotlin DSL,只需将构建脚本中的 .gradle 扩展名替换为 .gradle.kts。这也适用于 设置文件(例如 settings.gradle.kts)和 初始化脚本

请注意,您可以将 Groovy DSL 构建脚本与 Kotlin DSL 构建脚本混合使用,即 Kotlin DSL 构建脚本可以应用 Groovy DSL 构建脚本,并且多项目构建中的每个项目都可以使用其中一个。

我们建议您应用以下约定以获得更好的 IDE 支持

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

  • 根据模式 *.init.gradle.ktsinit.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

避免使用内部 Kotlin DSL API

在插件和构建脚本中使用内部 Kotlin DSL API 可能会在 Gradle 或插件更改时导致构建中断。 Kotlin DSL API 使用上面列出的包中的类型(但不包括这些包的子包)扩展了 Gradle 公共 API,这些类型在 相应的 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 允许您按名称引用构建模型的许多元素,即使它们是在运行时定义的。例如,您可以通过 configurations.implementation 获取 implementation 配置。

Kotlin DSL 使用类型安全的模型访问器来替换这种动态解析,这些访问器与插件贡献的模型元素一起使用。

了解何时可以使用类型安全的模型访问器

Kotlin DSL 目前支持对以下任何由插件贡献的元素使用类型安全的模型访问器

  • 依赖项和工件配置(例如,由 Java 插件贡献的 implementationruntimeOnly

  • 项目扩展和约定(例如 sourceSets

  • dependenciesrepositories 容器上的扩展

  • tasksconfigurations 容器中的元素

  • 项目扩展容器 中的元素(例如,由 Java 插件贡献并添加到 sourceSets 容器中的源集)

  • 上述每个元素上的扩展

只有主项目构建脚本和预编译项目脚本插件具有类型安全的模型访问器。初始化脚本、设置脚本、脚本插件没有。这些限制将在未来的 Gradle 版本中移除。

可用的类型安全模型访问器集是在评估脚本主体之前,紧接在 plugins {} 块之后计算的。在此之后贡献的任何模型元素都不适用于类型安全的模型访问器。例如,这包括您可能在自己的构建脚本中定义的任何配置。但是,这种方法确实意味着您可以对由父项目应用的插件贡献的任何模型元素使用类型安全的访问器。

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

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>类型,并提供对底层任务的延迟引用和延迟配置。以下是一些说明配置避免适用情况的示例

tasks.test {
    // lazy configuration
}

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

testProvider {
    // lazy configuration
}

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

对于除tasks之外的所有其他容器,元素的访问器都是NamedDomainObjectProvider<T>类型,并提供相同的行为。

了解在没有类型安全模型访问器的情况下该怎么做

考虑上面显示的示例构建脚本,它演示了类型安全访问器的使用。以下示例完全相同,只是它使用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。为此,您需要知道配置的模型元素的名称和/或类型。我们现在将向您展示如何通过详细查看上面的脚本来发现这些内容。

工件配置

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

示例 3. 工件配置
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 无法帮助您发现可用的配置,但您可以在相应的插件文档中查找它们,或者通过运行gradle 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

您可以通过查看应用插件的文档或运行 gradle kotlinDslAccessorsReport 来发现有哪些扩展和约定可用,该命令会打印访问所有应用插件贡献的模型元素所需的 Kotlin 代码。 该报告提供了名称和类型。 作为最后的手段,您还可以查看插件的源代码,但在大多数情况下应该没有必要。

请注意,如果您只需要扩展或约定的引用而无需配置它,或者您想执行单行配置,则也可以使用 the<T>() 函数,如下所示

the<SourceSetContainer>()["main"].srcDir("src/core/java")

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

项目扩展容器中的元素

基于容器的项目扩展(例如 SourceSetContainer)还允许您配置它们持有的元素。 在我们的示例构建脚本中,我们希望配置源集容器中名为 main 的源集,我们可以通过使用 named() 方法代替访问器来实现,如下所示

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

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

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

至于项目扩展和约定本身,您可以通过查看应用插件的文档或运行 gradle kotlinDslAccessorsReport 来发现任何容器中存在哪些元素。 作为最后的手段,您可能可以查看插件的源代码以了解它的作用,但在大多数情况下应该没有必要。

任务

任务不是通过基于容器的项目扩展进行管理的,但它们是容器的一部分,行为类似。这意味着您可以像配置源集一样配置任务,如本例所示

示例 6. 任务
build.gradle.kts
apply(plugin = "java-library")

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

我们使用 Gradle API 通过名称和类型引用任务,而不是使用访问器。请注意,必须显式指定任务的类型,否则脚本将无法编译,因为推断的类型将是 Task,而不是 Test,并且 testLogging 属性特定于 Test 任务类型。但是,如果您只需要配置属性或调用对所有任务通用的方法,则可以省略类型,即它们在 Task 接口上声明。

可以通过运行 gradle tasks 来发现有哪些任务可用。然后,您可以通过运行 gradle help --task <taskName> 来找出给定任务的类型,如这里所示

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

请注意,IDE 可以帮助您进行必要的导入,因此您只需要类型的简单名称,即没有包名部分。在这种情况下,无需导入 Test 任务类型,因为它属于 Gradle API,因此 隐式导入

关于约定

一些 Gradle 核心插件通过所谓的约定对象公开可配置性。它们与扩展具有相似的目的,并且现在已被扩展取代。约定已弃用。编写新插件时,请避免使用约定对象。

如上所示,Kotlin DSL 仅为 Project 上的约定对象提供访问器。在某些情况下,您需要与使用其他类型上的约定对象的 Gradle 插件进行交互。Kotlin DSL 提供 withConvention(T::class) {} 扩展函数来执行此操作

build.gradle.kts
sourceSets {
    main {
        withConvention(CustomSourceSetConvention::class) {
            someOption = "some value"
        }
    }
}

此技术主要用于为尚未迁移到扩展的语言插件添加的源集。

多项目构建

与单项目构建一样,您应该尝试在多项目构建中使用 plugins {} 块,以便您可以使用类型安全的访问器。多项目构建的另一个考虑因素是,在根构建脚本中或使用项目之间交叉配置的其他形式配置子项目时,您将无法使用类型安全的访问器。我们在以下部分更详细地讨论这两个主题。

应用插件

您可以在适用于它们的子项目中声明插件,但我们建议您也将其在根项目构建脚本中声明。这使得在构建中保持跨项目的插件版本一致变得更加容易。这种方法还提高了构建的性能。

使用 Gradle 插件》一章解释了如何在根项目构建脚本中使用版本声明插件,然后将其应用于相应的子项目的构建脚本。以下是使用三个子项目和三个插件的这种方法的示例。请注意,根构建脚本只声明社区插件,因为 Java 库插件与您使用的 Gradle 版本绑定。

settings.gradle.kts
rootProject.name = "multi-project-build"
include("domain", "infra", "http")
build.gradle.kts
plugins {
    id("com.github.johnrengelman.shadow") version "7.1.2" apply false
    id("io.ratpack.ratpack-java") version "1.8.2" apply false
}
domain/build.gradle.kts
plugins {
    `java-library`
}

dependencies {
    api("javax.measure:unit-api:1.0")
    implementation("tec.units:unit-ri:1.0.3")
}
infra/build.gradle.kts
plugins {
    `java-library`
    id("com.github.johnrengelman.shadow")
}

shadow {
    applicationDistribution.from("src/dist")
}

tasks.shadowJar {
    minimize()
}
http/build.gradle.kts
plugins {
    java
    id("io.ratpack.ratpack-java")
}

dependencies {
    implementation(project(":domain"))
    implementation(project(":infra"))
    implementation(ratpack.dependency("dropwizard-metrics"))
}

application {
    mainClass = "example.App"
}

ratpack.baseDir = file("src/ratpack/baseDir")

如果您的构建需要在 Gradle 插件门户网站之外的额外插件存储库,您应该在 settings.gradle.kts 文件中的 pluginManagement {} 块中声明它们,如下所示

settings.gradle.kts
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

Gradle 插件门户网站 以外的来源获取的插件只能通过 plugins {} 块声明,前提是它们已使用其 插件标记工件 发布。

在撰写本文时,google() 存储库中所有版本的 Android Gradle 插件(最高到 3.2.0)都缺少插件标记工件。

如果这些工件丢失,则无法使用 plugins {} 块。您必须改用在根项目构建脚本中的 buildscript {} 块中声明插件依赖项。以下是如何为 Android 插件执行此操作的示例

settings.gradle.kts
include("lib", "app")
build.gradle.kts
buildscript {
    repositories {
        google()
        gradlePluginPortal()
    }
    dependencies {
        classpath("com.android.tools.build:gradle:7.3.0")
    }
}
lib/build.gradle.kts
plugins {
    id("com.android.library")
}

android {
    // ...
}
app/build.gradle.kts
plugins {
    id("com.android.application")
}

android {
    // ...
}

此技术与 Android Studio 在创建新构建时生成的代码并没有太大区别。主要区别在于上述示例中的子项目构建脚本使用 plugins {} 块声明其插件。这意味着您可以对它们贡献的模型元素使用类型安全的访问器。

请注意,如果您想将此类插件应用于多项目构建的根项目构建脚本(而不是仅应用于其子项目)或单项目构建,则无法使用此技术。在这种情况下,您需要使用我们在另一部分中详细介绍的不同方法。

跨项目配置

跨项目配置是一种机制,您可以通过它从另一个项目的构建脚本配置项目。一个常见的例子是在根项目构建脚本中配置子项目。

采用这种方法意味着您将无法使用类型安全访问器来访问插件贡献的模型元素。相反,您必须依赖字符串文字和标准 Gradle API。

例如,让我们修改Java/Ratpack 示例构建,以从根项目构建脚本完全配置其子项目

示例 11. 跨项目配置
settings.gradle.kts
rootProject.name = "multi-project-build"
include("domain", "infra", "http")
build.gradle.kts
import com.github.jengelman.gradle.plugins.shadow.ShadowExtension
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import ratpack.gradle.RatpackExtension

plugins {
    id("com.github.johnrengelman.shadow") version "7.1.2" apply false
    id("io.ratpack.ratpack-java") version "1.8.2" apply false
}

project(":domain") {
    apply(plugin = "java-library")
    repositories { mavenCentral() }
    dependencies {
        "api"("javax.measure:unit-api:1.0")
        "implementation"("tec.units:unit-ri:1.0.3")
    }
}

project(":infra") {
    apply(plugin = "java-library")
    apply(plugin = "com.github.johnrengelman.shadow")
    configure<ShadowExtension> {
        applicationDistribution.from("src/dist")
    }
    tasks.named<ShadowJar>("shadowJar") {
        minimize()
    }
}

project(":http") {
    apply(plugin = "java")
    apply(plugin = "io.ratpack.ratpack-java")
    repositories { mavenCentral() }
    val ratpack = the<RatpackExtension>()
    dependencies {
        "implementation"(project(":domain"))
        "implementation"(project(":infra"))
        "implementation"(ratpack.dependency("dropwizard-metrics"))
        "runtimeOnly"("org.slf4j:slf4j-simple:1.7.25")
    }
    configure<JavaApplication> {
        mainClass = "example.App"
    }
    ratpack.baseDir = file("src/ratpack/baseDir")
}

请注意,我们如何使用apply()方法应用插件,因为plugins {}块在此上下文中不起作用。我们还使用标准 API 而不是类型安全访问器来配置任务、扩展和约定——我们在其他地方更详细地讨论了这种方法。

当您无法使用plugins {}块时

Gradle 插件门户以外的来源获取的插件可能可以使用也可能无法使用plugins {}块。这取决于它们是如何发布的,特别是它们是否已发布必要的插件标记工件

例如,Gradle 的 Android 插件没有发布到 Gradle 插件门户,并且——至少在插件的 3.2.0 版本之前——用于解析给定插件标识符的工件的元数据没有发布到 Google 存储库。

如果您的构建是多项目构建,并且您不需要将此类插件应用于您的项目,那么您可以使用上面描述的技术解决此问题。对于任何其他情况,请继续阅读。

发布插件时,请使用 Gradle 内置的Gradle 插件开发插件

它自动发布使您的插件可与plugins {}块一起使用的元数据。

在本节中,我们将向您展示如何将 Android 插件应用于单项目构建或多项目构建的根项目。目标是指示您的构建如何将com.android.application插件标识符映射到可解析的工件。这分两步完成

  • 将插件仓库添加到构建的设置脚本中

  • 将插件 ID 映射到相应的工件坐标

您可以通过在构建的设置脚本中配置pluginManagement {}块来完成这两个步骤。为了演示,以下示例将google()仓库(Android 插件发布的位置)添加到仓库搜索列表中,并使用resolutionStrategy {}块将com.android.application插件 ID 映射到google()仓库中可用的com.android.tools.build:gradle:<version>工件

settings.gradle.kts
pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
    }
    resolutionStrategy {
        eachPlugin {
            if(requested.id.namespace == "com.android") {
                useModule("com.android.tools.build:gradle:${requested.version}")
            }
        }
    }
}
build.gradle.kts
plugins {
    id("com.android.application") version "7.3.0"
}

android {
    // ...
}

事实上,以上示例适用于由指定模块提供的所有com.android.*插件。这是因为打包的模块包含有关哪个插件 ID 映射到哪个插件实现类的详细信息,使用的是在编写自定义插件章节中描述的属性文件机制。

有关pluginManagement {}块及其用途的更多信息,请参阅 Gradle 用户手册的插件管理部分。

使用容器对象

Gradle 构建模型大量使用容器对象(或简称“容器”)。例如,configurationstasks都是容器对象,分别包含ConfigurationTask对象。社区插件也贡献了容器,例如 Android 插件贡献的android.buildTypes容器。

Kotlin DSL 为构建作者提供了多种与容器交互的方式。接下来,我们将以tasks容器为例,介绍每种方式。

请注意,如果您正在配置受支持容器上的现有元素,则可以利用另一部分中描述的类型安全访问器。该部分还描述了哪些容器支持类型安全访问器。

使用容器 API

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

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

示例 13. 使用容器 API
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 代理属性。

示例 15. 容器范围
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 项目属性执行相同的操作,但只要您检查空值(标准 Kotlin 空安全规则 适用),构建就不会在使用 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 myProperty: String by extra  (3)
val myNullableProperty: String? by extra  (4)
1 在当前上下文中(在本例中为项目)创建一个名为 myNewProperty 的新额外属性,并将其初始化为值 "initial value",这也决定了属性的类型
2 创建一个新的额外属性,其初始值由提供的 lambda 计算得出
3 将当前上下文(在本例中为项目)中的现有额外属性绑定到 myProperty 引用
4 与上一行相同,但允许属性具有空值

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

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

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

额外属性不仅限于项目。例如,Task 扩展了 ExtensionAware,因此您也可以将额外属性附加到任务。以下示例在 test 任务上定义了一个新的 myNewTaskProperty,然后使用该属性初始化另一个任务

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.destination)
    }
}
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>("archiveTestReports") {
    archiveAppendix = testReportType  (2)
    from(test.get().reports.html.destination)
}
1 test 任务上创建并初始化一个额外属性,将其绑定到一个“全局”属性
2 使用“全局”属性初始化 archiveTestReports 任务

关于额外属性,我们还需要介绍最后一个语法,它将 extra 视为一个映射。我们建议您一般不要使用这种方法,因为您将失去 Kotlin 类型检查的优势,并且会阻止 IDE 提供尽可能多的支持。但是,它比委托属性语法更简洁,如果您只需要设置额外属性的值而无需在以后引用它,则可以使用它。

以下是一个简单的示例,演示了如何使用映射语法设置和读取额外属性

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

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

Kotlin 延迟属性赋值

Gradle 的 Kotlin DSL 支持使用=运算符进行延迟属性赋值。当使用延迟属性时,延迟属性赋值减少了 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 DSL 脚本相同的设置配置 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)
    }
}
buildSrc/src/main/groovy/myproject.java-conventions.gradle
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,而一个插件想要使用该 API,那么他们要么需要放弃对旧版 Gradle 版本的支持,要么需要对代码进行一些巧妙的组织,以便仅在新版本上执行新的代码路径。

向前兼容性

最大的问题是外部 kotlin-gradle-plugin 版本与 Gradle 附带的 kotlin-stdlib 版本之间的兼容性。更一般地说,任何传递依赖于 kotlin-stdlib 的插件与其 Gradle 附带的版本之间的兼容性。只要组合是兼容的,一切就应该正常工作。随着语言的成熟,这个问题将变得越来越不重要。

Kotlin 编译器参数

以下列出了用于编译 Kotlin DSL 脚本以及在应用了 `kotlin-dsl` 插件的项目中编译 Kotlin 源代码和脚本的 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

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

互操作性

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

引用 Kotlin 参考文档

Kotlin 的设计理念是与 Java 互操作。可以以自然的方式从 Kotlin 调用现有的 Java 代码,并且也可以从 Java 非常流畅地使用 Kotlin 代码。

Kotlin 参考文档中非常详细地介绍了 从 Kotlin 调用 Java从 Java 调用 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 函数,只需使用带有位置参数的普通方法调用即可。无法通过参数名称提供值。

要从 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 代码调用接受 闭包 参数的 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 插件配置农场时,插件期望一个委托闭包

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 构建脚本中应用

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 脚本

限制

  • 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 问题跟踪器 中报告问题。