将构建逻辑从 Groovy 迁移到 Kotlin
本节将引导你完成将基于 Groovy 的 Gradle 构建脚本转换为 Kotlin 的过程。
Gradle 较新的 Kotlin DSL 在支持的 IDE 中提供了令人愉悦的编辑体验:内容辅助、重构、文档等等。

另请阅读 Gradle Kotlin DSL 入门,以了解 Gradle Kotlin DSL 的特性、局限性和用法。 |
开始迁移之前
请阅读: 在迁移之前,了解以下重要信息非常有用
-
使用最新版本的 Gradle、应用的插件和你的 IDE 应该是你的第一步。
-
Kotlin DSL 在 Intellij IDEA 和 Android Studio 中得到完全支持。 其他 IDE,例如 Eclipse 或 NetBeans,尚未为编辑 Gradle Kotlin DSL 文件提供有用的工具,但是,导入和使用基于 Kotlin DSL 的构建与往常一样工作。
-
在 IntelliJ IDEA 中,你必须从 Gradle 模型导入你的项目,才能获得 Kotlin DSL 脚本的内容辅助和重构工具。
-
在某些情况下,Kotlin DSL 速度较慢。 例如,在首次使用、全新检出或临时 CI 代理时,已知速度较慢。 当 buildSrc 目录中的某些内容发生更改时,也会发生同样的情况,这会使构建脚本缓存失效。 构建配置时间较慢可能会影响 IDE 的响应速度,请查看关于 Gradle 性能的文档。
-
你必须使用 Java 8 或更高版本运行 Gradle。 不支持 Java 7。
-
已知嵌入式 Kotlin 编译器可在 Linux、macOS、Windows、Cygwin、FreeBSD 和 Solaris 的 x86-64 架构上运行。
-
了解 Kotlin 语法和基本语言特性非常有帮助。Kotlin 参考文档和Kotlin Koans 应该对你有所帮助。
-
使用
plugins {}
块声明 Gradle 插件可以显著改善编辑体验,并且强烈建议使用。 在将 Groovy 构建脚本转换为 Kotlin 之前,请考虑采用它。 -
Kotlin DSL 不支持
model {}
元素。 这是已停止使用的 Gradle 软件模型的一部分。 -
不建议启用孵化中的按需配置功能,因为它可能导致难以诊断的问题。
在Gradle Kotlin DSL 入门中阅读更多信息。
如果你遇到问题或怀疑存在错误,请利用 gradle/gradle
issue 跟踪器。
你不必一次性迁移所有内容! 基于 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 脚本。
重要的是要注意,虽然可以使用对象的 ext
属性修改 Groovy 额外属性,但在 Kotlin 中,它们使用 extra
属性进行修改。 查看每个对象并相应地更新构建脚本非常重要。
你可以在用户指南中找到示例。
脚本文件命名
Groovy DSL 脚本文件使用 .gradle 文件扩展名。 Kotlin DSL 脚本文件使用 .gradle.kts 文件扩展名。 |
要使用 Kotlin DSL,只需将你的文件命名为 build.gradle.kts
而不是 build.gradle
。
设置文件 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 {}
块的示例
plugins {
java
jacoco
`maven-publish`
id("org.springframework.boot") version "2.7.8"
}
plugins {
id 'java'
id 'jacoco'
id 'maven-publish'
id 'org.springframework.boot' version '2.7.8'
}
Kotlin DSL 为所有Gradle 核心插件提供了属性扩展,如上面的 java
、jacoco
或 maven-publish
声明所示。
第三方插件可以像 Groovy DSL 一样应用。 除了双引号和括号。 你也可以使用该样式应用核心插件。 但建议使用静态类型访问器,因为它们是类型安全的,并且将由你的 IDE 自动完成。
你也可以使用命令式 apply
语法,但随后非核心插件必须包含在构建脚本的类路径中
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")
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 {}
块应用的,那么 Kotlin 扩展函数可用于配置它们的扩展,就像在 Groovy 中一样。 以下示例显示了 Jacoco 插件的工作原理。
plugins {
jacoco
}
jacoco {
toolVersion = "0.8.1"
}
plugins {
id 'jacoco'
}
jacoco {
toolVersion = '0.8.1'
}
相比之下,如果你使用命令式 apply()
函数来应用插件,那么你将必须使用 configure<T>()
函数来配置该插件。 以下示例显示了 Checkstyle 插件的工作原理,方法是在 configure<T>()
函数中显式声明插件的扩展类 — CheckstyleExtension
—
apply(plugin = "checkstyle")
configure<CheckstyleExtension> {
maxErrors = 10
}
apply plugin: "checkstyle"
checkstyle {
maxErrors = 10
}
再次强调,我们强烈建议你通过 plugins {}
块以声明方式应用插件。
由于你的 IDE 知道插件提供的配置元素,因此当你向 IDE 请求建议时,它将包含这些元素。 这将发生在你的构建脚本的顶层 — 大多数插件扩展都添加到 Project
对象 — 以及扩展的配置块中。
你还可以运行 :kotlinDslAccessorsReport
任务来了解所有已应用插件贡献的扩展。 它会打印你可以用来访问这些扩展的 Kotlin 代码,并提供访问器方法的名称和类型。
如果你要配置的插件依赖于其方法签名中的 groovy.lang.Closure
或使用其他动态 Groovy 语义,则需要更多的工作才能从 Kotlin DSL 构建脚本配置该插件。 有关如何从 Kotlin 代码调用 Groovy 代码或将该插件的配置保留在 Groovy 脚本中的更多信息,请参阅Gradle Kotlin DSL 文档的互操作性部分。
插件还会贡献你可能想要直接配置的任务。 本主题将在下面的配置任务部分中介绍。
为了充分利用 Gradle Kotlin DSL 的优势,你应该努力保持构建脚本的声明性。 这里要记住的主要一点是,为了获得类型安全的访问器,必须在构建脚本的主体之前应用插件。
强烈建议阅读 Gradle 用户手册中关于使用 Gradle Kotlin DSL 配置插件的内容。
如果你的构建是多项目构建,例如大多数 Android 构建,请同时阅读后续关于多项目构建的部分。
最后,有一些策略可以将 plugins {}
块与未使用正确元数据发布的插件一起使用,例如 Android Gradle 插件。
配置避免
Gradle 4.9 引入了一个新的 API,用于在构建脚本和插件中创建和配置任务。 目的是让这个新的 API 最终取代现有的 API。
现有 Gradle Tasks API 和新 Gradle Tasks API 之间的主要区别之一是 Gradle 是否花费时间来创建
Task
实例并运行配置代码。 新的 API 允许 Gradle 延迟或完全避免配置永远不会在构建中执行的任务。 例如,在编译代码时,Gradle 不需要配置运行测试的任务。
有关更多信息,请参阅改进 Gradle API 以减少配置时间博客文章和用户手册中的任务配置避免章节。
Gradle Kotlin DSL 通过使类型安全模型访问器利用新的 API 并提供 DSL 构造来使其更易于使用,从而拥抱配置避免。 请放心,整个 Gradle API 仍然可用。
配置任务
配置任务的语法是 Groovy 和 Kotlin DSL 开始显著不同的地方。
tasks.jar {
archiveFileName = "foo.jar"
}
tasks.jar {
archiveFileName = 'foo.jar'
}
请注意,在 Kotlin 中,tasks.jar {}
表示法利用配置避免 API 并延迟 jar
任务的配置。
如果类型安全的任务访问器 tasks.jar
不可用,请参阅上面的配置插件部分,你可以回退到使用 tasks
容器 API。 以下示例的 Kotlin 版本与上面使用类型安全访问器的版本严格等效
tasks.named<Jar>("jar") {
archiveFileName = "foo.jar"
}
tasks.named('jar') {
archiveFileName = 'foo.jar'
}
请注意,由于 Kotlin 是一种静态类型语言,因此有必要显式指定任务的类型。 否则,脚本将无法编译,因为推断的类型将是 Task
,而不是 Jar
,并且 archiveName
属性特定于 Jar
任务类型。
如果配置避免妨碍了你的迁移,并且你想像 Groovy 一样急切地配置任务,你可以通过在 tasks
容器上使用急切配置 API 来实现
tasks.getByName<Jar>("jar") {
archiveFileName = "foo.jar"
}
tasks.getByName('jar') {
archiveFileName = 'foo.jar'
}
有关在 Gradle Kotlin DSL 中使用容器的详细信息,请参阅此处。
如果你不知道任务的类型,那么你可以通过内置的 help
任务找到该信息。 只需使用 --task
选项将你感兴趣的任务名称传递给它,就像这样
❯ ./gradlew help --task jar
...
Type
Jar (org.gradle.api.tasks.bundling.Jar)
让我们通过一个快速的实际示例将所有这些结合在一起,该示例配置了 Spring Boot 项目的 bootJar
和 bootRun
任务
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")
}
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 访问器时,任务配置会自动变为延迟配置。
现在,为了示例起见,让我们看看使用 API 而不是类型安全访问器应用的相同配置,类型安全访问器可能不可用,具体取决于构建逻辑结构,请参阅 Gradle 用户手册中相应的文档以获取更多信息。
我们首先通过 help
任务确定 bootJar
和 bootRun
任务的类型
❯ ./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)
现在我们知道了这两个任务的类型,我们可以导入相关的类型 — BootJar
和 BootRun
— 并根据需要配置任务。 请注意,IDE 可以帮助我们完成所需的导入,因此我们只需要简单的名称,即不带完整包的名称。 这是生成的构建脚本,包含完整的导入
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")
}
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("greeting") {
doLast { println("Hello, World!") }
}
task greeting {
doLast { println 'Hello, World!' }
}
请注意,以上内容使用 Groovy 和 Kotlin DSL 都急切地配置了创建的任务。
注册或创建任务也可以在 tasks
容器上完成,分别使用 register(…)
和 create(…)
函数,如下所示
tasks.register("greeting") {
doLast { println("Hello, World!") }
}
tasks.register('greeting') {
doLast { println('Hello, World!') }
}
tasks.create("greeting") {
doLast { println("Hello, World!") }
}
tasks.create('greeting') {
doLast { println('Hello, World!') }
}
上面的示例创建了未类型化的临时任务,但你通常更希望创建特定类型的任务。 这也可以使用相同的 register()
和 create()
方法完成。 这是一个创建 Zip
类型新任务的示例
tasks.register<Zip>("docZip") {
archiveFileName = "doc.zip"
from("doc")
}
tasks.register('docZip', Zip) {
archiveFileName = 'doc.zip'
from 'doc'
}
tasks.create<Zip>("docZip") {
archiveFileName = "doc.zip"
from("doc")
}
tasks.create(name: 'docZip', type: Zip) {
archiveFileName = 'doc.zip'
from 'doc'
}
配置和依赖
在现有配置中声明依赖关系类似于在 Groovy 构建脚本中完成的方式,你可以在此示例中看到
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")
}
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
容器的成员,因此你可以像引用任何其他配置一样引用它。
找出可用配置的最简单方法是向你的 IDE 请求 configurations
容器中的建议。
你还可以使用 :kotlinDslAccessorsReport
任务,该任务打印用于访问已应用插件贡献的配置的 Kotlin 代码,并提供所有这些访问器的名称。
请注意,如果你不使用 plugins {}
块来应用你的插件,那么你将无法以通常的方式配置这些插件提供的依赖配置。 相反,你将必须对配置名称使用字符串文字,这意味着你将无法获得 IDE 支持
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")
}
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
配置,并且我们向其添加了不同的依赖项
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")
}
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 {}
块中使用字符串文字来引用它们。 以下示例演示了这两种方法
// 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 中使用。 你将在Gradle Kotlin DSL 文档的互操作性部分中找到关于如何跨越静态 Kotlin 的动态边界的秘诀,无论动态 Groovy 构建逻辑位于何处。
有两个关键的最佳实践可以更轻松地在 Kotlin DSL 的静态上下文中工作
-
使用
plugins {}
块 -
将本地构建逻辑放在构建的 buildSrc 目录中
plugins {}
块旨在保持你的构建脚本的声明性,以便充分利用 Kotlin DSL。
利用buildSrc 项目旨在将你的构建逻辑组织到可轻松测试并提供良好 IDE 支持的共享本地插件和约定中。
Kotlin DSL 构建结构示例
根据你的构建结构,你可能对以下用户手册章节感兴趣
-
编写构建脚本章节演示了如何使用
apply(from = "")
来模块化构建脚本。 -
多项目构建章节演示了各种多项目构建结构。
-
开发自定义 Gradle 插件和 Gradle Kotlin DSL 入门章节演示了如何开发自定义 Gradle 插件。
-
组合构建章节演示了如何使用复合构建。
互操作性
当在你的构建逻辑中混合使用语言时,你可能必须跨越语言边界。 一个极端的例子是构建使用 Java、Groovy 和 Kotlin 实现的任务和插件,同时还使用 Kotlin DSL 和 Groovy DSL 构建脚本。
引用 Kotlin 参考文档
Kotlin 在设计时考虑了 Java 互操作性。 现有的 Java 代码可以从 Kotlin 以自然的方式调用,Kotlin 代码也可以从 Java 中相当顺利地使用。
在 Kotlin 参考文档中,从 Kotlin 调用 Java 和 从 Java 调用 Kotlin 都得到了很好的涵盖。
同样的情况也主要适用于与 Groovy 代码的互操作性。 此外,Kotlin DSL 提供了几种选择 Groovy 语义的方法。
请在Gradle Kotlin DSL 入门的互操作性部分中找到详细文档。