Gradle 任務詳述

2022-08-03 14:41 更新

在入門教程構(gòu)建基礎(chǔ)中,你已經(jīng)學習了如何創(chuàng)建簡單的任務。之后您還學習了如何將其他行為添加到這些任務中。并且你已經(jīng)學會了如何創(chuàng)建任務之間的依賴。這都是簡單的任務。但 Gradle 讓任務的概念更深遠。Gradle 支持增強的任務,也就是,有自己的屬性和方法的任務。這是真正的與你所使用的 Ant 目標(target)的不同之處。這種增強的任務可以由你提供,或由 Gradle 提供。

定義任務

在構(gòu)建基礎(chǔ)中我們已經(jīng)看到如何通過關(guān)鍵字這種風格來定義任務。在某些情況中,你可能需要使用這種關(guān)鍵字風格的幾種不同的變式。例如,在表達式中不能用這種關(guān)鍵字風格。

定義任務

build.gradle

task(hello) << {
    println "hello"
}
task(copy, type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}  

您還可以使用字符串作為任務名稱:

定義任務 — — 使用字符串作為任務名稱

build.gradle

task('hello') <<
{
    println "hello"
}
task('copy', type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}  

對于定義任務,有一種替代的語法你可能更愿意使用:

使用替代語法定義任務

build.gradle

tasks.create(name: 'hello') << {
    println "hello"
}
tasks.create(name: 'copy', type: Copy) {
    from(file('srcDir'))
    into(buildDir)
}  

在這里我們將任務添加到 tasks 集合。關(guān)于 create() 方法的更多變化可以看看 TaskContainer。

定位任務

你經(jīng)常需要在構(gòu)建文件中查找你所定義的任務,例如,為了去配置或是依賴它們。對這樣的情況,有很多種方法。首先,每個任務都可作為項目的一個屬性,并且使用任務名稱作為這個屬性名稱:

以屬性方式訪問任務

build.gradle

task hello
println hello.name
println project.hello.name  

任務也可以通過 tasks 集合來訪問。

通過 tasks 集合訪問任務

build.gradle

task hello
println tasks.hello.name
println tasks['hello'].name  

您可以從任何項目中,使用 tasks.getByPath() 方法獲取任務路徑并且通過這個路徑來訪問任務。你可以用任務名稱,相對路徑或者是絕對路徑作為參數(shù)調(diào)用 getByPath() 方法。

通過路徑訪問任務

build.gradle

project(':projectA') {
    task hello
}
task hello
println tasks.getByPath('hello').path
println tasks.getByPath(':hello').path
println tasks.getByPath('projectA:hello').path
println tasks.getByPath(':projectA:hello').path  

gradle -q hello的輸出結(jié)果

> gradle -q hello
:hello
:hello
:projectA:hello
:projectA:hello  

有關(guān)查找任務的更多選項,可以看一下 TaskContainer。

配置任務

作為一個例子,讓我們看看由 Gradle 提供的 Copy 任務。若要創(chuàng)建 Copy 任務,您可以在構(gòu)建腳本中聲明:

創(chuàng)建一個復制任務

build.gradle

task myCopy(type: Copy)  

上面的代碼創(chuàng)建了一個什么都沒做的復制任務??梢允褂盟?API 來配置這個任務(見 Copy)。下面的示例演示了幾種不同的方式來實現(xiàn)相同的配置。

配置任務的幾種方式

build.gradle

Copy myCopy = task(myCopy, type: Copy)
myCopy.from 'resources'
myCopy.into 'target'
myCopy.include('**/*.txt', '**/*.xml', '**/*.properties')  

這類似于我們通常在 Java 中配置對象的方式。您必須在每一次的配置語句重復上下文 (myCopy)。這顯得很冗余并且很不好讀。

還有另一種配置任務的方式。它也保留了上下文,且可以說是可讀性最強的。它是我們通常最喜歡的方式。

配置任務-使用閉包

build.gradle

task myCopy(type: Copy)
myCopy {
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}  

這種方式適用于任何任務。該例子的第 3 行只是 tasks.getByName() 方法的簡潔寫法。特別要注意的是,如果您向 getByName() 方法傳入一個閉包,這個閉包的應用是在配置這個任務的時候,而不是任務執(zhí)行的時候。

您也可以在定義一個任務的時候使用一個配置閉包。

使用閉包定義任務

build.gradle

task copy(type: Copy) {
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}  

對任務添加依賴

定義任務的依賴關(guān)系有幾種方法。在依賴管理基礎(chǔ)中,已經(jīng)向你介紹了使用任務名稱來定義依賴。任務的名稱可以指向同一個項目中的任務,或者其他項目中的任務。要引用另一個項目中的任務,你需要把它所屬的項目的路徑作為前綴加到它的名字中。下面是一個示例,添加了從 projectA:taskX 到 projectB:taskY 的依賴關(guān)系:

從另一個項目的任務上添加依賴

build.gradle

project('projectA') {
    task taskX(dependsOn: ':projectB:taskY') << {
        println 'taskX'
    }
}
project('projectB') {
    task taskY << {
        println 'taskY'
    }
}  

gradle -q taskX 的輸出結(jié)果

> gradle -q taskX
taskY
taskX  

您可以使用一個 Task 對象而不是任務名稱來定義依賴,如下:

使用 task 對象添加依賴

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
taskX.dependsOn taskY  

gradle -q taskX的輸出結(jié)果

> gradle -q taskX
taskY
taskX  

對于更高級的用法,您可以使用閉包來定義任務依賴。在計算依賴時,閉包會被傳入正在計算依賴的任務。這個閉包應該返回一個 Task 對象或是 Task 對象的集合,返回值會被作為這個任務的依賴項。下面的示例是從 taskX 加入了項目中所有名稱以 lib 開頭的任務的依賴:

使用閉包添加依賴

build.gradle

task taskX << {
    println 'taskX'
}
taskX.dependsOn {
    tasks.findAll { task -> task.name.startsWith('lib') }
}
task lib1 << {
    println 'lib1'
}
task lib2 << {
    println 'lib2'
}
task notALib << {
    println 'notALib'
}  

gradle -q taskX 的輸出結(jié)果

> gradle -q taskX
lib1
lib2
taskX  

有關(guān)任務依賴的詳細信息,請參閱 Task 的 API。

任務排序

任務排序還是一個孵化中的功能。請注意此功能在以后的 Gradle 版本中可能會改變。

在某些情況下,控制兩個任務的執(zhí)行的順序,而不引入這些任務之間的顯式依賴,是很有用的。任務排序和任務依賴之間的主要區(qū)別是,排序規(guī)則不會影響那些任務的執(zhí)行,而僅將執(zhí)行的順序。

任務排序在許多情況下可能很有用:

  • 強制任務順序執(zhí)行: 如,'build' 永遠不會在 'clean' 前面執(zhí)行。
  • 在構(gòu)建中盡早進行構(gòu)建驗證:如,驗證在開始發(fā)布的工作前有一個正確的證書。
  • 通過在長久驗證前運行快速驗證以得到更快的反饋:如,單元測試應在集成測試之前運行。
  • 一個任務聚合了某一特定類型的所有任務的結(jié)果:如,測試報告任務結(jié)合了所有執(zhí)行的測試任務的輸出。

有兩種排序規(guī)則是可用的:"必須在之后運行"和"應該在之后運行"。

通過使用 “ 必須在之后運行”的排序規(guī)則,您可以指定 taskB 必須總是運行在 taskA 之后,無論 taskA 和 taskB 這兩個任務在什么時候被調(diào)度執(zhí)行。這被表示為 taskB.mustRunAfter(taskA) 。“應該在之后運行”的排序規(guī)則與其類似,但沒有那么嚴格,因為它在兩種情況下會被忽略。首先是如果使用這一規(guī)則引入了一個排序循環(huán)。其次,當使用并行執(zhí)行,并且一個任務的所有依賴項除了任務應該在之后運行之外所有條件已滿足,那么這個任務將會運行,不管它的“應該在之后運行”的依賴項是否已經(jīng)運行了。當傾向于更快的反饋時,會使用“應該在之后運行”的規(guī)則,因為這種排序很有幫助但要求不嚴格。

目前使用這些規(guī)則仍有可能出現(xiàn) taskA 執(zhí)行而 taskB 沒有執(zhí)行,或者 taskB 執(zhí)行而 taskA 沒有執(zhí)行。

添加 '必須在之后運行 ' 的任務排序

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
taskY.mustRunAfter taskX  

gradle -q taskY taskX 的輸出結(jié)果

> gradle -q taskY taskX
taskX
taskY  

添加 '應該在之后運行 ' 的任務排序

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
taskY.shouldRunAfter taskX  

gradle -q taskY taskX 的輸出結(jié)果

> gradle -q taskY taskX
taskX
taskY  

在上面的例子中,它仍有可能執(zhí)行 taskY 而不會導致 taskX 也運行:

任務排序并不意味著任務執(zhí)行

gradle -q taskY 的輸出結(jié)果

> gradle -q taskY
taskY  

如果想指定兩個任務之間的“必須在之后運行”和“應該在之后運行”排序,可以使用 Task.mustRunAfter() 和 Task.shouldRunAfter() 方法。這些方法接受一個任務實例、 任務名稱或 Task.dependsOn()所接受的任何其他輸入作為參數(shù)。

請注意"B.mustRunAfter(A)"或"B.shouldRunAfter(A)"并不意味著這些任務之間的任何執(zhí)行上的依賴關(guān)系:

  • 它是可以獨立地執(zhí)行任務 A 和 B 的。排序規(guī)則僅在這兩項任務計劃執(zhí)行時起作用。
  • 當--continue 參數(shù)運行時,可能會是 A 執(zhí)行失敗后B執(zhí)行了。

如之前所述,如果“應該在之后運行”的排序規(guī)則引入了排序循環(huán),那么它將會被忽略。

當引入循環(huán)時,“應該在其之后運行”的任務排序會被忽略

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
task taskZ << {
    println 'taskZ'
}
taskX.dependsOn taskY
taskY.dependsOn taskZ
taskZ.shouldRunAfter taskX  

gradle -q taskX 的輸出結(jié)果

> gradle -q taskX
taskZ
taskY
taskX  

向任務添加描述

你可以向你的任務添加描述。例如,當執(zhí)行 gradle tasks 時顯示這個描述。

向任務添加描述

build.gradle

task copy(type: Copy) {
   description 'Copies the resource directory to the target directory.'
   from 'resources'
   into 'target'
   include('**/*.txt', '**/*.xml', '**/*.properties')
}  

替換任務

有時您想要替換一個任務。例如,您想要把通過 Java 插件添加的一個任務與不同類型的一個自定義任務進行交換。你可以這樣實現(xiàn):

重寫任務

build.gradle

task copy(type: Copy)
task copy(overwrite: true) << {
    println('I am the new one.')
}  

gradle -q copy 的輸出結(jié)果

> gradle -q copy
I am the new one.  

在這里我們用一個簡單的任務替換 Copy 類型的任務。當創(chuàng)建這個簡單的任務時,您必須將 overwrite 屬性設(shè)置為 true。否則 Gradle 將拋出異常,說這種名稱的任務已經(jīng)存在。

跳過任務

Gradle 提供多種方式來跳過任務的執(zhí)行。

使用斷言

你可以使用 onlyIf()方法將斷言附加到一項任務中。如果斷言結(jié)果為 true,才會執(zhí)行任務的操作。你可以用一個閉包來實現(xiàn)斷言。閉包會作為一個參數(shù)傳給任務,并且任務應該執(zhí)行時返回 true,或任務應該跳過時返回 false。斷言只在任務要執(zhí)行前才計算。

使用斷言跳過一個任務

build.gradle

task hello << {
    println 'hello world'
}
hello.onlyIf { !project.hasProperty('skipHello') }  

gradle hello -PskipHello 的輸出結(jié)果

> gradle hello -PskipHello
:hello SKIPPED
BUILD SUCCESSFUL
Total time: 1 secs  

使用 StopExecutionException

如果跳過任務的規(guī)則不能與斷言同時表達,您可以使用 StopExecutionException。如果一個操作(action)拋出了此異常,那么這個操作(action)接下來的行為和這個任務的其他 操作(action)都會被跳過。構(gòu)建會繼續(xù)執(zhí)行下一個任務。

使用 StopExecutionException 跳過任務

build.gradle

task compile << {
    println 'We are doing the compile.'
}
compile.doFirst {
    // Here you would put arbitrary conditions in real life. But we use this as an integration test, so we want defined behavior.
    if (true) { throw new StopExecutionException() }
}
task myTask(dependsOn: 'compile') << {
   println 'I am not affected'
}  

gradle -q myTask 的輸出結(jié)果

> gradle -q myTask
I am not affected  

如果您使用由 Gradle 提供的任務,那么此功能將非常有用。它允許您向一個任務的內(nèi)置操作中添加執(zhí)行條件。

啟用和禁用任務

每一項任務有一個默認值為 true 的 enabled 標記。將它設(shè)置為 false,可以不讓這個任務的任何操作執(zhí)行。

啟用和禁用任務

build.gradle

task disableMe << {
    println 'This should not be printed if the task is disabled.'
}
disableMe.enabled = false  

Gradle disableMe 的輸出結(jié)果

> gradle disableMe
:disableMe SKIPPED
BUILD SUCCESSFUL
Total time: 1 secs  

跳過處于最新狀態(tài)的任務

如果您使用 Gradle 自帶的任務,如 Java 插件所添加的任務的話,你可能已經(jīng)注意到 Gradle 將跳過處于最新狀態(tài)的任務。這種行在您自己定義的任務上也有效,而不僅僅是內(nèi)置任務。

聲明一個任務的輸入和輸出

讓我們來看一個例子。在這里我們的任務從一個 XML 源文件生成多個輸出文件。讓我們運行它幾次。

一個生成任務

build.gradle

task transform {
    ext.srcFile = file('mountains.xml')
    ext.destDir = new File(buildDir, 'generated')
    doLast {
        println "Transforming source file."
        destDir.mkdirs()
        def mountains = new XmlParser().parse(srcFile)
        mountains.mountain.each { mountain ->
            def name = mountain.name[0].text()
            def height = mountain.height[0].text()
            def destFile = new File(destDir, "${name}.txt")
            destFile.text = "$name -> ${height}\n"
        }
    }
}  

gradle transform 的輸出結(jié)果

> gradle transform
:transform
Transforming source file.  

gradle transform的輸出結(jié)果

> gradle transform
:transform
Transforming source file.  

請注意 Gradle 第二次執(zhí)行執(zhí)行這項任務時,即使什么都未作改變,也沒有跳過該任務。我們的示例任務被用一個操作(action)閉包來定義。Gradle 不知道這個閉包做了什么,也無法自動判斷這個任務是否為最新狀態(tài)。若要使用 Gradle 的最新狀態(tài)(up-to-date)檢查,您需要聲明這個任務的輸入和輸出。

每個任務都有一個 inputs 和 outputs 的屬性,用來聲明任務的輸入和輸出。下面,我們修改了我們的示例,聲明它將 XML 源文件作為輸入,并產(chǎn)生輸出到一個目標目錄。讓我們運行它幾次。

聲明一個任務的輸入和輸出

build.gradle

task transform {
    ext.srcFile = file('mountains.xml')
    ext.destDir = new File(buildDir, 'generated')
    inputs.file srcFile
    outputs.dir destDir
    doLast {
        println "Transforming source file."
        destDir.mkdirs()
        def mountains = new XmlParser().parse(srcFile)
        mountains.mountain.each { mountain ->
            def name = mountain.name[0].text()
            def height = mountain.height[0].text()
            def destFile = new File(destDir, "${name}.txt")
            destFile.text = "$name -> ${height}\n"
        }
    }
}  

gradle transform 的輸出結(jié)果

> gradle transform
:transform
Transforming source file.  

gradle transform 的輸出結(jié)果

> gradle transform
:transform UP-TO-DATE  

現(xiàn)在,Gradle 知道哪些文件要檢查以確定任務是否為最新狀態(tài)。

任務的 inputs 屬性是 TaskInputs 類型。任務的 outputs 屬性是 TaskOutputs 類型。

一個沒有定義輸出的任務將永遠不會被當作是最新的。對于任務的輸出并不是文件的場景,或者是更復雜的場景, TaskOutputs.upToDateWhen() 方法允許您以編程方式計算任務的輸出是否應該被判斷為最新狀態(tài)。

一個只定義了輸出的任務,如果自上一次構(gòu)建以來它的輸出沒有改變,那么它會被判定為最新狀態(tài)。

它是怎么實現(xiàn)的?

在第一次執(zhí)行任務之前,Gradle 對輸入進行一次快照。這個快照包含了輸入文件集和每個文件的內(nèi)容的哈希值。然后 Gradle 執(zhí)行該任務。如果任務成功完成,Gradle 將對輸出進行一次快照。該快照包含輸出文件集和每個文件的內(nèi)容的哈希值。Gradle 會保存這兩個快照,直到任務的下一次執(zhí)行。

之后每一次,在執(zhí)行任務之前,Gradle 會對輸入和輸出進行一次新的快照。如果新的快照和前一次的快照一樣,Gradle 會假定這些輸出是最新狀態(tài)的并跳過該任務。如果它們不一則, Gradle 則會執(zhí)行該任務。Gradle 會保存這兩個快照,直到任務的下一次執(zhí)行。

請注意,如果一個任務有一個指定的輸出目錄,在它上一次執(zhí)行之后添加到該目錄的所有文件都將被忽略,并且不會使這個任務成為過時狀態(tài)。這是不相關(guān)的任務可以在不互相干擾的情況下共用一個輸出目錄。如果你因為一些理由而不想這樣,請考慮使用 TaskOutputs.upToDateWhen()

任務規(guī)則

有時你想要有這樣一項任務,它的行為依賴于參數(shù)數(shù)值范圍的一個大數(shù)或是無限的數(shù)字。任務規(guī)則是提供此類任務的一個很好的表達方式:

任務規(guī)則

build.gradle

tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) << {
            println "Pinging: " + (taskName - 'ping')
        }
    }
}  

Gradle q pingServer1 的輸出結(jié)果

> gradle -q pingServer1
Pinging: Server1  

這個字符串參數(shù)被用作這條規(guī)則的描述。當對這個例子運行 gradle tasks 的時候,這個描述會被顯示。

規(guī)則不只是從命令行調(diào)用任務才起作用。你也可以對基于規(guī)則的任務創(chuàng)建依賴關(guān)系:

基于規(guī)則的任務依賴

build.gradle

tasks.addRule("Pattern: ping<ID>") { String taskName ->
    if (taskName.startsWith("ping")) {
        task(taskName) << {
            println "Pinging: " + (taskName - 'ping')
        }
    }
}
task groupPing {
    dependsOn pingServer1, pingServer2
}  

Gradle q groupPing 的輸出結(jié)果

> gradle -q groupPing
Pinging: Server1
Pinging: Server2  

析構(gòu)器任務

析構(gòu)器任務是一個孵化中的功能 。當最終的任務準備運行時,析構(gòu)器任務會自動地添加到任務圖中。

添加一個析構(gòu)器任務

build.gradle

task taskX << {
    println 'taskX'
}
task taskY << {
    println 'taskY'
}
taskX.finalizedBy taskY  

gradle -q taskX 的輸出結(jié)果

> gradle -q taskX
taskX
taskY  

即使最終的任務執(zhí)行失敗,析構(gòu)器任務也會被執(zhí)行。

執(zhí)行失敗的任務的任務析構(gòu)器

build.gradle

task taskX << {
    println 'taskX'
    throw new RuntimeException()
}
task taskY << {
    println 'taskY'
}
taskX.finalizedBy taskY  

gradle -q taskX 的輸出結(jié)果

> gradle -q taskX
taskX
taskY  

另一方面,如果最終的任務什么都不做的話,比如由于失敗的任務依賴項或如果它被認為是最新的狀態(tài),析構(gòu)任務不會執(zhí)行。

在不管構(gòu)建成功或是失敗,都必須清理創(chuàng)建的資源的情況下,析構(gòu)認為是很有用的。這樣的資源的一個例子是,一個 web 容器會在集成測試任務前開始,并且在之后關(guān)閉,即使有些測試失敗。

你可以使用 Task.finalizedBy()方法指定一個析構(gòu)器任務。這個方法接受一個任務實例、任務名稱或<a4><c5>Task.dependsOn()</c5></a4>所接受的任何其他輸入作為參數(shù)。

總結(jié)

如果你是從 Ant 轉(zhuǎn)過來的,像 Copy 這種增強的 Gradle 任務,看起來就像是一個 Ant 目標(target)和一個 Ant 任務(task)之間的混合物。實際上確實是這樣子。Gradle 沒有像 Ant 那樣對任務和目標進行分離。簡單的 Gradle 任務就像 Ant 的目標,而增強的 Gradle 任務還包括 Ant 任務方面的內(nèi)容。Gradle 的所有任務共享一個公共 API,您可以創(chuàng)建它們之間的依賴性。這樣的一個任務可能會比一個 Ant 任務更好配置。它充分利用了類型系統(tǒng),更具有表現(xiàn)力而且易于維護。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號