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

可以透過擴充 DefaultTask
類別並定義輸入、輸出和動作來建立自訂可操作工作。
工作輸入和輸出
可操作的工作有輸入和輸出。輸入和輸出可以是檔案、目錄或變數。
在可操作工作中
-
輸入包含檔案、資料夾和/或組態資料的集合。
例如,javaCompile
工作會採用 Java 原始碼檔案和建置指令碼組態(例如 Java 版本)等輸入。 -
輸出是指一個或多個檔案或資料夾。
例如,javaCompile
會產生類別檔案作為輸出。
然後,jar
任務會將這些類別檔案作為輸入,並產生 JAR 檔案。
清楚定義任務的輸入和輸出具有兩個目的
-
它們會告知 Gradle 任務的相依性。
例如,如果 Gradle 了解compileJava
任務的輸出會作為jar
任務的輸入,它會優先執行compileJava
。 -
它們會促進增量建置。
例如,假設 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)
java -cp 'libs/*' gradle.project.app.App
讓我們使用 task.register()
註冊一個名為 packageApp
的新任務
tasks.register<Zip>("packageApp") {
}
tasks.register(Zip, "packageApp") {
}
我們使用 Gradle 核心中的現有實作,也就是 Zip
任務實作(即 DefaultTask
的子類別)。由於我們在此註冊一個新任務,因此它尚未預先設定。我們需要設定輸入和輸出。
定義輸入和輸出是讓任務成為可執行任務的關鍵。
如果輸入是我們直接建立或編輯的檔案,例如執行檔案或 Java 原始碼,它通常會位於我們的專案目錄中的某個地方。為了確保我們使用正確的位置,我們使用 layout.projectDirectory
並定義專案目錄根目錄的相對路徑。
我們提供 jar
任務的輸出,以及所有相依項的 JAR(使用 configurations
.runtimeClasspath
)作為額外的輸入。
對於輸出,我們需要定義兩個屬性。
首先,目標目錄,它應該是建置資料夾內的目錄。我們可以透過 layout
存取它。
其次,我們需要指定一個 zip 檔案的名稱,我們稱之為 myApplication.zip
以下是完整的任務
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
}
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
生命週期任務
tasks.build {
dependsOn(packageApp)
}
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 會自動處理許多事情
import org.gradle.api.DefaultTask
public abstract class GenerateReportTask : DefaultTask() {
}
import org.gradle.api.DefaultTask
public abstract class GenerateReportTask extends DefaultTask {
}
接下來,我們使用屬性和註解來定義輸入和輸出。在此背景下,Gradle 中的屬性充當它們背後實際值的參考,允許 Gradle 追蹤任務之間的輸入和輸出。
對於我們的任務輸入,我們使用 Gradle 的 DirectoryProperty
。我們使用 @InputDirectory
註解它以指出它是任務的輸入
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
public abstract class GenerateReportTask : DefaultTask() {
@get:InputDirectory
lateinit var sourceDirectory: File
}
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.InputDirectory
public abstract class GenerateReportTask extends DefaultTask {
@InputDirectory
File sourceDirectory
}
類似地,對於輸出,我們使用 RegularFileProperty
並使用 @OutputFile
註解它。
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
}
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 存取輸入和輸出的程式碼
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}")
}
}
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
。同時,我們設定任務的輸入和輸出
tasks.register<GenerateReportTask>("generateReport") {
sourceDirectory = file("src/main")
reportFile = file("${layout.buildDirectory}/reports/directoryReport.txt")
}
tasks.build {
dependsOn("generateReport")
}
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
plugins {
id("my-convention-plugin")
}
version = "1.0"
application {
mainClass = "org.example.app.App"
}
plugins {
id 'my-convention-plugin'
}
version = '1.0'
application {
mainClass = 'org.example.app.App'
}
我們在 app
的建置檔案中定義一個名為 printVersion
的任務
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()}")
}
}
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
,我們可以註冊一個使用新任務實作的新任務
tasks.register<PrintVersion>("printVersion") {
// Configuration code
version = project.version as String
}
tasks.register(PrintVersion, "printVersion") {
// Configuration code
version = project.version.toString()
}
在任務的組態區塊內,我們可以撰寫修改任務的輸入和輸出屬性值的組態階段程式碼。任務動作在此處沒有任何方式被參照。
可以更簡潔地撰寫像這樣的簡單任務,並直接在建置指令碼中撰寫,而無需為任務建立一個獨立的類別。
讓我們註冊另一個任務,並將它命名為 printVersionDynamic
。
這次,我們沒有為任務定義類型,這表示任務將為一般類型 DefaultTask
。此一般類型沒有定義任何任務動作,表示它沒有註解為 @TaskAction
的方法。此類型對於定義「生命週期任務」很有用
tasks.register("printVersionDynamic") {
}
tasks.register("printVersionDynamic") {
}
不過,預設任務類型也可以用於動態定義具有自訂動作的任務,而無需額外的類別。這透過使用 doFirst{}
或 doLast{}
建構來完成。類似於定義方法和註解此 @TaskAction
,這會將動作新增到任務。
這些方法稱為 doFirst{}
和 doLast{}
,因為任務可以有多個動作。如果任務已經定義動作,你可以使用此區分來決定你的額外動作是否應該在現有動作之前或之後執行
tasks.register("printVersionDynamic") {
doFirst {
// Task action = Execution code
// Run before exiting actions
}
doLast {
// Task action = Execution code
// Run after existing actions
}
}
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()
陳述式
tasks.register("printVersionDynamic") {
inputs.property("version", project.version.toString())
doLast {
println("Version: ${inputs.properties["version"]}")
}
}
tasks.register("printVersionDynamic") {
inputs.property("version", project.version)
doLast {
println("Version: ${inputs.properties["version"]}")
}
}
我們在 Gradle 中看到了兩種實作自訂任務的替代方法。
動態設定讓它更簡潔。不過,在撰寫動態任務時,很容易混淆組態和執行時間狀態。你也可以看到動態任務中的「輸入」沒有類型,這可能會導致問題。當你將自訂任務實作為類別時,你可以清楚地將輸入定義為具有專用類型的屬性。
任務動作的動態修改可以為已經註冊的任務提供價值,但你因為某些原因需要修改這些任務。
讓我們以 compileJava
任務為例。
一旦任務註冊,你就無法移除它。相反地,你可以清除它的動作
tasks.compileJava {
// Clear existing actions
actions.clear()
// Add a new action
doLast {
println("Custom action: Compiling Java classes...")
}
}
tasks.compileJava {
// Clear existing actions
actions.clear()
// Add a new action
doLast {
println("Custom action: Compiling Java classes...")
}
}
另外,移除已由您使用的外掛設定的特定任務相依性也很困難,在某些情況下甚至不可能。您可修改其行為
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}")
}
}
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}")
}
}