如何將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函數的默認參數特性,必須傳入所有的參數。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章