如何将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函数的默认参数特性,必须传入所有的参数。
    原文作者:Acker飏
    原文地址: https://www.jianshu.com/p/9fe1a8605c32
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞