原版電子書下載下傳位址:Build and Test with Gradle.pdf
Chapter 2. Gradle Tasks
在Gradle建構檔案裡面,建構活動最基本的單元是任務(task)。任務被命名為Gradle完成一個建構時所執行的建構指令的集合。你已經在第一章中看到過了有關任務的例子,它們跟其他建構系統比看起來有點相似的抽象,但是Gradle提供了一個比你可能使用的建構系統更加豐富的模型。沒有把建構活動赤裸裸的聲明和依賴綁在一起,Gradle 任務是可用的頂級對象如果你想在建構中進行程式設計。
讓我們看下不同的定義任務的方式、任務定義的兩個關鍵方面以及我們可以用來執行我們自定義任務的task API。
2.1 Declaring a Task 聲明一個任務
在緒論中,我們看到了在同一時間如何去建立一個任務以及配置設定它的行為。但是,有一種更加簡單的方式去建立一個任務。你所需要的隻是一個任務名稱(例2-1).
例2-1. Declaring a task by name only
task hello
你可以看到通過運作gradle tasks後的結果(例2-1).
例2-2. Gradle’s report of the newly created task
Root Project
Help tasks
dependencies - Displays the dependencies of root project ‘task-lab’.
help - Displays a help message
projects - Displays the subprojects of root project ‘task-lab’.
properties - Displays the properties of root project ‘task-lab’.
dependencies - Displays the dependencies of root project ‘task-lab’.
help - Displays a help message
projects - Displays the subprojects of root project ‘task-lab’.
properties - Displays the properties of root project ‘task-lab’.
hello
2.2 Task Action 任務動作
然而,執行gradle hello 任務将不會産生任何結果,因為我們還沒有配置設定動作給這個任務。之前,我們配置設定了一個動作給一個帶有左移運算符的任務(例2-3).
例2-3. Giving a task a trivial action to perform
task hello <<{
println 'hello world'
}
提示:在Groovy中,像 << 操作符(來自于Java的左移運算符)可以超載于根據被操作對象的類型而擁有不同的意思。在這種情況下,Gradle 重載 << 後面附加一個代碼塊來表示任務執行的動作清單。這就等價于我們稍後章節将會講到的 doLast() 方法。
然而,現在我們有積累動作代碼的靈活性通過指向我們已經建立的任務對象(例2-4).
例2-4. Appending a task’s actions one at a time
task hello
hello <<{
print 'hello,'
}
hello <<{
println 'world'
}
現在,我們可以重新獲得熟悉的build輸出(例2-5).
例2-5. The output of the build with actions appended a piece at a time
$ gradle hello
hello,world
$
這又是一個簡單的build行為,但是它揭示了一個強大的洞察力:任務不是build活動的一次性聲明,而是Gradle程式設計環境中的一級對象。如果我們可以積累建構動作在建構檔案中,那麼我們就有可能做得更多。讓我們繼續往下探索。
2.3 Task Configuration 任務配置
Gradle的新使用者通常在嘗試定義任務動作時會絆倒在配置文法上。繼續我們之前的例子,我們可以引進一個配置塊來擴充它(例2-6).
例2-6. A mix of task configuration and task action definition,檔案命名為scratch.gradle
task initializeDatabase
initializeDatabase<<{println 'connect to database'}
initializeDatabase<<{println 'update database schema'}
initializeDatabase{println 'configuration database connection'}
運作這個build檔案,我們獲得的結果可以會有點違反直覺(例2-7).
例2-7. The output of the preceding build file
$ gradle -b scratch.gradle initializeDatabase
configuring database connection
:initializeDatabase
connect to database
update database schema
$
提示:Groovy 使用 “closure” 閉包形式來引用來個花括号之間的代碼塊。一個閉包函數就像一個對象,可以作為參數傳遞給一個方法或指派給一個變量,然後稍後才被執行。你将會看到在Groovy中到處都是閉包,因為它們完全适合配置代碼塊和建構動作。
如果第3個閉包用了又一個build動作的片段,那麼我們最後會看到期望它的資訊列印在最後,而不是最開始。原來,添加到任務名而沒有左移運算符的語句是不會建立額外的任務動作代碼。反而,它成了一個配置塊。當任務動作執行時,任務的配置塊運作于Gradle的生命周期階段期間,它運作于執行(execution)階段之前。
提示:每一次Gradle執行一個build的時候,它要貫穿3個生命周期階段:initialization(初始化),configuration(配置),以及execution(執行)。在執行階段,建構任務按它們依賴要求的順序執行。在配置階段,任務對象被編譯為一個内部的對象模型,通常稱作有向無環圖DAG(for directed acyclic graph)。在初始化階段,Gradle決定了哪個項目(project)要參與建構。在多項目建構中靠後的階段很重要。
配置閉包的添加類似于動作閉包,是以,我們可以像這樣寫之前的建構檔案,我們将會看到相同的輸出(例2-8).
例2-8. Appending configuration blocks
task initializeDatabase
initializeDatabase << { println 'connect to database' }
initializeDatabase << { println 'update database schema' }
initializeDatabase { print 'configuring ' }
initializeDatabase { println 'database connection' }
配置塊是設定任務動作執行時所需要的變量和資料結構的地方。配置結構讓你有可能将你的建構任務轉變成一個豐富的填充着有關建構資訊的對象模型,而不僅僅是一個在某些序列将執行的建構動作集。如果沒有這個配置與動作之間的差别,你就不得不建構額外的複雜性到你的任務以來關系中,這将導緻一個更加脆落的build以及一個更沒有表達力的溝通build的基礎資料結構的方式。
提示:每一次當你運作一個Gradle建構檔案時所有的配置代碼都要運作,不管執行時給的任務是什麼。
2.4 Tasks are Objects 任務即對象
現在你可能已經知道Gradle在執行建構之前會建立一個内部的對象模型。事實上,這就是Gradle很明确地做的事。你所聲明的每一個任務實際上是一個包含在整個項目中的任務對象。任務對象有屬性和方法就像其他的對象一樣。我們甚至能控制每一個任務對象的類型,通路方式,相應的特定類型功能。一些示例将會講清楚這些。
預設地,每一個新任務接受DefaultTask類型。類似Java代碼中的java.lang.Object,每一個Gradle任務繼承于這個對象類型–即使它有自己的類型也要繼承DefaultTask.預設任務實際上沒做任何事情例如編譯代碼或拷貝檔案,但是他們确實包含功能需求用于與Gradle項目模型相接合。
2.4.1 Methods of DefaultTask
(1)dependsOn(task)
添加一個任務作為調用任務的一個依賴。被依賴的任務總是比依賴它的任務更早執行。有一些列的方式來調用這個方法。如果任務world依賴任務hello,我們可以使用如下代碼(例2-9).
例2-9. Different ways of calling the dependsOn method
// Declare that world depends on hello
// Preserves any previously defined dependencies as well
task loadTestData {
dependsOn createSchema
}
// An alternate way to express the same dependency
task loadTestData {
dependsOn << createSchema
}
// Do the same using single quotes (which are usually optional)
task loadTestData {
dependsOn 'createSchema'
}
// Explicitly call the method on the task object
task loadTestData
loadTestData.dependsOn createSchema
// A shortcut for declaring dependencies
task loadTestData(dependsOn: createSchema)
一個任務可以依賴一個以上的任務。如果任務loadTestData依賴createSchema以及compileTestClasses,我們可以使用如下示例(例2-10).
例2-10. Different ways of calling the dependsOn method for multiple dependencies
// Declare dependencies one at a time
task loadTestData {
dependsOn << compileTestClasses
dependsOn << createSchema
}
// Pass dependencies as a variable-length list
task world {
dependsOn compileTestClasses, createSchema
}
// Explicitly call the method on the task object
task world
world.dependsOn compileTestClasses, createSchema
// A shortcut for dependencies only
// Note the Groovy list syntax
task world(dependsOn: [ compileTestClasses, createSchema ])
(2) doFirst(closure)
添加一個可執行的代碼塊到任務動作的開頭部分。在執行階段,每一個關聯任務的動作塊都被執行。doFirst方法允許你添加一點點行為給正在執行動作的開頭部分,即使那個動作被一個build檔案或一個你沒有控制的插件所定義。調用doFirst方法多次可以繼續添加動作的新代碼塊到任務的執行序列的開頭部分。
你可以在任務對象上直接調用doFirst方法,通過傳遞一個閉包給這個方法。這個閉包包含了要在任務的執行動作運作之前的代碼。
提示:正如我們前面已經提到的,一個閉包是一個有一對花括号包含的Groovy代碼塊。你可以傳遞一個閉包就像其他對象一樣。傳遞閉包給方法是一種普遍的Groovy習語。
例2-11. Calling the doFirst method on the task object
task setupDatabaseTests << {
// This is the task's existing action
println 'load test data'
}
setupDatabaseTests.doFirst {
println 'create schema'
}
例2-12. The results of the preceding build file
$ gradle setupDatabaseTests
:setupDatabaseTests
create schema
load test data
$
你也可以從任務的配置塊裡調用doFirst。回憶一下,配置塊是一段運作于任何任務動作之前的可執行代碼,在build的配置階段。在我們之前讨論的任務配置中,你也許已經想知道你是如何實際地使用配置塊。下面的這些示例展示了你可以從配置裡如何調用任務方法,這為修改任務行為提供了一個潛在的非常有表達力的格式(例2-13)。
例2-13. Calling the doFirst method inside the task’s configuration block
task setupDatabaseTests << {
println 'load test data'
}
setupDatabaseTests {
doFirst {
println 'create schema'
}
}
重複調用doFirst是附加行為。之前的每一個調用的動作代碼是保留的,新的閉包被按順序地附加到待執行的清單的開始位置。如果我們要創立一個資料庫用于內建測試(想一次隻做一小塊),那麼我們可以使用如下示例代碼例2-14.
例2-14. Repeated calls to doFirst are cumulative
task setupDatabaseTests << {
println 'load test data'
}
setupDatabaseTests.doFirst {
println 'create database schema'
}
setupDatabaseTests.doFirst {
println 'drop database schema'
}
例2-15 The output of the preceding example
$ gradle world
:setupDatabaseTests
drop database schema
create database schema
load test data
$
當然,這有點不自然在一個初始化序列中插入3個單獨的閉包并調用doFirst(),如上示例。然而,任務初始的定義不會立刻有效來改變你決定要做的,例如一些任務被定義在另一個build檔案讓你不可能或者不切實際地去修改的特殊情況。這種對例外的不可通路的build邏輯的程式設計式地修改會非常強大。
到目前為止,我們的示例使用的都是非常簡單的文法,這讓Gradle機制顯得更加明顯,盡管以大量的重複為代價。在現實世界的build中,我們将更可能地按如下示例組織任務例2-16.
例2-16. Repeated calls to doFirst, refactored
// Initial task definition (maybe not easily editable)
task setupDatabaseTests << {
println 'load test data'
}
// Our changes to the task (in a place we can edit them)
setupDatabaseTests {
doFirst {
println 'create database schema'
}
doFirst {
println 'drop database schema'
}
}
注意到我們把多個doFirst調用聚集在一個簡單的配置塊,這個發生在初始動作附加到world任務之後。
(3)doLast(closure)
doLast方法和doFirst非常類似,除了附加行為是附加到動作的結尾部分,而不是開始部分。如果你有想要運作在完成的将要執行的已存在任務之後的代碼塊,那麼你可以按如下示例去做 例2-17:
例2-17. An example of the doLast method
task setupDatabaseTests << {
println 'create database schema'
}
setupDatabaseTests.doLast {
println 'load test data'
}
就像doFirst一樣,重複調用doLast也是附加性的。每一個随後的調用按順序附加它的閉包到待執行清單(例2-18).
例2-18. Repeated calls to doLast are additive
task setupDatabaseTests << {
println 'create database schema'
}
setupDatabaseTests.doLast {
println 'load test data'
}
setupDatabaseTests.doLast {
println 'update version table'
}
(4)onlyIf(closure)
onlyIf方法允許你表達一個謂語predicate,這個謂語決定一個任務是否應該被執行。這個謂語的值即為閉包傳回的值。使用這個方法,你可以使一個另外可能作為build依賴鍊的一個正常部分運作的任務的執行失效。
提示:在Groovy中,閉包的最後一條語句作為閉包的傳回值,即使沒有給定return語句。一個包含了一個單一的表達式Groovy方法是一個傳回表達式的值的函數。
例2-19. A build file making use of the onlyIf method.
task createSchema << {
println 'create database schema'
}
task loadTestData(dependsOn: createSchema) << {
println 'load test data'
}
loadTestData.onlyIf {
System.properties['load.data'] == 'true'
}
例2-20. Two invocations of the preceding build file. Note differing results.
$ build loadTestData
create database schema
:loadTestData SKIPPED
$ gradle -Dload.data=true loadTestData
:createSchema
create database schema
:loadTestData
load test data
$
通過使用onliIf方法,你可以使用你能用Groovy代碼表達的任何的邏輯來切換單獨的任務執行與否,而不隻是我們在這使用的簡單的系統屬性測試。你可以讀檔案,調用web服務,檢查安全認證資訊或其他有關的資訊。
2.4.2 Properties of DefaultTask
(1)didWork
一個boolean屬性表示任務是否成功完成。并不是所有的任務一旦完成就會設定didWork屬性,但是一些内置的任務像Compile,Copy和Delete任務将會設定該屬性來反應它們的動作成功與否。一個處理過的任務的評估是特定的任務。例如,目前的對Java Compiler實作傳回為true的didWork屬性如果至少有一個檔案編譯成功。你可以在你自己的任務動作中設定didWork屬性來反映你寫的build代碼的執行結果。
例2-21 . Send an email upon successful compilation
apply plugin: 'java'
task emailMe(dependsOn: compileJava) << {
if(tasks.compileJava.didWork) {
println 'SEND EMAIL ANNOUNCING SUCCESS'
}
}
例2-22. The results of the didWork build
$ gradle -b didWork.gradle emailMe
SEND EMAIL ANNOUNCING SUCCESS
$
(2)enabled
一個boolean屬性表示任務是否将會執行。你可以設定任意任務的enabled屬性為false不讓它運作。它的依賴任務如果是enabled将依然執行。
例2-23. Disabling a task
task templates << {
println 'process email templates'
}
task sendEmails(dependsOn: templates) << {
println 'send emails'
}
sendEmails.enabled = false
例2-24. The build with a task disabled. Note that the dependency still runs.
$ gradle -b enabled.gradle sendEmails
:templates
process email templates
:sendEmails SKIPPED
$
提示: -b指令選擇指定特定的Gradle檔案而非預設的建構檔案build.gradle.
(3)path
一個string屬性,包含了一個任務的完全限定路徑。預設地,一個任務的路徑是一個簡單的以冒号引導的任務名。下面的build檔案示範了這點。
例2-25. A single-level build file that echoes its only task’s path
task echoMyPath << {
println "THIS TASK'S PATH IS ${path}"
}
例2-26. The results of the previous build file
$ gradle -b path.gradle echoMyPath
THIS TASK'S PATH IS :echoMyPath
$
這個引導冒号表明了任務位于build檔案的頂層位置。然而,對于一個給定的build,并不是所有的任務必須位于build檔案的頂層位置,因為Gradle支援依賴的子項目,或内嵌的build。如果一個任務位于一個稱作子項目subProject的内嵌建構檔案中,那麼這個路徑将會是:subProject:echoMyPath.更多嵌套build的詳情請參考第6章。
(4)logger
一個對内部Gradle logger對象的引用。Gradle logger對象實作了org.slf4j.Logger接口,但是添加了一些額外的日志層級。由logger支援的日志層級如下所述。
-
DEBUG.For high-volume logging messages which are of interest to the build developer, but should be suppressed during normal build execution. When this log level
is selected, Gradle automatically provides a richer log formatter, including the
timestamp, log level, and logger name of each message. All other log levels emit
only the undecorated log message.
-
INFO.For lower-volume informative build messages which may be of optional
interest during build execution.
-
LIFECYCLE.Low-volume messages, usually from Gradle itself, about changes in the
build lifecycle and the execution of the build tool. When Gradle is executed without
the -q command line option, this is the default logging level. Calls to the println
method emit log statements at this level.
-
WARN.Low-volume but important messages, alerting the executor of the build of
potential problems. When the log level is set to WARN, QUIET-level messages are not
emitted.
-
QUIET. Messages which should appear even if the quiet switch was specified on the
Gradle command line. (Executing Gradle with the -q command line option causes
this to be the default log level.) System.out.println is directed to the logger at this
log level. When the log level is set to QUIET, WARN-level messages are not emitted.
-
ERROR. Rare but critically important log messages which should be emitted in all
cases. Intended to communicate build failures. If the log level is set to ERROR, calls
to System.out.println will not show up in the console.
例2-27.A task illustrating the effects of each logging level. This slightly trickier Groovy code
sets the log level to each of the valid options, attempting to emit a log message at each log level each
time.
task logLevel << {
def levels = ['DEBUG',
'INFO',
'LIFECYCLE',
'QUIET',
'WARN',
'ERROR']
levels.each { level ->
logging.level = level
def logMessage = "SETTING LogLevel=${level}"
logger.error logMessage
logger.error '-' * logMessage.size()
logger.debug 'DEBUG ENABLED'
logger.info 'INFO ENABLED'
logger.lifecycle 'LIFECYCLE ENABLED'
logger.warn 'WARN ENABLED'
logger.quiet 'QUIET ENABLED'
logger.error 'ERROR ENABLED'
println 'THIS IS println OUTPUT'
logger.error ' '
}
}
例2-28. The output generated by the preceding build file.
$ gradle -b logging.gradle logLevel
:: [ERROR] [org.gradle.api.Task] SETTING LogLevel=DEBUG
:: [ERROR] [org.gradle.api.Task] ----------------------
:: [DEBUG] [org.gradle.api.Task] DEBUG ENABLED
:: [INFO] [org.gradle.api.Task] INFO ENABLED
:: [LIFECYCLE] [org.gradle.api.Task] LIFECYCLE ENABLED
:: [WARN] [org.gradle.api.Task] WARN ENABLED
:: [QUIET] [org.gradle.api.Task] QUIET ENABLED
:: [ERROR] [org.gradle.api.Task] ERROR ENABLED
:: [ERROR] [org.gradle.api.Task]
SETTING LogLevel=INFO
---------------------
INFO ENABLED
LIFECYCLE ENABLED
WARN ENABLED
QUIET ENABLED
ERROR ENABLED
SETTING LogLevel=LIFECYCLE
--------------------------
LIFECYCLE ENABLED
WARN ENABLED
QUIET ENABLED
ERROR ENABLED
SETTING LogLevel=QUIET
----------------------
QUIET ENABLED
ERROR ENABLED
SETTING LogLevel=WARN
---------------------
WARN ENABLED
ERROR ENABLED
SETTING LogLevel=ERROR
----------------------
ERROR ENABLED
$
(5)logging
logging屬性給了我們通路日志層級的權限。正如在如上logger屬性讨論中,logging.level屬性可讀可寫來改變build使用的日志層級。
(6)description
description屬性就像它的意思一樣:一小片人類可讀的中繼資料用于說明任務的目的。有很多方式來設定description,如例2-29和例2-30所示。
例2-29. Setting the description and task behavior all in one
task helloWorld(description: 'Says hello to the world') << {
println 'hello, world'
}
例2-30. The two ways of declaring task behavior and description separately
task helloWorld << {
println 'hello, world'
}
helloWorld {
description = 'Says hello to the world'
}
// Another way to do it
helloWorld.description = 'Says hello to the world'
(7)temporaryDir
temporaryDir 屬性傳回一個File對象指向一個屬于build檔案的臨時目錄。這個目錄生成可用于需要一個存儲立即的任意工作結果的臨時地方,或為了處理任務裡面的臨時檔案。
2.4.3 Dynamic Properties
正如我們已經看到的,任務是由一系列固有屬性組成的,這些屬性對Gradle使用者來說是不可或缺的。然而,我們也可以配置設定任何其他的我們想要的屬性給任務。一個任務對象函數就像一個哈希map,可以包含無論其他任意我們想要的指派給任務的名值對屬性(隻要名稱不與内置屬性名稱沖突)。
離開我們熟悉的“hello,world”示例,我們假設有一個叫做createArtifact的任務,它依賴于一個叫copyFiles的任務。copyFiles任務的工作是收集來自于多個來源的檔案然後拷貝它們到一個臨時目錄,這個目錄是createArtifact任務之後将會組裝為一個部署工件的地方。根據建構參數,檔案清單可能會改變,但是這個工件必須包含一個manifest清單來排列它們,目的是為了滿足部署應用的需求。這是一個絕佳的機會來使用動态屬性(例2-31和例2-32).
例2-31. Build file showing a dynamic task property
task copyFiles {
// Find files from wherever, copy them
// (then hardcode a list of files for illustration)
fileManifest = [ 'data.csv', 'config.json' ]
}
task createArtifact(dependsOn: copyFiles) << {
println "FILES IN MANIFEST: ${copyFiles.fileManifest}"
}
例2-32. The output of the above build file
$ gradle -b dynamic.gradle createArtifact
FILES IN MANIFEST: [data.csv, config.json]
$
原版電子書下載下傳位址:Build and Test with Gradle.pdf