可操作的工作描述 Gradle 中的工作。這些工作有動作。在 Gradle 核心,compileJava 工作會編譯 Java 原始碼。JarZip 工作會將檔案壓縮成封存檔。

writing tasks 3

可以透過擴充 DefaultTask 類別並定義輸入、輸出和動作來建立自訂可操作工作。

工作輸入和輸出

可操作的工作有輸入和輸出。輸入和輸出可以是檔案、目錄或變數。

在可操作工作中

  • 輸入包含檔案、資料夾和/或組態資料的集合。
    例如,javaCompile 工作會採用 Java 原始碼檔案和建置指令碼組態(例如 Java 版本)等輸入。

  • 輸出是指一個或多個檔案或資料夾。
    例如,javaCompile 會產生類別檔案作為輸出。

然後,jar 任務會將這些類別檔案作為輸入,並產生 JAR 檔案。

清楚定義任務的輸入和輸出具有兩個目的

  1. 它們會告知 Gradle 任務的相依性。
    例如,如果 Gradle 了解 compileJava 任務的輸出會作為 jar 任務的輸入,它會優先執行 compileJava

  2. 它們會促進增量建置。
    例如,假設 Gradle 辨識到任務的輸入和輸出保持不變。在這種情況下,它可以利用前一次建置執行或建置快取的結果,完全避免重新執行任務動作。

當您套用外掛程式(例如 java-library 外掛程式)時,Gradle 會自動註冊一些任務,並使用預設值設定它們。

讓我們在一個假想的範例專案中定義一個任務,將 JAR 和啟動指令碼封裝到一個檔案中

gradle-project
├── app
│   ├── build.gradle.kts    // app build logic
│   ├── run.sh              // script file
│   └── ...                 // some java code
├── settings.gradle.kts     // includes app subproject
├── gradle
├── gradlew
└── gradlew.bat
gradle-project
├── app
│   ├── build.gradle    // app build logic
│   ├── run.sh          // script file
│   └── ...             // some java code
├── settings.gradle     // includes app subproject
├── gradle
├── gradlew
└── gradlew.bat

run.sh 指令碼可以從建置中執行 Java 應用程式(一旦封裝為 JAR)

app/run.sh
java -cp 'libs/*' gradle.project.app.App

讓我們使用 task.register() 註冊一個名為 packageApp 的新任務

app/build.gradle.kts
tasks.register<Zip>("packageApp") {

}
app/build.gradle
tasks.register(Zip, "packageApp") {

}

我們使用 Gradle 核心中的現有實作,也就是 Zip 任務實作(即 DefaultTask 的子類別)。由於我們在此註冊一個新任務,因此它尚未預先設定。我們需要設定輸入和輸出。

定義輸入和輸出是讓任務成為可執行任務的關鍵。

對於 Zip 任務類型,我們可以使用 from() 方法將檔案新增到輸入中。在我們的案例中,我們新增執行指令碼。

如果輸入是我們直接建立或編輯的檔案,例如執行檔案或 Java 原始碼,它通常會位於我們的專案目錄中的某個地方。為了確保我們使用正確的位置,我們使用 layout.projectDirectory 並定義專案目錄根目錄的相對路徑。

我們提供 jar 任務的輸出,以及所有相依項的 JAR(使用 configurations.runtimeClasspath)作為額外的輸入。

對於輸出,我們需要定義兩個屬性。

首先,目標目錄,它應該是建置資料夾內的目錄。我們可以透過 layout 存取它。

其次,我們需要指定一個 zip 檔案的名稱,我們稱之為 myApplication.zip

以下是完整的任務

app/build.gradle.kts
val packageApp = tasks.register<Zip>("packageApp") {
    from(layout.projectDirectory.file("run.sh"))                // input - run.sh file
    from(tasks.jar) {                                           // input - jar task output
        into("libs")
    }
    from(configurations.runtimeClasspath) {                     // input - jar of dependencies
        into("libs")
    }
    destinationDirectory.set(layout.buildDirectory.dir("dist")) // output - location of the zip file
    archiveFileName.set("myApplication.zip")                    // output - name of the zip file
}
app/build.gradle
def packageApp = tasks.register(Zip, 'packageApp') {
    from layout.projectDirectory.file('run.sh')                 // input - run.sh file
    from tasks.jar {                                            // input - jar task output
        into 'libs'
    }
    from configurations.runtimeClasspath {                      // input - jar of dependencies
        into 'libs'
    }
    destinationDirectory.set(layout.buildDirectory.dir('dist')) // output - location of the zip file
    archiveFileName.set('myApplication.zip')                    // output - name of the zip file
}

如果我們執行我們的 packageApp 任務,就會產生 myApplication.zip

$./gradlew :app:packageApp

> Task :app:compileJava
> Task :app:processResources NO-SOURCE
> Task :app:classes
> Task :app:jar
> Task :app:packageApp

BUILD SUCCESSFUL in 1s
3 actionable tasks: 3 executed

Gradle 執行了一些任務來建立 JAR 檔案,其中包括編譯 app 專案的程式碼和編譯程式碼相依性。

查看新建立的 ZIP 檔案,我們可以看到它包含執行 Java 應用程式所需的一切

> unzip -l ./app/build/dist/myApplication.zip

Archive:  ./app/build/dist/myApplication.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       42  01-31-2024 14:16   run.sh
        0  01-31-2024 14:22   libs/
      847  01-31-2024 14:22   libs/app.jar
  3041591  01-29-2024 14:20   libs/guava-32.1.2-jre.jar
     4617  01-29-2024 14:15   libs/failureaccess-1.0.1.jar
     2199  01-29-2024 14:15   libs/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar
    19936  01-29-2024 14:15   libs/jsr305-3.0.2.jar
   223979  01-31-2024 14:16   libs/checker-qual-3.33.0.jar
    16017  01-31-2024 14:16   libs/error_prone_annotations-2.18.0.jar
---------                     -------
  3309228                     9 files

可操作的任務應該連線到生命週期任務,這樣開發人員只需要執行生命週期任務即可。

到目前為止,我們直接呼叫新的任務。讓我們將它連線到生命週期任務。

將下列內容新增到建置指令碼,以便使用 dependsOn()packageApp 可操作任務連線到 build 生命週期任務

app/build.gradle.kts
tasks.build {
    dependsOn(packageApp)
}
app/build.gradle
tasks.build {
    dependsOn(packageApp)
}

我們看到執行 :build 也會執行 :packageApp

$ ./gradlew :app:build

> Task :app:compileJava UP-TO-DATE
> Task :app:processResources NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:jar UP-TO-DATE
> Task :app:startScripts
> Task :app:distTar
> Task :app:distZip
> Task :app:assemble
> Task :app:compileTestJava
> Task :app:processTestResources NO-SOURCE
> Task :app:testClasses
> Task :app:test
> Task :app:check
> Task :app:packageApp
> Task :app:build

BUILD SUCCESSFUL in 1s
8 actionable tasks: 6 executed, 2 up-to-date

您可以在需要時定義自己的生命週期任務。

透過擴充 DefaultTask 來執行任務

為了滿足更多個別需求,而且如果沒有現有的外掛程式提供您需要的建置功能,您可以建立自己的任務執行。

執行一個類別表示建立一個自訂類別(也就是 類型),這是透過子類化 DefaultTask 來完成的

讓我們從 Gradle init 為一個簡單的 Java 應用程式建立的範例開始,其中原始程式碼在 app 子專案中,而共用的建置邏輯在 buildSrc

gradle-project
├── app
│   ├── build.gradle.kts
│   └── src                 // some java code
│       └── ...
├── buildSrc
│   ├── build.gradle.kts
│   ├── settings.gradle.kts
│   └── src                 // common build logic
│       └── ...
├── settings.gradle.kts
├── gradle
├── gradlew
└── gradlew.bat
gradle-project
├── app
│   ├── build.gradle
│   └── src             // some java code
│       └── ...
├── buildSrc
│   ├── build.gradle
│   ├── settings.gradle
│   └── src             // common build logic
│       └── ...
├── settings.gradle
├── gradle
├── gradlew
└── gradlew.bat

我們在 ./buildSrc/src/main/kotlin/GenerateReportTask.kt./buildSrc/src/main/groovy/GenerateReportTask.groovy 中建立一個名為 GenerateReportTask 的類別。

為了讓 Gradle 知道我們正在執行一個任務,我們擴充了 Gradle 附帶的 DefaultTask 類別。讓我們的任務類別成為 abstract 也很有幫助,因為 Gradle 會自動處理許多事情

buildSrc/src/main/kotlin/GenerateReportTask.kt
import org.gradle.api.DefaultTask

public abstract class GenerateReportTask : DefaultTask() {

}
buildSrc/src/main/groovy/GenerateReportTask.groovy
import org.gradle.api.DefaultTask

public abstract class GenerateReportTask extends DefaultTask {

}

接下來,我們使用屬性和註解來定義輸入和輸出。在此背景下,Gradle 中的屬性充當它們背後實際值的參考,允許 Gradle 追蹤任務之間的輸入和輸出。

對於我們的任務輸入,我們使用 Gradle 的 DirectoryProperty。我們使用 @InputDirectory 註解它以指出它是任務的輸入

buildSrc/src/main/kotlin/GenerateReportTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory

public abstract class GenerateReportTask : DefaultTask() {

    @get:InputDirectory
    lateinit var sourceDirectory: File

}
buildSrc/src/main/groovy/GenerateReportTask.groovy
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory

public abstract class GenerateReportTask extends DefaultTask {

    @InputDirectory
    File sourceDirectory

}

類似地,對於輸出,我們使用 RegularFileProperty 並使用 @OutputFile 註解它。

buildSrc/src/main/kotlin/GenerateReportTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile

public abstract class GenerateReportTask : DefaultTask() {

    @get:InputDirectory
    lateinit var sourceDirectory: File

    @get:OutputFile
    lateinit var reportFile: File

}
buildSrc/src/main/groovy/GenerateReportTask.groovy
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile

public abstract class GenerateReportTask extends DefaultTask {

    @InputDirectory
    File sourceDirectory

    @OutputFile
    File reportFile

}

在定義輸入和輸出後,唯一剩下的就是實際任務動作,它在使用 @TaskAction 註解的方法中實作。在此方法內,我們撰寫使用 Gradle 特定 API 存取輸入和輸出的程式碼

buildSrc/src/main/kotlin/GenerateReportTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

public abstract class GenerateReportTask : DefaultTask() {

    @get:InputDirectory
    lateinit var sourceDirectory: File

    @get:OutputFile
    lateinit var reportFile: File

    @TaskAction
    fun generateReport() {
        val fileCount = sourceDirectory.listFiles().count { it.isFile }
        val directoryCount = sourceDirectory.listFiles().count { it.isDirectory }

        val reportContent = """
            |Report for directory: ${sourceDirectory.absolutePath}
            |------------------------------
            |Number of files: $fileCount
            |Number of subdirectories: $directoryCount
        """.trimMargin()

        reportFile.writeText(reportContent)
        println("Report generated at: ${reportFile.absolutePath}")
    }
}
buildSrc/src/main/groovy/GenerateReportTask.groovy
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

public abstract class GenerateReportTask extends DefaultTask {

    @InputDirectory
    File sourceDirectory

    @OutputFile
    File reportFile

    @TaskAction
    void generateReport() {
        def fileCount = sourceDirectory.listFiles().count { it.isFile() }
        def directoryCount = sourceDirectory.listFiles().count { it.isDirectory() }

        def reportContent = """
            Report for directory: ${sourceDirectory.absolutePath}
            ------------------------------
            Number of files: $fileCount
            Number of subdirectories: $directoryCount
        """.trim()

        reportFile.text = reportContent
        println("Report generated at: ${reportFile.absolutePath}")
    }
}

任務動作會產生 sourceDirectory 中檔案的報告。

在應用程式建置檔案中,我們使用 task.register() 註冊 GenerateReportTask 類型的任務,並將其命名為 generateReport。同時,我們設定任務的輸入和輸出

app/build.gradle.kts
tasks.register<GenerateReportTask>("generateReport") {
    sourceDirectory = file("src/main")
    reportFile = file("${layout.buildDirectory}/reports/directoryReport.txt")
}

tasks.build {
    dependsOn("generateReport")
}
app/build.gradle
import org.gradle.api.tasks.Copy

tasks.register(GenerateReportTask, "generateReport") {
    sourceDirectory = file("src/main")
    reportFile = file("${layout.buildDirectory}/reports/directoryReport.txt")
}

tasks.build.dependsOn("generateReport")

generateReport 任務已連接到 build 任務。

透過執行建置,我們觀察到我們的啟動指令碼產生任務已執行,並且在後續建置中為 UP-TO-DATE。Gradle 的增量建置和快取機制可與自訂任務無縫運作

./gradlew :app:build
> Task :buildSrc:checkKotlinGradlePluginConfigurationErrors
> Task :buildSrc:compileKotlin UP-TO-DATE
> Task :buildSrc:compileJava NO-SOURCE
> Task :buildSrc:compileGroovy NO-SOURCE
> Task :buildSrc:pluginDescriptors UP-TO-DATE
> Task :buildSrc:processResources NO-SOURCE
> Task :buildSrc:classes UP-TO-DATE
> Task :buildSrc:jar UP-TO-DATE
> Task :app:compileJava UP-TO-DATE
> Task :app:processResources NO-SOURCE
> Task :app:classes UP-TO-DATE
> Task :app:jar UP-TO-DATE
> Task :app:startScripts UP-TO-DATE
> Task :app:distTar UP-TO-DATE
> Task :app:distZip UP-TO-DATE
> Task :app:assemble UP-TO-DATE
> Task :app:compileTestJava UP-TO-DATE
> Task :app:processTestResources NO-SOURCE
> Task :app:testClasses UP-TO-DATE
> Task :app:test UP-TO-DATE
> Task :app:check UP-TO-DATE

> Task :app:generateReport
Report generated at: ./app/build/reports/directoryReport.txt

> Task :app:packageApp
> Task :app:build

BUILD SUCCESSFUL in 1s
13 actionable tasks: 10 executed, 3 up-to-date

任務動作

任務動作是實作任務執行的程式碼,如前一節所示。例如,javaCompile 任務動作會呼叫 Java 編譯器將原始碼轉換為位元組碼。

可以動態修改已註冊任務的任務動作。這有助於測試、修補或修改核心建置邏輯。

我們來看一個簡單 Gradle 建置的範例,其中一個 app 子專案組成一個 Java 應用程式,包含一個 Java 類別並使用 Gradle 的 application 外掛程式。專案在 buildSrc 資料夾中有共用建置邏輯,其中包含 my-convention-plugin

app/build.gradle.kts
plugins {
    id("my-convention-plugin")
}

version = "1.0"

application {
    mainClass = "org.example.app.App"
}
app/build.gradle
plugins {
    id 'my-convention-plugin'
}

version = '1.0'

application {
    mainClass = 'org.example.app.App'
}

我們在 app 的建置檔案中定義一個名為 printVersion 的任務

buildSrc/src/main/kotlin/PrintVersion.kt
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

abstract class PrintVersion : DefaultTask() {

    // Configuration code
    @get:Input
    abstract val version: Property<String>

    // Execution code
    @TaskAction
    fun print() {
        println("Version: ${version.get()}")
    }
}
buildSrc/src/main/groovy/PrintVersion.groovy
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

abstract class PrintVersion extends DefaultTask {

    // Configuration code
    @Input
    abstract Property<String> getVersion()

    // Execution code
    @TaskAction
    void printVersion() {
        println("Version: ${getVersion().get()}")
    }
}

此任務執行一項簡單的工作:它會將專案版本印出到命令列。

此類別會延伸 DefaultTask,並且有一個 @Input,其類型為 Property<String>。它有一個使用 @TaskAction 註解的方法,用於印出版本。

請注意,任務實作清楚區分「設定程式碼」和「執行程式碼」。

設定程式碼會在 Gradle 的設定階段執行。它會在記憶體中建立專案模型,以便 Gradle 知道它需要針對特定建置呼叫執行哪些動作。任務動作周圍的所有內容,例如輸入或輸出屬性,都是此設定程式碼的一部分。

任務動作方法內的程式碼是執行實際工作的執行程式碼。如果任務是任務圖的一部分,且無法略過,因為它是 UP-TO-DATE 或已從快取中取得,則它會存取輸入和輸出以執行一些工作。

一旦任務實作完成,它就可以在建置設定中使用。在我們的慣例外掛程式中,my-convention-plugin,我們可以註冊一個使用新任務實作的新任務

app/build.gradle.kts
tasks.register<PrintVersion>("printVersion") {

    // Configuration code
    version = project.version as String
}
app/build.gradle
tasks.register(PrintVersion, "printVersion") {

    // Configuration code
    version = project.version.toString()
}

在任務的組態區塊內,我們可以撰寫修改任務的輸入和輸出屬性值的組態階段程式碼。任務動作在此處沒有任何方式被參照。

可以更簡潔地撰寫像這樣的簡單任務,並直接在建置指令碼中撰寫,而無需為任務建立一個獨立的類別。

讓我們註冊另一個任務,並將它命名為 printVersionDynamic

這次,我們沒有為任務定義類型,這表示任務將為一般類型 DefaultTask。此一般類型沒有定義任何任務動作,表示它沒有註解為 @TaskAction 的方法。此類型對於定義「生命週期任務」很有用

app/build.gradle.kts
tasks.register("printVersionDynamic") {

}
app/build.gradle
tasks.register("printVersionDynamic") {

}

不過,預設任務類型也可以用於動態定義具有自訂動作的任務,而無需額外的類別。這透過使用 doFirst{}doLast{} 建構來完成。類似於定義方法和註解此 @TaskAction,這會將動作新增到任務。

這些方法稱為 doFirst{}doLast{},因為任務可以有多個動作。如果任務已經定義動作,你可以使用此區分來決定你的額外動作是否應該在現有動作之前或之後執行

app/build.gradle.kts
tasks.register("printVersionDynamic") {
    doFirst {
        // Task action = Execution code
        // Run before exiting actions
    }
    doLast {
        // Task action = Execution code
        // Run after existing actions
    }
}
app/build.gradle
tasks.register("printVersionDynamic") {
    doFirst {
        // Task action = Execution code
        // Run before exiting actions
    }
    doLast {
        // Task action = Execution code
        // Run after existing actions
    }
}

如果你只有一個動作,這是這裡的情況,因為我們從一個空的任務開始,我們通常使用 doLast{} 方法。

在任務中,我們首先將我們想要列印的版本動態宣告為輸入。我們使用所有任務都具有的常規輸入屬性,而不是宣告屬性並使用 @Input 註解它。然後,我們在 doLast{} 方法內新增動作程式碼,一個 println() 陳述式

app/build.gradle.kts
tasks.register("printVersionDynamic") {
    inputs.property("version", project.version.toString())
    doLast {
        println("Version: ${inputs.properties["version"]}")
    }
}
app/build.gradle
tasks.register("printVersionDynamic") {
    inputs.property("version", project.version)
    doLast {
        println("Version: ${inputs.properties["version"]}")
    }
}

我們在 Gradle 中看到了兩種實作自訂任務的替代方法。

動態設定讓它更簡潔。不過,在撰寫動態任務時,很容易混淆組態和執行時間狀態。你也可以看到動態任務中的「輸入」沒有類型,這可能會導致問題。當你將自訂任務實作為類別時,你可以清楚地將輸入定義為具有專用類型的屬性。

任務動作的動態修改可以為已經註冊的任務提供價值,但你因為某些原因需要修改這些任務。

讓我們以 compileJava 任務為例。

一旦任務註冊,你就無法移除它。相反地,你可以清除它的動作

app/build.gradle.kts
tasks.compileJava {
    // Clear existing actions
    actions.clear()

    // Add a new action
    doLast {
        println("Custom action: Compiling Java classes...")
    }
}
app/build.gradle
tasks.compileJava {
    // Clear existing actions
    actions.clear()

    // Add a new action
    doLast {
        println("Custom action: Compiling Java classes...")
    }
}

另外,移除已由您使用的外掛設定的特定任務相依性也很困難,在某些情況下甚至不可能。您可修改其行為

app/build.gradle.kts
tasks.compileJava {
    // Modify the task behavior
    doLast {
        val outputDir = File("$buildDir/compiledClasses")
        outputDir.mkdirs()

        val compiledFiles = sourceSets["main"].output.files
        compiledFiles.forEach { compiledFile ->
            val destinationFile = File(outputDir, compiledFile.name)
            compiledFile.copyTo(destinationFile, true)
        }

        println("Java compilation completed. Compiled classes copied to: ${outputDir.absolutePath}")
    }
}
app/build.gradle
tasks.compileJava {
    // Modify the task behavior
    doLast {
        def outputDir = file("$buildDir/compiledClasses")
        outputDir.mkdirs()

        def compiledFiles = sourceSets["main"].output.files
        compiledFiles.each { compiledFile ->
            def destinationFile = new File(outputDir, compiledFile.name)
            compiledFile.copyTo(destinationFile)
        }

        println("Java compilation completed. Compiled classes copied to: ${outputDir.absolutePath}")
    }
}