如何将Gradle构建脚本语言从Groovy迁移到Kotlin

关于为何要使用Kotlin DSL来编写Gradle构建脚本大家可以看看这篇文章Kotlin Meets Gradle

总的来说Kotlin和Groovy语言有着很大的差异,但各自都有自己的优势。

Kotlin是静态类型语言,并且具有内置的空安全性,还具最牛的IDE工具(IDEA),包含从自动完成到重构之间的一切。

另一方面,Groovy本质上是高度动态的,因此非常灵活,但缺乏合适的IDE工具给予支持。

Gradle是在Java的JVM之上实现的,而Groovy DSL和Kotlin DSL都是在Gradle Java API的基础上实现的。

注意:

如果你想在开始之前先了解Kotlin语言,或许你需要一些参考资料,那么Kotlin参考文档(中文文档)就是你所需的。并且在Kotlin Koans中提供了一种有趣的方式来学习Kotlin,你在其中能快速的学习到Kotlin的各项基础知识和用法

1. 当Groovy遇到Kotlin

Kotlin语言是静态类型的,并且具有内建的空安全性,另一边Groovy本质上是高度动态的。

  • Kotlin语言比Groovy语言更加严格
  • Kotlin DSL比Groovy DSL更严格

两种DSL都提供了与Gradle的动态可扩展模型以及运行时进行交互的手段。

使用Kotlin DSL:

  • 更多的套路来实现动态化
  • 更加的安全以及更多的工具

在Gradle的最佳实践中倾向于更多的声明式构建,更少的动态构造,这正是是Kotlin大放光彩的地方,从这个意义上来说,Kotlin DSL将会鼓励并促进Gradle的这个最佳实践。

这使得在使用Kotlin DSL去应用Gradle最佳实践时将变得更加容易。

2. 品尝差异

首先,我们将从脚本的角度来看Groovy DSL和Kotlin DSL之间的主要区别。

  • 文件名
  • 插件
  • 任务处理
  • 依赖及配置
  • 属性
  • 集合与容器
  • 扩展

2.1 文件名

  • Groovy DSL脚本文件扩展名为*.gradle
  • Kotlin DSL脚本文件扩展名为*.gradle.kts

要使用Kotlin DSL,只需要将 build.gradle 改为 build.gradle.kts即可。

settings.gradle 文件也可以被重命名为settings.gradle.kts

在多项目构建中,你可以在一部分模块中使用Groovy DSL(使用build.gradle文件),在另外一些模块使用Kotlin DSL(使用build.gradle.kts文件),所以你不需要被迫同时迁移所有的东西。

2.2 使用核心插件

使用 plugin 块:

//Groovy
plugins {
    id 'java'
    id 'jacoco'
}
//Kotlin
plugins {
    java
    id("jacoco")
}

正如你在jacoco示例中所看到的,Groovy和Kotlin可以使用相同的语法(当然,除了Kotlin中必须使用的双引号和括号外)。

但是,Kotlin DSL还为所有Gradle核心插件定义了扩展属性,所以你可以直接使用它们,如上例所示的java

你也可以使用较旧的apply语法:

//Groovy
apply plugin: 'checkstyle'
//Kotlin
apply(plugin = "checkstyle")

2.3 使用外部插件

仍然使用 plugins 块:

//Groovy
plugins {
    id 'org.springframework.boot' version '2.0.1.RELEASE'
}
//Kotlin
plugins {
    id("org.springframework.boot") version "2.0.1.RELEASE"
}

较旧的apply语法:

//Groovy
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath("gradle.plugin.com.boxfuse.client:gradle-plugin-publishing:5.0.3")
    }
}

apply plugin: 'org.flywaydb.flyway'
//Kotlin
buildscript {
    repositories {
        gradlePluginPortal()
    }
    dependencies {
        classpath("gradle.plugin.com.boxfuse.client:gradle-plugin-publishing:5.0.3")
    }
}

apply(plugin = "org.flywaydb.flyway")

2.4 配置任务

在这里Groovy和Kotlin开始有所不同了,由于Kotlin是一种静态类型的语言,如果你想通过使用自动完成功能来发现可用的属性和方法从而在静态类型中受益,你需要知道并提供想要配置任务的类型。

以下将展示如何配置jar任务的单个属性:

//Groovy
jar.archiveName = 'foo.jar'
//Kotlin
tasks.getByName<Jar>("jar").archiveName = "foo.jar"

注意,明确指定任务的类型是必须,否则脚本将不会编译,因为推断的类型jar将会是Task,而且archiveName属性只是特定存在于于Jar类型中的。

不过,你若只需要配置或调用Task中声明的属性或方法,则可以省略该类型:

//Groovy
test.doLast {
    println("test completed")
}
//Kotlin
tasks["test"].doLast {
    println("test completed")
}

如果你需要在同一个任务中配置多个属性或调用多个方法,你可以按照如下方式将它们在一个块中进行分组:

//Groovy
jar {
    archiveName = 'foo.jar'
    into('META-INF') {
        from('bar')
    }
}
//Kotlin
tasks.getByName<Jar>("jar") {
    archiveName = "foo.jar"
    into("META-INF") {
        from("bar")
    }
}

但是还有另一种配置任务的方式:使用Kotlin 委托属性

如果你需要一个任务的引用以供之后使用,那么这个此功能将特别有用:

//Groovy
jar {
    archiveName = 'foo.jar'
}

jar.into('META-INF') {
    from('bar')
}
//Kotlin
val jar by tasks.getting(Jar::class) {
    archiveName = "foo.jar"
}

jar.into("META-INF") {
    from("bar")
}

再次提醒,如果你需要进行任务特定的配置,则需要提供任务的类型(例如本例中的jar)。

这意味着有时需要深入了解自定义插件的文档或源代码,以发现其自定义任务的类型,并导入它们或使用其完全限定名称。

另一种方法是使用 tasks命令来显示可用任务列表。从那里获得给定任务的类型,比如jar,在命令行使用./gradlew help --task jar,这会告诉你该任务的类型。

如果你正在使用外部插件,则尤其应当如此:

//Groovy
plugins {
    id('java')
    id 'org.springframework.boot' version '2.0.1.RELEASE'
}

repositories {
    jcenter()
}

apply plugin: 'io.spring.dependency-management'

bootJar {
    archiveName = 'app.jar'
    mainClassName = 'com.example.demo.Demo'
}

bootRun {
    main = 'com.example.demo.Demo'
    args '--spring.profiles.active=demo'
}
//Kotlin
import org.springframework.boot.gradle.tasks.bundling.BootJar
import org.springframework.boot.gradle.tasks.run.BootRun

plugins {
    java
    id("org.springframework.boot") version "2.0.1.RELEASE"
}

repositories {
    jcenter()
}

apply(plugin = "io.spring.dependency-management")

tasks {
    getByName<BootJar>("bootJar") {
        archiveName = "app.jar"
        mainClassName = "com.example.demo.Demo"
    }

    getByName<BootRun>("bootRun") {
        main = "com.example.demo.Demo"
        args("--spring.profiles.active=demo")
    }
}

在上面Kotlin版本的代码片段中,我们需要知道bootJar任务的类型是BootJarbootRun任务的类型是BootRun,然后IDE将自动帮助完成相应的导入。

2.5 创建任务

创建任务可以在tasks容器上完成:

//Groovy
task greeting {
    doLast { println("Hello, World!") }
}
//Kotlin
tasks.create("greeting") {
    doLast { println("Hello, World!") }
}

或者直接在Project上使用顶层的API函数:

//Groovy
task greeting {
    doLast { println("Hello, World!") }
}
//Kotlin
task("greeting") {
    doLast { println("Hello, World!") }
}

或者通过使用Kotlin委托属性,这在需要对创建的任务建立引用以供之后使用时非常有用:

//Groovy
task greeting {
    doLast { println("Hello, World!") }
}
//Kotlin
val greeting by tasks.creating {
    doLast { println("Hello, World!") }
}

若想创建一个给定类型的任务(例子中的Zip):

//Groovy
task docZip(type: Zip) {
    archiveName = 'doc.zip'
    from 'doc'
}
//Kotlin
tasks.create<Zip>("docZip") {
    archiveName = "doc.zip"
    from("doc")
}

使用 Project 的API 也可以达到同样的效果:

//Groovy
task docZip(type: Zip) {
    archiveName = 'doc.zip'
    from 'doc'
}
//Kotlin
task<Zip>("docZip") {
    archiveName = "doc.zip"
    from("doc")
}

或者使用Kotlin委托属性:

//Groovy
task docZip(type: Zip) {
    archiveName = 'doc.zip'
    from 'doc'
}
//Kotlin
val docZip by tasks.creating(Zip::class) {
    archiveName = "doc.zip"
    from("doc")
}

2.6 依赖及配置

在现有配置中声明依赖关系与在Groovy中没有多大区别:

//Groovy
plugins {
    id 'java'
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'io.jsonwebtoken:jjwt:0.9.0'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
//Kotlin
plugins {
    java
}
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("io.jsonwebtoken:jjwt:0.9.0")
    runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(module = "junit")
    }
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

请注意,如果不使用该plugins {}块来声明插件,那么本来应当由accessors来加载插件的方式将不可用,然后你必须通过直接写名称的方式来解决:

//Groovy
apply plugin: 'java'
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'io.jsonwebtoken:jjwt:0.9.0'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
//Kotlin
apply(plugin = "java")
dependencies {
    "implementation"("org.springframework.boot:spring-boot-starter-web")
    "implementation"("io.jsonwebtoken:jjwt:0.9.0")
    "runtimeOnly"("org.postgresql:postgresql")
    "testImplementation"("org.springframework.boot:spring-boot-starter-test") {
        exclude(module = "junit")
    }
    "testRuntimeOnly"("org.junit.jupiter:junit-jupiter-engine")
}

当然,感谢kotlin的委托属性,我们也可以将他们放到scope内

//Groovy
apply plugin: 'java'
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'io.jsonwebtoken:jjwt:0.9.0'
    runtimeOnly 'org.postgresql:postgresql'
    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude(module: 'junit')
    }
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
}
//Kotlin
apply(plugin = "java")
val implementation by configurations
val runtimeOnly by configurations
val testImplementation by configurations
val testRuntimeOnly by configurations
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("io.jsonwebtoken:jjwt:0.9.0")
    runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(module = "junit")
    }
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
}

2.7 自定义配置和依赖关系

有时你需要添加自己的配置,并为其添加依赖:

//Groovy
configurations {
    db
    integTestImplementation {
        extendsFrom testImplementation
    }
}

dependencies {
    db 'org.postgresql:postgresql'
    integTestImplementation 'com.ninja-squad:DbSetup:2.1.0'
}
//Kotlin
val db by configurations.creating
val integTestImplementation by configurations.creating {
    extendsFrom(configurations["testImplementation"])
}

dependencies {
    db("org.postgresql:postgresql")
    integTestImplementation("com.ninja-squad:DbSetup:2.1.0")
}

请注意,在上面的例子中,您只能使用db(…)integTestImplementation(…)因为它们之前都被声明为属性。如果它们是在其他地方定义的,则可以通过委托来获取它们的configurations,或者可以使用字符串的形式向配置中添加依赖项:

//Kotlin
//获取testRuntimeOnly的配置
val testRuntimeOnly by configurations

dependencies {
    testRuntimeOnly("org.postgresql:postgresql")
    "db"("org.postgresql:postgresql")
    "integTestImplementation"("com.ninja-squad:DbSetup:2.1.0")
}

2.8 扩展

很多插件都可以通过自带的扩展来配置它们。如果这些插件是通过plugins {}块来声明的,那么可以使用Kotlin扩展函数来配置它们的扩展,跟在Groovy中一样。

另一方面,如果你还在使用较旧的apply函数来声明插件(在以下例子中,对于checkstyle插件而言就是这样的),则必须使用configure<T> {}函数来配置它们:

//Groovy
jacoco {
    toolVersion = "0.8.1"
}

springBoot {
    buildInfo {
        properties {
            time = null
        }
    }
}

checkstyle {
    maxErrors = 10
}
//Kotlin
jacoco {
    toolVersion = "0.8.1"
}

springBoot {
    buildInfo {
        properties {
            time = null
        }
    }
}

configure<CheckstyleExtension> {
    maxErrors = 10
}

2.9 从动态到静态

Gradle核心提供了构建模型的基础构建块,如果你用来构建和编写插件都可以通过这些脚本和插件与该构建模型进行交互,这些交互包括对构建模型的构建( 例如添加配置,任务或扩展) 以及配置构建模型的元素(配置,任务,扩展等...)。

Gradle Java API允许在构建以及编写插件中使用任何JVM语言与构建模型进行交互。在使用Java API时,你需要查询该模型中由插件提供的元素,主要是名称,类型或两者都需要。

在Gradle Java API之上,Gradle DSL提供了更加简洁的语法。

我们来举个栗子。比方说,我们创建了一个用Java实现的Gradle插件,在其中首先声明使用了distribution插件,然后创建一个叫samplesdistributions并添加一些常规内容:

public class MyPlugin implements Plugin<Project> {
   @Override
   public void apply(final Project project) {

        project.getPlugins().apply("distribution");

        ExtensionContainer extensions = project.getExtensions();
        DistributionContainer distributions = extensions.getByType(DistributionContainer.class);
        Distribution samples = distributions.create("samples");
        samples.getContents().from(project.getLayout().getProjectDirectory().dir("src/samples"));
   }
}

它很冗长,但不要专注于此。

distribution插件为project提供了扩展并且类型是DistributionContainer。上面的示例是按照类型来查询project的扩展,然后使用它。它也可以通过名称来获取project.getExtensions().getByName("distributions")DistributionContainer在与之交互之前需要进行映射。换句话说,由插件提供扩展的模型是通过名称、类型或两者来解决的,这样有很多的约束和定义,就像要完成某种仪式一样。

然而这两种Gradle DSL的主要目标都是减少这这种仪式感。在这两个DSL中都是通过使用简洁的编程语言、语法助手和结构来实现的,这使得使用Gradle可扩展模型将更加容易。

现在让我们看看实现完全相同功能的代码,但是是使用Groovy DSL来实现:

//Groovy
plugins {
    id 'distribution'
}
distributions {
    samples {
        contents {
            from layout.projectDirectory.dir('src/samples')
        }
    }
}

然后是Kotlin DSL:

//Kotlin
plugins {
    id("distribution")
}
distributions {
    create("samples") {
        contents {
            from(layout.projectDirectory.dir("src/samples"))
        }
    }
}

在上面的两个脚本中,由distributions插件对DistributionContainer类型提供扩展只需要简单地通过名称来调用。两种DSL都提供了通过插件来解决模型元素扩展的结构。

在上面的两个脚本中,samplesdistribution都是在distributions扩展中创建和配置的,这是一个对象集合, 在Groovy DSL和Kotlin DSL都提供了语法帮助。

其中有一些差异,但关注点是相同的。

3. 迁移策略

使用Kotlin DSL的*.gradle.kts脚本和使用Groovy DSL的脚本*.gradle都可以参与相同的构建。在./buildSrc下实现的Gradle插件、构建以及通过外部获取到的都可以使用任意JVM语言,这使得可以逐步迁移,而不会阻碍团队。

机械化的迁移 vs. 通过重构以获得最佳实践:

  • 两种都有可能

  • 前者对于简单的构建就足够了

  • 一个复杂且高度动态的构建逻辑将需要进行一些重构

  • 外部插件可能无法提供良好的Kotlin DSL体验,需要寻找变通之法

4. 在Kotlin中调用Java或Groovy

5. 在Java或者Groovy中调用Kotlin

  • 从Java调用Kotlin
  • 要从Groovy调用Kotlin扩展函数,可以将其看做静态函数进行调用并将接收者(receiver)作为第一个参数传递给该静态函数。
  • 不能在groovy中使用kotlin函数的默认参数特性,必须传入所有的参数。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容