每個(gè)新創(chuàng)建的任務(wù)都是org.gradle.api.DefaultTask類型,org.gradle.api.Task的標(biāo)準(zhǔn)實(shí)現(xiàn),DefaultTask所有的域都是私有的,意味著他們只能通過setter和getter方法來訪問,慶幸的是Groovy提供了一些語法糖來允許你通過名字來使用域。
許多公司或者開源組織有他們自己的發(fā)布版本的措施,一般用主版本號和次版本號來表示,也會(huì)用SNAPSHOT來表示項(xiàng)目正在開發(fā)中,版本號是通過String類型來表示,如果你想準(zhǔn)確獲得項(xiàng)目的主版本號,那應(yīng)該怎么辦?使用正則表達(dá)式匹配點(diǎn)號然后過濾得到主版本號和次版本號?如果我們用一個(gè)類來表示是不是更簡單?
你可以很簡單的通過類的域來設(shè)置、查詢和修改你的版本號的某個(gè)部分,甚至你可以把版本信息直接保存在一個(gè)文件里,比如一個(gè)文件或者數(shù)據(jù)庫里,避免通過修改構(gòu)建腳本來更改版本號,如下圖所示:
http://wiki.jikexueyuan.com/project/gradleIn-action/images/dag26.png" alt="" />
通過編程來控制版本對于自動(dòng)化項(xiàng)目生命周期很有必要,比如:你的代碼通過了單元測試準(zhǔn)備交付了,當(dāng)前的版本是1.3-SNAPSHOT,在打包成war文件之前你想把它變成發(fā)布版本1.3然后自動(dòng)部署到服務(wù)器中,這些步驟可以劃分為多個(gè)任務(wù):一個(gè)用來修改項(xiàng)目的版本號另一個(gè)用于打包WAR文件。
動(dòng)作就是在你的任務(wù)中放置構(gòu)建邏輯的地方,Task接口提供了兩個(gè)方法來聲明任務(wù)的動(dòng)作:
doFirst和doLast,當(dāng)任務(wù)執(zhí)行的時(shí)候,定義在閉包里的動(dòng)作邏輯就按順序執(zhí)行。
接下來我們會(huì)寫一個(gè)簡單的任務(wù)printVersion,任務(wù)的作用就是打印項(xiàng)目的版本號,在任務(wù) 的最后一個(gè)動(dòng)作定義這個(gè)邏輯。
version = '0.1-SNAPSHOT'
task printVersion {
doLast {
println "Version: $version"
}
}
前面我們講過左移操作符是方法doLast的快捷鍵,他們的作用是一樣的,當(dāng)你執(zhí)行g(shù)radle printVersion,你應(yīng)該得到下面的輸出:
gradle printVersion
:printVersion
Version: 0.1-SNAPSHOT
如果你用doFirst方法的話輸出的結(jié)果是一樣的:
task printVersion {
doFirst {
println "Version: $version"
}
}
給已經(jīng)存在的任務(wù)添加動(dòng)作
到目前為止,你只是給printVersion這個(gè)任務(wù)添加了單個(gè)動(dòng)作,要么是第一個(gè)或者最后一個(gè),對于每個(gè)任務(wù)可以有多個(gè)動(dòng)作,實(shí)際上,當(dāng)任務(wù)創(chuàng)建的時(shí)候你可以添加任意多個(gè)動(dòng)作,每一個(gè)任務(wù)都有一個(gè)動(dòng)作清單,他們在運(yùn)行的時(shí)候是執(zhí)行的,接下來我們來修改之前的例子:
task printVersion {
//任務(wù)的初始聲明可以添加first和last動(dòng)作
doFirst {
println "Before reading the project version"
}
doLast {
println "Version: $version"
}
}
//你可以在任務(wù)的動(dòng)作列表的最前面添加其他任務(wù),比如:
printVersion.doFirst { println "First action" }
由此可知,我們可以添加額外的動(dòng)作給已經(jīng)存在的任務(wù),當(dāng)你想添加動(dòng)作的那個(gè)任務(wù)不是你自己寫的時(shí)候這會(huì)非常有用,你可以添加一些自定義的邏輯,比如你可以添加doFirst動(dòng)作到compile-Java任務(wù)來檢查項(xiàng)目是否包含至少一個(gè)source文件。
訪問任務(wù)屬性
接下來我們來改善一下輸出版本號的方法,Gradle提供一個(gè)基于SLF4J庫的日志實(shí)現(xiàn),除了實(shí)現(xiàn)了基本的日志級別(DEBUG, ERROR, INFO, TRACE, WARN))外,還添加了額外的級別,日志實(shí)例可以通過任務(wù)的方法來直接訪問,接下來,你將用QUIET級別打印項(xiàng)目的版本號:
task printVersion << {
logger.quiet "Version: $version"
}
訪問任務(wù)的屬性是不是很容易?接下來我將給你展示兩個(gè)其他的屬性,group和description,兩個(gè)都是documentation任務(wù)的一部分,description屬性簡短的表示任務(wù)的目的,group表示任務(wù)的邏輯分組。
task printVersion(group: 'versioning', description: 'Prints project version.') << {
logger.quiet "Version: $version"
}
你也可以通過setter方法來設(shè)置屬性:
task printVersion {
group = 'versioning'
description = 'Prints project version.'
doLast {
logger.quiet "Version: $version"
}
}
當(dāng)你運(yùn)行g(shù)radle tasks,你會(huì)看到任務(wù)顯示在正確的分組里和它的描述信息:
gradle tasks
:tasks
...
Versioning tasks
----------------
printVersion - Prints project version.
...
dependsOn方法用來聲明一個(gè)任務(wù)依賴于一個(gè)或者多個(gè)任務(wù),接下來通過一個(gè)例子來講解運(yùn)用不同的方法來應(yīng)用依賴:
task first << { println "first" }
task second << { println "second" }
//聲明多個(gè)依賴
task printVersion(dependsOn: [second, first]) << {
logger.quiet "Version: $version"
}
task third << { println "third" }
//通過任務(wù)名稱來聲明依賴
third.dependsOn('printVersion')
你可以通過命令行調(diào)用third任務(wù)來執(zhí)行這個(gè)任務(wù)依賴鏈:
$ gradle -q third
first
second
Version: 0.1-SNAPSHOT
third
仔細(xì)看這個(gè)執(zhí)行順序,你有沒用發(fā)現(xiàn)printVersion聲明了對second和first任務(wù)的依賴,但是first在second任務(wù)前執(zhí)行了,Gradle里面任務(wù)的執(zhí)行順序并不是確定的。
任務(wù)依賴執(zhí)行順序
Gradle并不保證依賴的任務(wù)能夠按順序執(zhí)行,dependsOn方法只是定義這些任務(wù)應(yīng)該在這個(gè)任務(wù)之前執(zhí)行,但是這些依賴的任務(wù)具體怎么執(zhí)行它并不關(guān)心,如果你習(xí)慣用命令式的構(gòu)建工具來定義依賴(比如ant)這可能會(huì)難以理解。在Gradle里面,執(zhí)行順序是由任務(wù)的輸入輸出特性決定的,這樣做有很多優(yōu)點(diǎn),比如你想修改構(gòu)建邏輯的時(shí)候你不需要去了解整個(gè)任務(wù)依賴鏈,另一方面,因?yàn)槿蝿?wù)不是順序執(zhí)行的,就可以并發(fā)的執(zhí)行來提高性能。
在實(shí)際情況中,你可能需要在一個(gè)任務(wù)執(zhí)行之后進(jìn)行一些清理工作,一個(gè)典型的例子就是Web容器在部署應(yīng)用之后要進(jìn)行集成測試,Gradle提供了一個(gè)finalizer任務(wù)來實(shí)現(xiàn)這個(gè)功能,你可以用finalizedBy方法來結(jié)束一個(gè)指定的任務(wù):
task first << { println "first" }
task second << { println "second" }
//聲明first結(jié)束后執(zhí)行second任務(wù)
first.finalizedBy second
你會(huì)發(fā)現(xiàn)任務(wù)first結(jié)束后自動(dòng)觸發(fā)任務(wù)second:
$ gradle -q first
first
second
接下來我們來學(xué)習(xí)怎么在build腳本中定義一些隨機(jī)的代碼,在實(shí)際情況下,如果你熟悉Groovy的語法你可以編寫一些類或者方法,接下來你將會(huì)創(chuàng)建一個(gè)表示版本的類,在Java中一個(gè)class遵循bean的約定(POJO),就是添加setter和getter方法來訪問類的域,到后面發(fā)現(xiàn)手工寫這些方法很煩人,Groovy有個(gè)對應(yīng)的概念叫POGO(plain-old Groovy object),他們的setter和getter方法在生成字節(jié)碼的時(shí)候自動(dòng)添加,因此運(yùn)行的時(shí)候可以直接訪問,看下面這個(gè)例子:
version = new ProjectVersion(0, 1)
class ProjectVersion {
Integer major
Integer minor
Boolean release
ProjectVersion(Integer major, Integer minor) {
this.major = major
this.minor = minor
this.release = Boolean.FALSE
}
ProjectVersion(Integer major, Integer minor, Boolean release) {
this(major, minor)
this.release = release
}
@Override
String toString() {
//只有release為false的時(shí)候才添加后綴SNAPSHOT
"$major.$minor${release ? '' : '-SNAPSHOT'}"
}
}
當(dāng)運(yùn)行這個(gè)修改的腳本之后,你可以看到printVersion的輸出和之前一樣,但是你還是得手工修改build腳本來更改版本號,接下來你將學(xué)習(xí)如何把版本號存儲(chǔ)在一個(gè)文件里然后配置你的腳本去讀取這個(gè)配置。
在你寫代碼之前,你要新建一個(gè)屬性文件version.properties,內(nèi)容如下:
major = 0
minor = 1
release = false
添加任務(wù)配置塊
接下來我們將聲明一個(gè)任務(wù)loadVersion來從屬性文件中讀取版本號并賦給ProjectVersion實(shí)例,第一眼看起來和其他定義的任務(wù)一樣,仔細(xì)一看你會(huì)注意到你沒有定義動(dòng)作或者使用左移操作符,在Gradle里稱之為任務(wù)配置塊(task configuration)。
ext.versionFile = file('version.properties')
//配置任務(wù)沒有左移操作符
task loadVersion {
project.version = readVersion()
}
ProjectVersion readVersion() {
logger.quiet 'Reading the version file.'
//如果文件不存在拋出異常
if(!versionFile.exists()) {
throw new GradleException("Required version file does not exist:$versionFile.canonicalPath")
}
Properties versionProps = new Properties()
//groovy的file實(shí)現(xiàn)了添加方法通過新創(chuàng)建的流來讀取
versionFile.withInputStream { stream ->
versionProps.load(stream)
}
//在Groovy中如果這是最后一個(gè)語句你可以省略return關(guān)鍵字
new ProjectVersion(versionProps.major.toInteger(),
versionProps.minor.toInteger(), versionProps.release.toBoolean())
}
接下來運(yùn)行printVersion,你會(huì)看到loadVersion任務(wù)先執(zhí)行了:
$ gradle printVersion
Reading the version file.
:printVersion
Version: 0.1-SNAPSHOT
你也許會(huì)很奇怪這個(gè)任務(wù)是怎么調(diào)用的,你沒有聲明依賴,也沒有在命令行中調(diào)用它。任務(wù)配置塊總是在任務(wù)動(dòng)作之前執(zhí)行的,理解這個(gè)行為的關(guān)鍵就是Gradle的構(gòu)建生命周期,我們來看下Gradle的構(gòu)建階段:
http://wiki.jikexueyuan.com/project/gradleIn-action/images/4-1.png" alt="" />
Gradle的構(gòu)建生命周期
無論你什么時(shí)候執(zhí)行一個(gè)gradle build,都會(huì)經(jīng)過三個(gè)不同的階段:初始化、配置和執(zhí)行。
在初始化階段,Gradle給你的項(xiàng)目創(chuàng)建一個(gè)Project實(shí)例,你的構(gòu)建腳本只定義了單個(gè)項(xiàng)目,在多項(xiàng)目構(gòu)建的上下文環(huán)境中,構(gòu)建的階段更為重要。根據(jù)你正在執(zhí)行的項(xiàng)目,Gradle找出這個(gè)項(xiàng)目的依賴。
下一個(gè)階段就是配置階段,Gradle構(gòu)建一些在構(gòu)建過程中需要的一些模型數(shù)據(jù),當(dāng)你的項(xiàng)目或者指定的任務(wù)需要一些配置的時(shí)候這個(gè)階段很有幫助。
記住不管你執(zhí)行哪個(gè)build哪怕是gradle tasks配置代碼都會(huì)執(zhí)行
在執(zhí)行階段任務(wù)按順序執(zhí)行,執(zhí)行順序是通過依賴關(guān)系決定的,標(biāo)記為up-to-date的任務(wù)會(huì)跳過,比如任務(wù)B依賴于任務(wù)A,當(dāng)你運(yùn)行g(shù)radle B的時(shí)候執(zhí)行順序?qū)⑹茿->B。
Gradle通過比較兩次build之間輸入和輸出有沒有變化來確定這個(gè)任務(wù)是否是最新的,如果從上一個(gè)執(zhí)行之后這個(gè)任務(wù)的輸入和輸出沒有發(fā)生改變這個(gè)任務(wù)就標(biāo)記為up-to-date,跳過這個(gè)任務(wù)。
http://wiki.jikexueyuan.com/project/gradleIn-action/images/4-2.png" alt="" />
輸入可以是一個(gè)目錄、一個(gè)或者多個(gè)文件或者隨機(jī)的屬性,任務(wù)的輸出可以是路徑或者文件,輸入和輸出在DefaultTask類中用域來表示。假設(shè)你想創(chuàng)建一個(gè)任務(wù)把項(xiàng)目的版本由SNAPSHOT改為release,下面的代碼定義一個(gè)新任務(wù)給release變量賦值為true,然后把改變寫入到文件中。
task makeReleaseVersion(group: 'versioning', description: 'Makes project a release version.') << {
version.release = true
//ant的propertyfile任務(wù)提供很方便的方法來修改屬性文件
ant.propertyfile(file: versionFile) {
entry(key: 'release', type:'string',operation: '=', value: 'true')
}
}
運(yùn)行這個(gè)任務(wù)會(huì)修改版本屬性并寫入到文件中。
$ gradle makeReleaseVersion
:makeReleaseVersion
$ gradle printVersion
:printVersion
Version: 0.1
makeReleaseVersion的邏輯比較簡單,你可能不用考慮代碼維護(hù)的問題,隨著構(gòu)建邏輯越來越復(fù)雜,你添加了越來越多的簡單的任務(wù),這時(shí)候你就有需要用類和方法來結(jié)構(gòu)化你的代碼,你可以把你編寫源代碼的那一套代碼實(shí)踐搬過來。
編寫自定義任務(wù)類
之前提到過,Gradle會(huì)給每一個(gè)任務(wù)創(chuàng)建一個(gè)DefaultTask類型的實(shí)例,當(dāng)你要?jiǎng)?chuàng)建一個(gè)自定義的任務(wù)時(shí),你需要?jiǎng)?chuàng)建一個(gè)繼承自DefaultTask的類,看看下面這個(gè)例子:
class ReleaseVersionTask extends DefaultTask {
//通過注解聲明任務(wù)的輸入和輸出
@Input Boolean release
@OutputFile File destFile
ReleaseVersionTask() {
//在構(gòu)造器里設(shè)置任務(wù)的分組和描述
group = 'versioning'
description = 'Makes project a release version.'
}
//通過注解聲明要執(zhí)行的任務(wù)
@TaskAction
void start() {
project.version.release = true
ant.propertyfile(file: destFile) {
entry(key: 'release', type: 'string', operation: '=', value: 'true')
}
}
}
通過注解來表達(dá)輸入和輸出
任務(wù)輸入和輸出注解給你的實(shí)現(xiàn)添加了語法糖,他們和調(diào)用TasksInputs和TaskOutputs方法是一樣的效果,你一眼就知道任務(wù)期望什么樣的輸入數(shù)據(jù)以及會(huì)產(chǎn)生什么輸出。我們使用@Input注解來聲明輸入屬性release,用@OutputFile來定義輸出文件。
使用自定義的任務(wù)
上面我們實(shí)現(xiàn)了自定義的動(dòng)作方法,但是我們怎么使用這個(gè)方法,你需要在build腳本中創(chuàng)建一個(gè)ReleaseVersionTask類型的任務(wù),通過給屬性賦值來設(shè)定輸入和輸出:
//定義一個(gè)ReleaseVersionTask類型的任務(wù)
task makeReleaseVersion(type: ReleaseVersionTask) {
//設(shè)定任務(wù)屬性
release = version.release
destFile = versionFile
}
復(fù)用自定義的任務(wù)
假設(shè)你在另一個(gè)項(xiàng)目中想使用前面這個(gè)自定義的任務(wù),在另一個(gè)項(xiàng)目中需求又不太一樣,用來表示版本的POGO有不同的域,比如下面這個(gè):
class ProjectVersion {
Integer min
Integer maj
Boolean prodReady
@Override
String toString() {
"$maj.$min${prodReady? '' : '-SNAPSHOT'}"
}
}
此外,你還想把版本文件名改為project-version.properties,需要怎么做才能復(fù)用上面那個(gè)自定義的任務(wù)呢?
task makeReleaseVersion(type: ReleaseVersionTask) {
release = version.prodReady
//不同的版本文件
destFile = file('project-version.properties')
}
Gradle自帶的任務(wù)類型繼承自DefaultTask,Gradle提供了很多自帶的任務(wù)類型,這里我只介紹兩個(gè),Zip和copy用在發(fā)布項(xiàng)目中。
//eg.使用任務(wù)類型來備份發(fā)布版本
task createDistribution(type: Zip, dependsOn: makeReleaseVersion) {
//引用war任務(wù)的輸出
from war.outputs.files
//把所有文件放進(jìn)ZIP文件的src目錄
from(sourceSets*.allSource) {
into 'src'
}
//添加版本文件
from(rootDir) {
include versionFile.name
}
}
task backupReleaseDistribution(type: Copy) {
//引用createDistribution的輸出
from createDistribution.outputs.files
into "$buildDir/backup"
}
task release(dependsOn: backupReleaseDistribution) << {
logger.quiet 'Releasing the project...'
}
任務(wù)依賴推導(dǎo)
你可能注意到上面通過dependsOn方法來顯示聲明兩個(gè)任務(wù)之間的依賴,可是,一些任務(wù)并不是直接依賴于其他任務(wù)(比如上面createDistribution依賴于war)。Gradle怎么知道在任務(wù)之前執(zhí)行哪個(gè)任務(wù)?通過使用一個(gè)任務(wù)的輸出作為另一個(gè)任務(wù)的輸入,依賴就推導(dǎo)出來了,結(jié)果依賴的任務(wù)自動(dòng)執(zhí)行了,我們來看一下完整的執(zhí)行圖:
$ gradle release
:makeReleaseVersion
:compileJava
:processResources UP-TO-DATE
:classes
:war
:createDistribution
:backupReleaseDistribution
:release
Releasing the project...
運(yùn)行build之后你可以在build/distribution目錄找到生成的ZIP文件,這是打包任務(wù)的默認(rèn)輸出目錄,下面這個(gè)圖是生成的目錄樹:
http://wiki.jikexueyuan.com/project/gradleIn-action/images/4-3.png" alt="" />
在前面我們創(chuàng)建了兩個(gè)類,ProjectVersion和ReleaseVersionTask,這些類可以移動(dòng)到你項(xiàng)目的buildSrc目錄,buildSrc目錄是一個(gè)放置源代碼的可選目錄,你可以很容易的管理你的代碼。Gradle采用了標(biāo)準(zhǔn)的項(xiàng)目布局,java代碼在src/main/java目錄,Groovy代碼應(yīng)該在src/main/groovy目錄,在這些目錄的任何代碼都會(huì)自動(dòng)編譯然后放置到項(xiàng)目的classpath目錄。這里你是在處理class,你可以把他們放到指定的包里面,假如com.manning.gia,下面顯示了Groovy類在項(xiàng)目中的目錄結(jié)構(gòu):
http://wiki.jikexueyuan.com/project/gradleIn-action/images/4-4.png" alt="" />
不過要記住把這些類放在源代碼目錄需要額外的工作,這和在腳本文件中定義有點(diǎn)不一樣,你需要導(dǎo)入Gradle的API,看看下面這個(gè)例子:
package com.manning.gia
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
class ReleaseVersionTask extends DefaultTask {
(...)
}
反過來,你的構(gòu)建腳本需要從buildSrc中導(dǎo)入編譯的classes(比如 com.manning.gia.ReleaseVersionTask),下面這個(gè)是編譯任務(wù)輸出:
$ gradle makeReleaseVersion
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes
:buildSrc:jar
:buildSrc:assemble
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test
:buildSrc:check
:buildSrc:build
:makeReleaseVersion UP-TO-DATE
到此為止你學(xué)習(xí)了簡單任務(wù)的創(chuàng)建,自定義的task類,指定Gradle API提供的task類型,查看了任務(wù)動(dòng)作和任務(wù)配置的區(qū)別,以及他們的使用情形,任務(wù)配置和任務(wù)動(dòng)作是在不同階段執(zhí)行的。