将构建逻辑从 Groovy 迁移到 Kotlin
本节将引导您将基于 Groovy 的 Gradle 构建脚本转换为 Kotlin。
Gradle 的较新 Kotlin DSL 在支持的 IDE 中提供愉快的编辑体验:内容辅助、重构、文档等等。
请阅读 Gradle Kotlin DSL 入门,了解 Gradle Kotlin DSL 的具体细节、限制和用法。 用户手册的其余部分包含构建脚本摘录,演示了 Groovy DSL 和 Kotlin DSL。这是查找如何执行此操作以及每个 DSL 的最佳位置;它涵盖了从 使用插件 到 自定义依赖项解析行为 的所有 Gradle 功能。 |
开始迁移之前
请阅读:在迁移之前,了解以下重要信息非常有帮助
-
首先,请使用最新版本的 Gradle、应用插件和您的 IDE。
-
IntelliJ IDEA 和 Android Studio 完全支持 Kotlin DSL。其他 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 之前,请考虑在您的 Groovy 构建脚本中采用它。 -
Kotlin DSL 不支持
model {}
元素。这是 已弃用的 Gradle 软件模型 的一部分。 -
不建议启用孵化配置按需功能,因为它会导致非常难以诊断的问题。
在 Gradle Kotlin DSL Primer 中了解更多信息。
如果您遇到问题或疑似错误,请利用 gradle/gradle
问题跟踪器。
您不必一次性迁移所有内容! 基于 Groovy 和 Kotlin 的构建脚本都可以 apply
彼此的脚本。您可以在 Kotlin DSL 示例 中找到对未涵盖在内的任何 Gradle 功能的灵感。
准备您的 Groovy 脚本
Kotlin 和 Groovy 语言之间的一些简单差异可能会使脚本转换变得繁琐。
-
Groovy 字符串可以用单引号
'string'
或双引号"string"
引用,而 Kotlin 要求使用双引号"string"
。 -
Groovy 允许在调用函数时省略括号,而 Kotlin 始终需要括号。
-
Gradle Groovy DSL 允许在分配属性时省略
=
赋值运算符,而 Kotlin 始终需要赋值运算符。
作为第一个迁移步骤,建议通过以下方式准备您的 Groovy 构建脚本:
-
使用双引号统一引号,
-
区分函数调用和属性分配(分别使用括号和赋值运算符)。
前者可以通过搜索 '
并替换为 "
来轻松完成。例如,
group 'com.acme'
dependencies {
implementation 'com.acme:example:1.0'
}
变为
group "com.acme"
dependencies {
implementation "com.acme:example:1.0"
}
下一步稍微复杂一些,因为在 Groovy 脚本中区分函数调用和属性分配可能并不容易。一个好的策略是首先将所有模棱两可的语句设为属性分配,然后通过将失败的语句转换为函数调用来修复构建。
例如,
group "com.acme"
dependencies {
implementation "com.acme:example:1.0"
}
变为
group = "com.acme" (1)
dependencies {
implementation("com.acme:example:1.0") (2)
}
1 | 属性分配 |
2 | 函数调用 |
虽然仍然是有效的 Groovy,但现在它不再模棱两可,并且接近 Kotlin 语法,这使得将脚本重命名为 Gradle Kotlin DSL 脚本变得更加容易。
需要注意的是,虽然 Groovy 额外属性可以使用对象的 ext
属性进行修改,但在 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>()
函数来配置该插件。以下示例展示了如何通过在 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 任务 API 和新 Gradle 任务 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
容器的成员可用,因此您可以像引用任何其他配置一样引用它。
找出哪些配置可用的最简单方法是在 configurations
容器中询问您的 IDE 以获取建议。
您还可以使用 :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 调用 Java 和从 Java 调用 Kotlin 在 Kotlin 参考文档中都有很好的介绍。
与 Groovy 代码的互操作性也基本相同。此外,Kotlin DSL 提供了几种方法来选择使用 Groovy 语义。
请在 Gradle Kotlin DSL Primer 的互操作性部分找到详细的文档。