将构建逻辑从 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 编译器可在 x86-64 架构上的 Linux、macOS、Windows、Cygwin、FreeBSD 和 Solaris 上运行。
-
了解 Kotlin 语法和基本语言功能非常有帮助。Kotlin 参考文档和Kotlin Koans应该对您有用。
-
使用
plugins {}
块声明 Gradle 插件可以显着改善编辑体验,强烈推荐。在将其转换为 Kotlin 之前,请考虑在 Groovy 构建脚本中采用它。 -
Kotlin DSL 将不支持
model {}
元素。这是已停产的 Gradle 软件模型的一部分。 -
不建议启用按需孵化配置功能,因为它可能会导致非常难以诊断的问题。
请阅读Gradle Kotlin DSL 入门了解更多内容。
如果您遇到麻烦或疑似错误,请利用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
根据模式命名应用的脚本*.settings.gradle.kts
, -
根据模式命名初始化脚本
*.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>()
函数来配置该插件。以下示例通过 在函数CheckstyleExtension
中显式声明插件的扩展类,展示了 Checkstyle 插件的工作原理configure<T>()
:
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 是否花时间创建
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
, not Jar
,并且该archiveName
属性特定于Jar
任务类型。
如果避免配置阻碍了您的迁移,并且您想要像 Groovy 一样急切地配置任务,您可以通过在容器上使用急切配置 API 来实现tasks
:
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 用户手册中的相应文档以获取更多信息。
我们首先通过任务来确定bootJar
和任务的类型:bootRun
help
❯ ./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'
}
请注意,在上面的示例中,我们只能在块中使用db(…)
and表示法,因为这两种配置都已通过方法预先声明为委托属性。如果配置是在其他地方定义的,则只能通过首先通过(而不是)创建委托属性来引用它们,或者通过在块中使用字符串文字来引用它们。以下示例演示了这两种方法:integTestImplementation(…)
dependencies {}
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 中使用它们就越困难。无论动态 Groovy 构建逻辑位于何处,您都可以在Gradle Kotlin DSL 文档的互操作性部分中找到有关如何跨越静态 Kotlin 动态边界的秘诀。
有两个关键的最佳实践可以让您在 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 互操作性。 Kotlin 可以自然地调用现有的 Java 代码,Java 也可以相当顺利地使用 Kotlin 代码。
Kotlin 参考文档中详细介绍了从Kotlin 调用 Java和从 Java 调用 Kotlin 。
这同样适用于与 Groovy 代码的互操作性。此外,Kotlin DSL 提供了多种选择 Groovy 语义的方法。