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

安卓工作室

Eclipse集成开发环境

克利翁

ApacheNetBeans

Visual Studio 代码(LSP)

视觉工作室

1 Gradle Kotlin DSL脚本中的Kotlin 语法 高亮显示 2 Gradle Kotlin DSL脚本中的代码补全、导航文档、重构...

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

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

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

IntelliJ IDEA 和 Android Studio(源自 IntelliJ IDEA)都会检测您何时对构建逻辑进行更改并提供两个建议:

  1. 再次导入整个构建

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

    重新加载脚本依赖项

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

故障排除

IDE 支持由两个组件提供:

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

  • 摇篮

支持级别根据每个版本的不同而有所不同。

如果遇到问题,您应该尝试的第一件事是./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 天未使用,则将其删除。

如果上述内容不足以查明问题,您可以org.gradle.kotlin.dsl.logging.tapi在 IDE 中启用系统属性。这将导致 Gradle 守护进程在其位于$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.kts构建脚本的扩展名代替.gradle.这也适用于设置文件(例如settings.gradle.kts)和初始化脚本

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

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

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

  • 根据模式或简单地命名初始化脚本*.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

当 Gradle 或插件发生更改时,在插件和构建脚本中使用内部 Kotlin DSL API 可能会破坏构建。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

通过将 Gradle属性设置org.gradle.kotlin.dsl.allWarningsAsErrorstrue:可以将构建配置为在脚本编译过程中发出任何警告时失败:

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

类型安全模型访问器

Groovy DSL 允许您按名称引用构建模型的许多元素,即使它们是在运行时定义的。想想命名配置、命名源集等等。例如,您可以implementation通过获取配置configurations.implementation

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

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

Kotlin DSL 目前支持由插件提供的以下任何类型的类型安全模型访问器:

  • 依赖项和工件配置(例如implementation并由runtimeOnlyJava 插件提供)

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

  • dependenciesrepositories容器上的扩展

  • tasksconfigurations容器中的元素

  • 项目扩展容器中的元素(例如添加到sourceSets容器的 Java 插件提供的源集)

  • 对上述每一项的扩展

只有主项目构建脚本和预编译项目脚本插件具有类型安全的模型访问器。初始化脚本、设置脚本、脚本插件则不然。这些限制将在未来的 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 库插件api提供的、implementationtestImplementation依赖项配置使用类型安全访问器
2 使用访问器来配置sourceSets项目扩展
3 使用访问器来配置main源集
4 使用访问器来配置源集java的源main
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 实现的 Binary 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 将无法帮助您发现可用的配置,但您可以在相应插件的文档中或通过运行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
}

请注意,这是类型的sourceSetsGradle 扩展,也是类型的扩展。ProjectSourceSetContainerjavaProjectJavaPluginExtension

您可以通过查看所应用插件的文档或运行来发现可用的扩展和约定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.作为最后的手段,您也许可以查看插件的源代码以了解它的功能,但在大多数情况下这不是必需的。

任务

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

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

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

我们使用 Gradle API 按名称和类型引用任务,而不是使用访问器。请注意,必须显式指定任务的类型,否则脚本将无法编译,因为推断的类型将是Task, not 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 插件门户之上的其他插件存储库,您应该在文件pluginManagement {}的块中声明它们settings.gradle.kts,如下所示:

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

从Gradle 插件门户以外的来源获取的插件只能通过块声明,前提是它们与插件标记工件plugins {}一起发布。

截至撰写本文时,google()存储库中存在的所有 Android Plugin for 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 示例构建,以从根项目构建脚本完全配置其子项目:

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 Plugin开发插件

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

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

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

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

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

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 映射到哪个插件实现类的详细信息,使用“编写自定义插件”一章中描述的属性文件机制。

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

使用容器对象

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

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 Task获取对名为的现有任务的类型引用check
2 注册一个名为的新无类型任务myTask1
3 compileJava获取对名为类型的现有任务的引用JavaCompile
4 注册一个名为myCopy1类型的新任务Copy
5 获取对已命名的现有(无类型)任务的引用assemble并对其进行配置 - 您只能配置Task使用此语法可用的属性和方法
6 注册一个名为myTask2并配置的新无类型任务 - 您只能配置Task在这种情况下可用的属性和方法
7 test获取对名为type 的现有任务的引用Test并对其进行配置 - 在这种情况下,您可以访问指定类型的属性和方法
8 myCopy2注册一个名为type 的新任务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 委托属性的组合:

Example 15. Container scope
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 委托属性绑定来访问项目属性。下面是一个示例片段,演示了几个项目属性的技术,其中必须定义其中一个属性:

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

相同的方法适用于设置和初始化脚本,只不过您分别使用by settings和代替。by gradleby project

额外属性

任何实现ExtensionAware接口的对象都可以使用额外的属性。 Kotlin DSL 允许您使用by extra以下示例中演示的任何形式来访问额外的属性并通过委托属性创建新属性:

构建.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并使用 value 对其进行初始化"initial value",这也确定了属性的类型
2 创建一个新的额外属性,其初始值由提供的 lambda 计算得出
3 将当前上下文(本例中为项目)中的现有额外属性绑定到myProperty引用
4 与上一行相同,但允许属性具有空值

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

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

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

额外的属性不仅限于项目。例如,Taskextends ExtensionAware,因此您也可以将额外的属性附加到任务。下面是一个示例,它myNewTaskPropertytest任务上定义了一个新属性,然后使用该属性来初始化另一个任务:

构建.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 在任务上创建一个新的reportType额外属性test
2 使test任务的reportType额外属性可用于配置archiveTestReports任务

如果您愿意使用即时配置而不是配置避免 API,则可以为报告类型使用单个“全局”属性,如下所示:

构建.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视为映射。我们建议一般不要使用它,因为您会失go Kotlin 类型检查的好处,并且它会阻止 IDE 提供尽可能多的支持。但是,它比委托属性语法更简洁,如果您只需要设置额外属性的值而不稍后引用它,则可以合理地使用它。

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

构建.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(没有设置器)并且类型为Property或 的属性ConfigurableFileCollection。由于属性必须是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 Plugin,增加了对编译 Kotlin 源文件的支持。

  • kotlin-stdlibkotlin-reflect和依赖项添加到和配置gradleKotlinDsl()中,这样您就可以在 Kotlin 代码中使用这些 Kotlin 库和 Gradle API。compileOnlytestImplementation

  • 使用与 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的多个匹配版本。有关详细信息,请参阅 Gradle兼容性矩阵的 Kotlin 部分。这些模块中的包通过 Gradle 类路径可见。kotlin-stdlibkotlin-reflectkotlin

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

向后兼容性

我们的方法是仅在主要的 Gradle 版本上进行向后突破的 Kotlin 升级。我们将始终清楚地记录我们发布的 Kotlin 版本,并在主要版本发布之前宣布升级计划。

想要与较旧的 Gradle 版本保持兼容的插件作者需要将其 API 使用限制为与这些旧版本兼容的子集。它与 Gradle 中的任何其他新 API 并没有真正的不同。例如,如果我们引入一个新的 API 来解决依赖关系,并且一个插件想要使用该 API,那么他们要么需要放弃对旧 Gradle 版本的支持,要么需要对其代码进行一些巧妙的组织,以便仅在较新的版本上执行新的代码路径版本。

向前兼容性

kotlin-gradle-plugin最大的问题是外部版本和kotlin-stdlibGradle 附带的版本之间的兼容性。更一般地说,在任何传递依赖的插件kotlin-stdlib及其随 Gradle 附带的版本之间。只要组合兼容,一切都应该有效。随着语言的成熟,这将不再是一个问题。

Kotlin 编译器参数

这些是用于在应用了该插件的项目中编译 Kotlin DSL 脚本以及 Kotlin 源代码和脚本的 Kotlin 编译器参数kotlin-dsl

-java-parameters

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

-Xjvm-default=all

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

-Xsam-conversions=class

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

-Xjsr305=strict

设置 Kotlin 的 Java 互操作性以严格遵循 JSR-305 注释,以提高 null 安全性。有关更多信息,请参阅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 函数,您需要传递 a 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
    })
}

有时,通过查看源代码并没有一个好的方法来判断要使用哪个版本。通常,如果您得到NullPointerExceptionwith closureOf<T> {},则 usingdelegateClosureOf<T> {} 将解决问题。

这两个实用函数对于配置闭包很有用,但某些插件可能期望 Groovy 闭包用于其他目的。KotlinClosure0to类型KotlinClosure2允许以更大的灵活性使 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 APIGroovyObject可用
3 调用methodName方法,传递一些参数
4 配置blockName属性,映射到Closure获取方法调用
5 调用another采用命名参数的方法,映射到Map<String, ?>采用方法调用的Groovy 命名参数

使用 Groovy 脚本

处理假设 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 脚本

局限性

  • 众所周知,Kotlin DSL在首次使用时比 Groovy DSL 慢,例如在干净的结帐或短暂的持续集成代理上。更改buildSrc目录中的某些内容也会产生影响,因为它会使构建脚本缓存失效。造成这种情况的主要原因是 Kotlin DSL 的脚本编译速度较慢。

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

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

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

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