理解Gradle中的任務
Gradle的構建過程基于任務(Task)的概念,而每個任務都可以包含一個或多個動作(Action)。
任務是構建中執行的一些獨立的工作單元,例如編譯類、創建JAR、生成Javadoc或將存檔發布到倉庫。
我們使用gradle taskName或gradlew taskName來執行任務,比如build:
$ ./gradlew build?
項目中所有可用任務都是來自Gradle插件和構建腳本。
比如在子項目構建腳本中創建的任務:
可用任務清單
我們通過運行tasks任務來列出項目所有可用任務。
$ ./gradlew tasks
讓我們以一個非常基本的Gradle項目為例,該項目具有以下結構:
?settings.gradle.kts文件定義了根項目名稱和app子項目:
rootProject.name = "HelloGradle"
include("app")
目前app子項目的構建文件暫時是一個空文件。
為了看app子項目中的任務,運行./gradlew :app:tasks?
結果中只能看到少量的輔助任務(Help Tasks),它們是Gradle核心提供的用于分析構建的任務。其他任務,如構建項目或編譯代碼的任務,則是由插件來添加的。
我們接下來在app構建腳本里添加application插件:
//app/build.gradle.kts
plugins {
? ? id("application")
}
application插件會添加一些生命周期任務。 現在再運行./gradlew app:tasks,我們看到多了一些和構建相關的任務,比如assemble?和?build任務:
任務結果
當Gradle執行一個任務時,它會在控制臺中顯示任務的結果標簽。
這些標簽描述了一個任務是否有要執行的動作,Gradle是否執行了這些動作。動作包括但不限于編譯代碼、壓縮文件和發布存檔。
(no label)?or?EXECUTED
任務執行了動作。
任務有動作,Gradle執行了它們。
任務沒有動作,有一些依賴項,Gradle執行了一個或多個依賴項。
UP-TO-DATE
Gradle會檢查任務的輸入和輸出,如果輸入沒有變化,并且輸出已經存在且是最新的,那么任務就不會被執行,并標記為UP-TO-DATE。
任務有輸出和輸入,但都沒有改變。
任務有動作,但是告訴Gradle它不會改變輸出。
任務沒有動作,有一些依賴項,但所有依賴都是UP-TO-DATE,?SKIPPED?或?FROM-CACHE的。
任務沒有動作,也沒有任何依賴項。
FROM-CACHE
Gradle檢測到任務的輸出已經在構建緩存中時,會直接從緩存中加載,比如build cache。?
SKIPPED
任務不執行他的動作
任務被跳過可能是因為某個先決條件未滿足,或者任務被明確地配置為跳過,比如使用命令行選項--exclude-task排除。
NO_SOURCE
任務不需要執行他的動作
任務有輸入和輸出,但沒有源文件。
FROM-CACHE和UP-TO-DATE都是Gradle優化構建過程的手段,都有助于減少不必要的任務執行,提高構建速度。可能會弄不明白什么時候是UP-TO-DATE或者FROM-CACHE,在本文后面介紹緩存任務的時候,我們再做進一步說明。?
任務組group和描述description
任務組和描述用于組織和描述任務。
Groups
任務組被用來對任務進行分類,當運行./gradlew tasks時,所有任務會被列在各自的組中,這樣更容易理解它們的目的和與其他任務的關系,使用group屬性設置組。
Descriptions
描述提供了任務功能的簡要解釋。當運行./gradlew tasks時,描述會顯示在每個任務的旁邊,幫助開發者了解它的用途以及如何使用它,使用description屬性設置描述。
我們回頭去看一下前面gradle-project執行tasks的輸出結果:
:run任務屬于Application分組, 對其描述是 "Runs this project as a JVM application"。?
這個任務在代碼中的定義會像這樣:
//app/build.gradle.kts
tasks.register("run") {
? ? group = "Application"
? ? description = "Runs this project as a JVM application."
}
私有和隱藏任務
Gradle不支持將任務標記為“私有”。
當我們運行:tasks時,默認只會顯示那些分配了任務組的任務,即所謂的可見任務,而那些沒有分配group的任務,就是隱藏任務, 需要注意,隱藏任務依舊可以被Gradle執行,只是不顯示而已。
如下所示,我們創建了一個任務helloTask,執行./gradlew :app:tasks,任務列表里并沒有找到helloTask任務。
//app/build.gradle.kts
tasks.register("helloTask") {
? ? println("Hello")
}
給它分配一個group?
tasks.register("helloTask") {
? ? group = "Other"
? ? description = "Hello task"
? ? println("Hello")
}
任務就出現在了指定的Other分組下?:
執行./gradlew tasks --all 可以顯示所有任務,包括隱藏的。?
比如上面的helloHiddenTask,我們沒有設置group屬性,也顯示在了Other分組下。?
分組任務
如果我們想要自定義執行tasks時向用戶顯示哪些任務,可以對任務分組并設置每個組的可見性。
示例gradle-project雖然只是一個簡單的Java應用,列出的可用的任務卻非常多,使用構建的開發人員很少直接需要其中的許多任務。
我們可以通過配置tasks任務,限制任務顯示到一個特定的分組。
我們修改一下構建腳本,創建一個自己的分組,使用displayGroup屬性來指定要顯示的任務組。
//app/build.gradle.kts
val myBuildGroup = "my app build" ? ? ? ? ? ? ? // Create a group name
tasks.register<TaskReportTask>("tasksAll") {? ? // Register the tasksAll task
? ? group = myBuildGroup
? ? description = "Show additional tasks."
? ? setShowDetail(true)
? ? println("register tasksAll")
}
tasks.named<TaskReportTask>("tasks") {?
? ? displayGroup = myBuildGroup
}
在Gradle中,我們執行tasks任務時,會使用此類型的一個實例TaskReportTask,其中displayGroup屬性用來控制要顯示的任務組,默認值是null, 可以使用命令行選項 '--group'設置,設置后就只顯示這個分組的任務。
任務類別?
Gradle中的任務分為兩類:
1. Actionable tasks(可操作任務)
2. Lifecycle tasks(生命周期任務)
可執行任務定義了Gradle應該執行的具體操作。例如,:compileJava?任務,它編譯項目的Java代碼。這些操作包括創建JAR文件、壓縮文件、發布歸檔文件等。當你應用了一個插件,如java-library,Gradle會自動添加與該插件相關的可執行任務。
生命周期任務定義了一系列的目標(targets),你可以調用這些目標來執行一系列的操作。例如,:build?就是一個常見的生命周期任務,用于構建整個項目。這類任務本身不執行具體的操作(actions)。相反,它們捆綁了可執行任務,當調用生命周期任務時,會觸發與之關聯的可執行任務。 基礎Gradle插件(base Gradle plugin)只添加生命周期任務。這意味著如果你沒有添加任何插件,Gradle仍然會提供這些基本的生命周期任務。
我們再看一下之前例子的:tasks結果
如果我們執行:build任務,會看到有好幾個任務都被執行了,包括:app:compileJava任務。
可以表述為可執行任務:compileJava捆綁到了生命周期任務:build中。?
增量任務
Gradle任務的一個關鍵特征是它們的增量性。
Gradle可以重用之前構建的結果。因此,如果我們之前構建過項目,并且只進行了小幅更改,那么重新運行:build將不需要Gradle執行大量工作。
例如,如果我們只修改項目中的測試代碼,保持生產代碼不變,執行構建將僅重新編譯測試代碼。Gradle將生產代碼的任務標記為UP-TO-DATE,表明自上次成功構建以來,它保持不變:
緩存任務
Gradle可以使用構建緩存來重用過去構建的結果。
要啟用此功能,請使用--build-cache命令行參數或在gradle.properties文件中設置org.gradle.caching=true來激活構建緩存。
此優化有可能顯著加速項目的構建:
當Gradle可以從緩存中獲取一個任務的輸出時,它會給這個任務貼上FROM-CACHE的標簽。如果經常在分支之間切換,構建緩存很方便。Gradle支持本地和遠程構建緩存。
我們通過實際執行其中的compileJava源碼編譯任務來加深對任務結果為UP-TO-DATE和FROM-CACHE的理解:
我們把之前的gradle-project例子修改一下
//app/build.gradle.kts
//添加執行入口類
application {
? ? mainClass = "gradle.project.App"
}
//app/src/main/java/gradle/project/App.java
package gradle.project;
public class App {
? ? public static void main(String[] args) {
? ? ? ? System.out.println("Hello World!");
?? }
}
現在app子項目是一個可執行的Java 應用程序,App類是應用的執行入口。
先./gradlew clean刪除各子項目build目錄,確保項目是干凈的狀態。
執行compileJava任務編譯java,? --build-cache告訴Gradle本次使用構建緩存,需要--info選項顯示一些額外的構建信息:
./gradlew --build-cache compileJava --info
Settings evaluated using settings file '/Users/roy/Downloads/gradle-project/settings.gradle'.
Using local directory build cache for the root build (location = /Users/xxx/.gradle/caches/build-cache-1, removeUnusedEntriesAfter = 7 days).
Projects loaded. Root project using build file '/Users/xxx/Downloads/gradle-project/build.gradle'.
> Task :app:compileJava
Stored cache entry for task ':app:compileJava' with cache key db35214e1886f8b0ebbcc16e2fa7a618
BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed
?執行成功后,app子項目里多了build目錄,輸出的信息里也看到了build cache目錄默認的位置Gradle User Home/caches/build-cache-1/,在其中緩存了任務的輸出。
再次執行
./gradlew --build-cache compileJava --info
> Task :app:compileJava UP-TO-DATE
Build cache key for task ':app:compileJava' is 3804aa4dacefba7c96c077f8de82ae3d
Skipping task ':app:compileJava' as it is up-to-date.
BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date
因為我們沒有做任何改動,build目錄里已經最新的輸出了,所以compileJava任務會跳過,此時任務被標記為UP-TO-DATE。
執行./gradlew clean刪除app的build目錄,然后再執行
./gradlew --build-cache compileJava --info?
> Task :app:compileJava FROM-CACHE
Build cache key for task ':app:compileJava' is db35214e1886f8b0ebbcc16e2fa7a618
Task ':app:compileJava' is not up-to-date because:
? Output property 'destinationDirectory' file /Users/xx/Downloads/gradle-project/app/build/classes/java/main has been removed.
? Output property 'destinationDirectory' file /Users/xx/Downloads/gradle-project/app/build/classes/java/main/gradle has been removed.
? Output property 'destinationDirectory' file /Users/xx/Downloads/gradle-project/app/build/classes/java/main/gradle/project has been removed.
? Output property 'options.generatedSourceOutputDirectory' file /Users/xx/Downloads/gradle-project/app/build/generated/sources/annotationProcessor/java/main has been removed.
Loaded cache entry for task ':app:compileJava' with cache key db35214e1886f8b0ebbcc16e2fa7a618
BUILD SUCCESSFUL in 1s
1 actionable task: 1 from cache
任務標記為FROM-CACHE,并且從緩存中加載了上次執行的輸出,日志中也有提示:
Loaded cache entry for task ':app:compileJava' with cache key db35214e1886f8b0ebbcc16e2fa7a618
?另外,app的build目錄也有了,里面的輸出應該就是直接從build cache中拿過來的。
?再再一次執行./gradlew --build-cache compileJava --info, 任務又是UP-TO-DATE了。
> Task :app:compileJava UP-TO-DATE
所以,對于要執行的任務,只要項目里已經存在最新輸出,它就是UP-TO-DATE;否則如果任務啟用構建緩存,并且在緩存里有最新輸出,就是FROM-CACHE。?
可緩存任務和不可緩存任務
在Gradle中并非所有任務都可以或應該被緩存,除了少數內置任務是可緩存外,大部分任務由于各種原因(如不可預測的輸出、外部依賴、任務配置等)可能不適合緩存。這些任務通常被標記為non-cacheable。
使用--build-cache選項可以讓Gradle啟用構建緩存功能。當這個選項被啟用時,Gradle會嘗試緩存可緩存任務的輸出,并在后續構建中重用這些輸出。
對于non-cacheable任務,Gradle會忽略構建緩存機制,并總是執行這些任務。通常是因為這些任務的輸出可能依賴于不可預測或不可重復的因素,或者任務本身的配置不允許緩存。
開發者也可以顯式地配置這類任務使其可緩存。
要確定一個任務是否可緩存,你可以查看任務的輸出。如果任務有一個buildCacheable屬性,并且它被設置為true,那么該任務就是可緩存的。如果任務沒有明確的buildCacheable屬性設置,或者它被設置為false,那么該任務就是non-cacheable的。
構建緩存的有效性還取決于構建環境的穩定性和一致性。如果構建環境經常變化(例如,使用了不同的構建機器或文件系統),那么構建緩存的效果可能會受到限制。因此,在使用構建緩存時,確保構建環境的一致性是非常重要的。
開發任務
在開發Gradle任務時,我們有兩個選擇:
1.使用現有的Gradle任務類型,比如Zip,Copy或Delete
2.創建自己的任務類型,比如MyResolveTask或者CustomTaskUsingToolchains
任務類型就是是Gradle Task類的子類。
對于Gradle任務,有三種狀態需要考慮:
1.注冊任務 - 在構建邏輯中使用一個任務(由您實現或由Gradle提供)。
2.配置任務 - 定義任務的輸入和輸出。
3.實現任務 - 創建一個自定義任務類(即自定義類型)
注冊通常使用register()方法完成。
配置任務通常使用named()方法完成。
實現任務通常是通過擴展Gradle的DefaultTask類來完成的:
①: 注冊Copy類型的myCopy任務。
②: 根據Copy API為注冊的myCopy任務配置它所需的輸入和輸出。
③: 實現一個名為MyCopyTask的自定義任務類型,它擴展了DefaultTask并定義了copyFiles任務操作。
1.注冊任務
我們可以通過在構建腳本和插件中注冊任務來定義Gradle要執行的操作。
使用字符串作為任務名來定義任務:
//build.gradle.kts
tasks.register("myCopy") {
?? doFirst {
? ? ? ? // Task action = Execution code
? ? ? ? // Run before exiting actions
? ? }
? ? doLast {
? ? ? ? // Task action = Execution code
? ? ? ? // Run after existing actions
? ? }
}
register()方法會將myCopy任務添加到TaskCollection中。
2.配置任務
Gradle任務必須經過配置來完成他們的操作。比如一個任務需要ZIP一個文件,我們就必須要配置文件名和文件位置。這可以參考Gradle Zip任務提供的API,學習如何正確進行配置。
在上圖示例中,我們注冊了一個myCopy任務
tasks.register<Copy>("myCopy")
我們可以在注冊的時候就立即用代碼塊配置任務:
tasks.register<Copy>("myCopy") {
?? from("resources")
?? into("target")
?? include("**/*.txt", "**/*.xml", "**/*.properties")
}
因為這個任務是Copy類型, 是Gradle支持的任務類型,所以可以使用Copy API,如from、to。
之后在需要的地方,都可以通過named()方法查找對應名字的任務來配置:
//build.gradle.kts
tasks.named<Copy>("myCopy") {
? ? from("resources")
? ? into("target")
? ? include("**/*.txt", "**/*.xml", "**/*.properties")
}
?注意,在named()調用時,如果指定的任務還沒有注冊,就會構建失敗。
3.實現任務
Gradle提供了很多任務類型,包括 Delete, Javadoc, Copy, Exec, Tar和Pmd等等,如果都滿足不了我們的構建邏輯需求,我們可以實現一個自定義的任務類型。
要創建一個任務類型,需要擴展DefaultTask,然后定義為一個抽象類(不用是具體實現類):
//app/build.gradle.kts
abstract class MyCopyTask extends DefaultTask {}
Gradle 會通過解析?build.gradle?文件中的配置動態創建任務實例,這些實例是任務類的具體實現,它們包含了在配置中設置的所有參數和指定的動作。
task("taskName")與tasks.register("taskName")
在 Gradle 中,task("taskName")?和?tasks.register("taskName")?都被用來創建新的任務,但它們屬于不同的 API 并具有一些細微的差別和用法上的考慮。
傳統 DSL(領域特定語言)方式:
task("taskName")?是 Gradle 的傳統 DSL(領域特定語言)方法,用于在?build.gradle?文件中聲明一個任務。這種方式相對直觀和簡單,適用于簡單的任務定義。
任務注冊(Tasks API)方式:
tasks.register("taskName")?是 Gradle Tasks API 的一部分,用于以編程方式注冊任務。它提供了更多的靈活性和控制,尤其是在需要基于其他任務或項目配置動態創建任務時。
主要區別:傳統 DSL 方法在配置階段就執行了任務的配置代碼,而 Tasks API 則允許延遲配置,直到執行階段才執行配置代碼。這有助于避免在配置階段發生不必要的副作用。
隨著 Gradle 的不斷進化,Tasks API 被認為是更現代和推薦的方式來創建和注冊任務。
tasks.register("taskName")?實現延遲配置的原理主要基于 Gradle 的任務生命周期和任務注冊機制。
在 Gradle 構建的生命周期中,任務(Task)的創建和配置是分開的。傳統的?task("taskName")?語法在項目的配置階段(configuration phase)就立即創建并配置了任務。這意味著,即使在任務從未被執行的情況下,其配置代碼也會被執行。這有時可能會導致不必要的副作用,比如提前計算了某些值,或者執行了只在任務執行時才需要的邏輯。
相比之下,tasks.register("taskName")?使用了一種不同的方法。這個方法實際上并沒有立即創建任務,而是注冊了一個任務工廠(TaskFactory)。這個工廠會在任務首次執行時(execution phase)被調用,從而創建任務實例并執行其配置,這就是所謂的“延遲配置”(lazy configuration)。
具體來說,當你調用?tasks.register("taskName")?時,Gradle 會創建一個?TaskRegistration?對象,該對象封裝了任務的配置邏輯(即你傳遞給?register?方法的閉包)。這個?TaskRegistration?對象會被添加到 Gradle 的任務容器中,但不會立即創建任務實例。
當 Gradle 執行階段到來,并且需要執行名為 "taskName" 的任務時,Gradle 會從任務容器中檢索相應的?TaskRegistration?對象,并調用其工廠方法來創建任務實例。此時,閉包中的配置邏輯才會被執行,從而配置新創建的任務實例。