轉發自:
http://www.cnblogs.com/CloudTeng/p/3417762.html
原文鏈接:
- Gradle快速入門
- 創建Task的多種方法
- 讀懂Gradle語法
- 增量式構建
- 自定義Property
- 使用java Plugin
- 依賴管理
- 構建多個Project
- 自定義Task類型
- 自定義Plugin
下載本系列文章的Github示例代碼:
git clone https://github.com/davenkin/gradle-learning.git
Gradle 快速入門
和Maven一樣,Gradle只是提供了構建項目的一個框架,真正起作用的是Plugin。Gradle在默認情況下爲我們提供了許多常用的Plugin,其中包括有構建Java項目的Plugin,還有War,Ear等。與Maven不同的是,Gradle不提供內建的項目生命週期管理,只是java Plugin向Project中添加了許多Task,這些Task依次執行,爲我們營造了一種如同Maven般項目構建週期。更多有關Maven的知識,讀者可以訪問Maven官網,或者可以參考筆者寫的Maven學習系列文章。
現在我們都在談領域驅動設計,Gradle本身的領域對象主要有Project和Task。Project爲Task提供了執行上下文,所有的Plugin要麼向Project中添加用於配置的Property,要麼向Project中添加不同的Task。一個Task表示一個邏輯上較爲獨立的執行過程,比如編譯Java源代碼,拷貝文件,打包Jar文件,甚至可以是執行一個系統命令或者調用Ant。另外,一個Task可以讀取和設置Project的Property以完成特定的操作。
讓我們來看一個最簡單的Task,創建一個build.gradle文件,內容如下:
task helloWorld<< {
println "Hello World!"
}
這裏的“<<”表示向helloWorld中加入執行代碼——其實就是groovy代碼。Gradle向我們提供了一整套DSL,所以在很多時候我們寫的代碼似乎已經脫離了groovy,但是在底層依然是執行的groovy。比如上面的task關鍵字,其實就是一個groovy中的方法,而大括號之間的內容則表示傳遞給task()方法的一個閉包。除了“<<”之外,我們還很多種方式可以定義一個Task,我們將在本系列後續的文章中講到。
在與build.gradle相同的目錄下執行:
gradle helloWorld
命令行輸出如下:
:helloWorld
Hello World!
BUILD SUCCESSFUL
Total time: 2.544secs
在默認情況下,Gradle將當前目錄下的build.gradle文件作爲項目的構建文件。在上面的例子中,我們創建了一個名爲helloWorld的Task,在執行gradle命令時,我們指定執行這個helloWorld Task。這裏的helloWorld是一個DefaultTask類型的對象,這也是定義一個Task時的默認類型,當然我們也可以顯式地聲明Task的類型,甚至可以自定義一個Task類型(我們將在本系列的後續文章中講到)。
比如,我們可以定義一個用於文件拷貝的Task:
taskcopyFile(type: Copy) {
from 'xml'
into 'destination'
}
以上copyFile將xml文件夾中的所有內容拷貝到destination文件夾中。這裏的兩個文件夾都是相對於當前Project而言的,即build.gradle文件所在的目錄。
Task之間可以存在依賴關係,比如taskA依賴於taskB,那麼在執行taskA時,Gradle會先執行taskB,然後再執行taskB。聲明Task依賴關係的一種方式是在定義一個Task的時候:
tasktaskA(dependsOn: taskB) {
//do something
}
Gradle在默認情況下爲我們提供了幾個常用的Task,比如查看Project的Properties、顯示當前Project中定義的所有Task等。可以通過一下命令查看Project中所有的Task:
gradle tasks
輸出如下:
:tasks
------------------------------------------------------------
All tasks runnable fromroot project
------------------------------------------------------------
Build Setup tasks
-----------------
setupBuild - Initializes anew Gradle build. [incubating]
wrapper - Generates Gradlewrapper files. [incubating]
Help tasks
----------
dependencies - Displaysall dependencies declared in root project 'gradle-blog'.
dependencyInsight -Displays the insight into a specific dependency in root project 'gradle-blog'.
help - Displays a helpmessage
projects - Displays thesub-projects of root project 'gradle-blog'.
properties - Displays theproperties of root project 'gradle-blog'.
tasks - Displays the tasksrunnable from root project 'gradle-blog'.
Other tasks
-----------
copyFile
helloWorld
To see all tasks and moredetail, run with --all.
BUILD SUCCESSFUL
Total time: 2.845 secs
可以看到,除了我們自己定義的copyFile和helloWorld之外,Gradle還默認爲我們提供了dependencies、projects和properties等Task。dependencies用於顯示Project的依賴信息,projects用於顯示所有Project,包括根Project和子Project,而properties則用於顯示一個Project所包含的所有Property。
在默認情況下,Gradle已經爲Project添加了很多Property,我們可以調用以下命令進行查看:
gradle properties
輸出如下:
:properties
------------------------------------------------------------
Root project
------------------------------------------------------------
allprojects: [root project'gradle-blog']
ant: org.gradle.api.internal.project.DefaultAntBuilder@1342097
buildDir:/home/davenkin/Desktop/gradle-blog/build
buildFile:/home/davenkin/Desktop/gradle-blog/build.gradle
...
configurations: []
convention:org.gradle.api.internal.plugins.DefaultConvention@11492ed
copyFile: task ':copyFile'
...
ext:org.gradle.api.internal.plugins.DefaultExtraPropertiesExtension@1b5d53a
extensions:org.gradle.api.internal.plugins.DefaultConvention@11492ed
...
helloWorld: task':helloWorld'
...
plugins:[org.gradle.api.plugins.HelpTasksPlugin@7359f7]
project: root project'gradle-blog'
...
properties: {...}
repositories: []
tasks: [task ':copyFile',task ':helloWorld']
version: unspecified
BUILD SUCCESSFUL
Total time: 2.667 secs
在以上Property中,allprojects表示所有的Project,這裏只包含一個根Project,在多項目構建中,它將包含多個Project;buildDir表示構建結果的輸出目錄;我們自己定義的helloWorld和copyFile也成爲了Project中的Property。另外,Project還包括用於執行Ant命令的DefaultAntBuilder(Property名爲ant)和Project的描述屬性description。
創建Task的多種方法
Gradle的Project從本質上說只是含有多個Task的容器,一個Task與Ant的Target相似,表示一個邏輯上的執行單元。我們可以通過很多種方式定義Task,所有的Task都存放在Project的TaskContainer中。
(1)調用Project的task()方法創建Task
在使用Gradle時,創建Task最常見的方式便是:
task hello1 << {
println 'hello1'
}
這裏的“<<”表示追加的意思,即向hello中加入執行過程。我們還可以使用doLast來達到同樣的效果:
task hello2 {
doLast {
println 'hello2'}
}
另外,如果需要向Task的最前面加入執行過程,我們可以使用doFirst:
task hello3 {
doFirst {
println 'hello3'}
}
在上面的3個例子中,Gradle的DSL向我們展示了一種非常自然的風格來創建Task,而事實上這些都只是一種內部DSL,也即必須符合groovy的語法要求。上面的task關鍵字實際上是一個方法調用,該方法屬於Project。Project中存在多個重載的task()方法。和Ruby等動態語言一樣,在調用groovy方法時,我們不用將參數放在括號裏面。
以上我們自定義的3個Task都位於TaskContainer中,Project中的tasks屬性即表示該TaskContainer。爲此,我們可以新建一個Task來顯示這些信息:
task showTasks {
println tasks.class
println tasks.size()
}
將以上4個Task放在同一個build.gradle中,再執行gradle showTasks,命令行輸出如下:
...
classorg.gradle.api.internal.tasks.DefaultTaskContainer_Decorated
4
...
上面的DefaultTaskContainer_Decorated表示tasks類型,而4表示該TaskContainer中包含有4個自定義的Task——包括showTasks本身。
(2)通過TaskContainer的create()方法創建Task
在上文中我們講到,通過task()方法創建的Task都被存放在了TaskContainer中,而Project又維護了一個TaskContainer類型的屬性tasks,那麼我們完全可以直接向TaskContainer裏面添加Task。查查TaskContainer的API文檔可以發現,TaskContainer向我們提供了大量重載的create()方法用於添加Task。
tasks.create(name: 'hello4') << {
println 'hello4'
}
(3)聲明Task之間的依賴關係
Task之間是可以存在依賴關係,比如TaskA依賴TaskB,那麼在執行TaskA時,Gradle會先執行TaskB,再執行TaskA。我們可以在定義一個Task的同時聲明它的依賴關係:
task hello5(dependsOn:hello4) << {
println 'hello5'
}
當然,我們也可以在定義Task之後再聲明依賴:
task hello6 << {
println 'hello6'
}
hello6.dependsOn hello5
(4)配置Task
一個Task除了執行操作之外,還可以包含多個Property,其中有Gradle爲每個Task默認定義的Property,比如description,logger等。另外,每一個特定的Task類型還可以含有特定的Property,比如Copy的from和to等。當然,我們還可以動態地向Task中加入額外的Property。在執行一個Task之前,我們通常都需要先設定Property的值,Gradle提供了多種方法設置Task的Property值。
首先,我們可以在定義Task的時候對Property進行配置:
task hello7 << {
description = "this ishello7"
println description
}
我們還可以通過閉包的方式來配置一個已有的Task:
task hello8 << {
println description
}
hello8 {
description = "this is hello8"
}
需要注意的是,對hello8的description設置發生在創建該Task之後,在執行“gradle hello8”時,命令行依然可以打印出正確的“this is hello8”,這是因爲Gradle在執行Task時分爲兩個階段,首先是配置階段,然後纔是實際執行階段。所以在執行hello8之前,Gradle會掃描整個build.gradle文檔,將hello8的description設置爲“this is hello8”,然後執行hello8,此時hello8的description已經包含了設置後的值。
我們還可以通過調用Task的configure()方法完成Property的設置:
task hello9 << {
println description
}
hello9.configure {
description = "this ishello9"
}
實際上,通過閉包的方式配置Task在內部也是通過調用Task的configure()方法完成的,對此我們將在後續的文章中詳細地講到。
讀懂Gradle
Gradle是一種聲明式的構建工具。在執行時,Gradle並不會一開始便順序執行build.gradle文件中的內容,而是分爲兩個階段,第一個階段是配置階段,然後纔是實際的執行階段。在
配置階段,Gradle將讀取所有build.gradle文件的所有內容來配置Project和Task等,比如設置Project和Task的Property,處理Task之間的依賴關係等。
雖然很多時候我們只需要照着網上的例子寫自己的DSL語句就行了,但是此時我們所知道的也就只有這麼多了。如果我們能夠了解GradleDSL的內部工作機制,那麼我們便可以達到
舉一反三的效果。在前面的文章中我們講到,Gradle的DSL只是Groovy語言的內部DSL,也即必須遵循Groovy的語法規則。現在,讓我們先來看看以下非常簡單的Task:
task showDescription1 <<{
description = 'this is taskshowDescription'
println description
}
task showDescription2 << {
println description
}
showDescription2.description = 'this is task showDescription'
task showDescription3 << {
println description
}
showDescription3 {
description = 'this is taskshowDescription'
}
以上3個Task完成的功能均相同,即先設置Task的description屬性,在將其輸出到命令行。但是,他們對description的設置方式是不同的。
對於showDescription1,我們在定義一個Task的同時便設置description;
對於showDescription2,其本身便是Project的一個Property;
對於showDescription3,我們是在一個和它同名的方法中設置description。
事實上,對於每一個Task,Gradle都會在Project中創建一個同名的Property,所以我們可以將該Task當作Property來訪問,showDescription2便是這種情況。另外,Gradle還會創建一個同名的方法,該方法接受一個閉包,我們可以使用該方法來配置Task,showDescription3便是這種情況。
要讀懂Gradle,我們首先需要了解Groovy語言中的兩個概念,一個Groovy中的Bean概念,一個是Groovy閉包的delegate機制。
Groovy中的Bean和Java中的Bean有一個很大的不同,即Groovy爲每一個字段都會自動生成getter和setter,並且我們可以通過像訪問字段本身一樣調用getter和setter,比如:
class GroovyBeanExample {
private String name
}
def bean = new GroovyBeanExample()
bean.name = 'this is name'
println bean.name
我們看到,GroovyBeanExample只定義了一個私有的name屬性,並沒有getter和setter。但是在使用時,我們可以直接對name進行訪問,無論時讀還是寫。事實上,我們並不是
在直接訪問name屬性,當我們執行"bean.name = 'this isname'"時,我們實際調用的是"bean.setName('this isname')",而在調用"println bean.name"時,我們實際調用的是
"println bean.getName()"。這裏的原因在於,Groovy動態地爲name創建了getter和setter,採用像直接訪問的方式的目的是爲了增加代碼的可讀性,使它更加自然,而在內部,Groovy依然
是在調用setter和getter方法。這樣,我們便可以理解上面對showDescription2的description設置原理。
另外,Gradle大量地使用了Groovy閉包的delegate機制。簡單來說,delegate機制可以使我們將一個閉包中的執行代碼的作用對象設置成任意其他對象。比如:
class Child {
private String name
}
class Parent {
Child child = new Child();
void configChild(Closure c) {
c.delegate = child
c.setResolveStrategy Closure.DELEGATE_FIRST
c()
}
}
def parent = new Parent()
parent.configChild {
name = "child name"
}
println parent.child.name
在上面的例子中,當我們調用configChild()方法時,我們並沒有指出name屬性是屬於Child的,但是它的確是在設置Child的name屬性。事實上光從該方法的調用中,我們根本不知道name是屬於哪個對象的,你可能會認爲它是屬於Parent的。真實情況是,在默認情況下,name的確被認爲是屬於Parent的,但是我們在configChild()方法的定義中做了手腳,使其不再訪問Parent中的name(Parent也沒有name屬性),而是Child的name。在configChild()方法中,我們將該方法接受的閉包的delegate設置成了child,然後將該閉包的ResolveStrategy設置成了DELEGATE_FIRST。這樣,在調用configChild()時,所跟閉包中代碼被代理到了child上,即這些代碼實際上是在child上執行的。此外,閉包的ResolveStrategy在默認情況下是OWNER_FIRST,即它會先查找閉包的owner(這裏即parent),如果owner存在,則在owner上執行閉包中的代碼。這裏我們將其設置成了DELEGATE_FIRST,即該閉包會首先查找delegate(本例中即child),如果找到,該閉包便會在delegate上執行。對於上面的showDescription3,便是這種情況。當然,實際情況會稍微複雜一點,比如showDescription3()方法會在內部調用showDescription3的configure()方法,再在configure()方法中執行閉包中的代碼。
你可能會發現,在使用Gradle時,我們並沒有像上面的parent.configChild()一樣指明方法調用的對象,而是在build.gradle文件中直接調用task(),apply()和configuration()方法等,這是
因爲在沒有說明調用對象的情況下,Gradle會自動將調用對象設置成當前Project。比如調用apply()方法和調用project.apply()方法的效果是一樣的。查查Gradle的Project文檔,你會發現這些方法都是Project類的方法。
另外舉個例子,對於configurations()方法(它的作用我們將在後面的文章中講到),該方法實際上會將所跟閉包的delegate設置成ConfigurationContainer,然後在該ConfigurationContainer上執行閉包中的代碼。再比如,dependencies()方法,該方法會將所跟閉包的delegate設置成DependencyHandler。
還有,Project還定義了configure(Object object,ClosureconfigureClosure)方法,該方法是專門用來配置對象的(比如Task),它會將configureClosure的delegate設置成object,之後configureClosure中的執行代碼其實是在object上執行的。和Groovy Bean一樣,delegate機制的一個好處是可以增加所創建DSL的可讀性。
在下一篇文章中,我們將講到如何進行增量式構建。
增量式構建
如果我們將Gradle的Task看作一個黑盒子,那麼我們便可以抽象出輸入和輸出的概念,一個Task對輸入進行操作,然後產生輸出。比如,在使用java插件編譯源代碼時,輸入即爲Java源文件,輸出則爲class文件。如果多次執行一個Task時的輸入和輸出是一樣的,那麼我們便可以認爲這樣的Task是沒有必要重複執行的。此時,反覆執行相同的Task是冗餘的,並且是耗時的。
爲了解決這樣的問題,Gradle引入了增量式構建的概念。在增量式構建中,我們爲每個Task定義輸入(inputs)和輸入(outputs),如果在執行一個Task時,如果它的輸入和輸出與前一次執行時沒有發生變化,那麼Gradle便會認爲該Task是最新的(UP-TO-DATE),因此Gradle將不予執行。一個Task的inputs和outputs可以是一個或多個文件,可以是文件夾,還可以是Project的某個Property,甚至可以是某個閉包所定義的條件。
每個Task都擁有inputs和outputs屬性,他們的類型分別爲TaskInputs和TaskOutputs。在下面的例子中,我們展示了這麼一種場景:名爲combineFileContent的Task從sourceDir目錄中讀取所有的文件,然後將每個文件的內容合併到destination.txt文件中。讓我們先來看看沒有定義Task輸入和輸出的情況:
task combineFileContentNonIncremental {
def sources =fileTree('sourceDir')
def destination =file('destination.txt')
doLast {
destination.withPrintWriter { writer ->
sources.each {source ->
writer.println source.text
}
}
}
}
多次執行“gradle combineFileContentNonIncremental”時,整個Task都會反覆執行,即便在第一次執行後我們已經得到了所需的結果。如果該combineFileContentNonIncremental是一個繁重的Task,那麼多次重複執行勢必造成沒必要的時間耗費。
這時,我們可以將sources聲明爲該Task的inputs,而將destination聲明爲outputs,重新創建一個Task如下:
task combineFileContentIncremental {
def sources =fileTree('sourceDir')
def destination =file('destination.txt')
inputs.dir sources
outputs.file destination
doLast {
destination.withPrintWriter { writer ->
sources.each {source ->
writer.println source.text
}
}
}
}
相比之下,後一個Task只比前一個Task多了兩行代碼:
inputs.dir sources
outputs.file destination
當首次執行combineFileContentIncremental時,Gradle會完整地執行該Task。但是緊接着再執行一次,命令行顯示:
:combineFileContentIncremental UP-TO-DATE
BUILD SUCCESSFUL
Total time: 2.104 secs
我們發現,combineFileContentIncremental被標記爲UP-TO-DATE,表示該Task是最新的,Gradle將不予執行。在實際應用中,你將遇到很多這樣的情況,因爲Gradle的很多插件都引入了增量式構建機制。
如果我們修改了inputs(即sourceDir文件夾)中的任何一個文件或刪除掉了destination.txt,當調用“gradlecombineFileContentIncremental”時,Gradle又會重新執行,因爲此時的Task已經不再是最新的了。對於outputs,我們還可以使用upToDateWhen()方法來決定一個Task的outputs是否爲最新的,該方法接受一個閉包作爲檢查條件,感興趣的讀者可以自行了解。
自定義Property
在前面的文章中我們講到,設置和讀取Project的Property是使用Gradle的一個很重要的方面。比如,很多Plugin都會向Project中加入額外的Property,在使用這些Plugin時,我們需要對這些Property進行賦值。
Gradle在默認情況下已經爲Project定義了很多Property,其中比較常用的有:
project:Project本身
name:Project的名字
path:Project的絕對路徑
description:Project的描述信息
buildDir:Project構建結果存放目錄
version:Project的版本號
以下,我們首先設置Project的version和description屬性,再定義showProjectProperties以打印這些屬性:
version = 'this is the project version'
description = 'this is the project description'
task showProjectProperties << {
println version
println project.description
}
請注意,在打印description時,我們使用了project.description,而不是直接使用description。原因在於,Project和Task都擁有description屬性,而定義Task的閉包將delegate設置成了當前的Task,故如果直接使用description,此時打印的是showProjectProperties的description,而不是Project的,所以我們需要顯式地指明project。有關delegate的更多知識,請參考本系列的這篇文章。
Gradle還爲我們提供了多種方法來自定義Project的Property。
(1)在build.gradle文件中定義Property
在build.gradle文件中向Project添加額外的Property時,我們並不能直接定義,而是應該通過ext來定義。如果要添加一個名爲property1的Property,我們應該:
ext.property1 = "this is property1"
另外,我們也可以通過閉包的方式:
ext {
property2 = "this isproperty2"
}
在定義了Property後,使用這些Property時我們則不需要ext,而是可以直接訪問:
task showProperties <<{
println property1
println property2
}
事實上,任何實現了ExtensionAware接口的Gradle對象都可以通過這種方式來添加額外的Property,比如Task也實現了該接口。
(2)通過命令行參數定義Property
Gradle還提供了-P命令行參數來設置Property,比如:
task showCommandLieProperties << {
println property3
}
在執行“gradle showCommandLieProperties”時,終端輸出如下:
* What went wrong:
Execution failed fortask ':showCommandLieProperties'.
> Could not find property 'property3' on task':showCommandLieProperties'.
表示property3並沒有被定義,在調用gradle命令時,通過-P參數傳入該Property:
gradle -Pproperty3="this is property3"showCommandLieProperties
此時終端顯示:
:showCommandLieProperties
this is property3
BUILD SUCCESSFUL
(3)通過JVM系統參數定義Property
我們知道,在java中,我們可以通過-D參數定義JVM的系統參數,然後在代碼中可以可以通過System.getProperty()進行獲取。在Gradle中,我們也可以通過-D的方式向Project傳入
Property,只是此時我們需要遵循一些約定:每一個通過-D方式聲明的Property都需要以“org.gradle.project”爲前綴,對於上面的showCommandLieProperties,我們也可以通過以下方式
設置property3:
gradle -Dorg.gradle.project.property3="this isanother property3" showCommandLieProperties
(4)通過環境變量設置Property
我們還可以通過設置環境變量的方式設置Project的Property。這種方式和(3)一樣,需要我們遵循一些約定:在定義環境變量時,每一個Property都需要以“ORG_GRADLE_PROJECT_”爲前綴:
export ORG_GRADLE_PROJECT_property3="this is yetanother property3"
在調用showCommandLieProperties時,我們便不需要傳入命令行參數了:
gradle showCommandLieProperties
在筆者所工作的項目中,我們的持續集成服務器便是通過這種方式爲Gradle設置Property的。
使用Java Plugin
Gradle最常用的Plugin便是java Plugin了。和其他Plugin一樣,java Plugin並沒有什麼特別的地方,只是向Project中引入了多個Task和Property。當然,java Plugin也有比較與衆不同的地方,其中之一便是它在項目中引入了構建生命週期的概念,就像Maven一樣。但是,和Maven不同的是,Gradle的項目構建生命週期並不是Gradle的內建機制,而是由Plugin自己引入的。
(1)java Plugin引入的主要Task
執行“gradle build”,我們已經可以看到javaPlugin所引入的主要Task:
:compileJava
:processResources
:classes
:jar
:assemble
:compileTestJava
:processTestResources
:testClasses
:test
:check
:build
BUILD SUCCESSFUL
Total time: 4.813 secs
build也是java Plugin所引入的一個Task,它依賴於其他Task,其他Task又依賴於另外的Task,所以有了以上Task執行列表。以上Task執行列表基本上描述了java Plugin向項目中所引入的構建生命週期概念。
除了定義衆多的Task外,java Plugin還向Project中加入了一些額外的Property。比如,sourceCompatibility用於指定在編譯Java源文件時所使用的Java版本,archivesBaseName用於指定打包成Jar文件時的文件名稱。
(2)Java項目的目錄結構
在默認情況下,Gradle採用了與Maven相同的Java項目目錄結構:
關於Maven標準目錄結構,請參考Maven官網。當然,跟Maven一樣,以上只是默認的目錄結構,我們可以通過配置來修改這些目錄結構。
(3)配置已有sourceset
Gradle在採用了Maven目錄結構的同時,還融入了自己的一些概念,即source set。對於上圖中的目錄結構,Gradle實際上爲我們創建了2個source set,一個名爲main,一個名爲test。
請注意,這裏的source set的名字main與上圖目錄結構中的main文件夾並無必然的聯繫,只是在默認情況下,Gradle爲了source set概念到文件系統目錄結構的映射方便,才採用了相同的名字。對於test,也是如此。我們完全可以在build.gradle文件中重新配置這些sourceset所對應的目錄結構,同時,我們還可以創建新的source set。
從本質上講,Gradle的每個source set都包含有一個名字,並且包含有一個名爲java的Property和一個名爲resources的Property,他們分別用於表示該source set所包含的Java源文件集合和資源文件集合。在實際應用時,我們可以將他們設置成任何目錄值。比如,我們可以重新設置main的目錄結構:
sourceSets {
main {
java {
srcDir 'java-sources'
}
resources {
srcDir 'resources'
}
}
}
此時所對應的項目目錄結構如下:
我們重新設置了main的目錄結構,而對於test,我們保留了Gradle默認的目錄結構。
(4)創建新的sourceset
要創建一個新的source set也是非常簡單的,比如,我們可以創建一個名爲api的source set來存放程序中的接口類:
sourceSets {
api
}
當然,以上配置也可以與main放在一起。在默認情況下,該api所對應的Java源文件目錄被Gradle設置爲${path-to-project}/src/api/java,而資源文件目錄則被設置成了${path-to-project}/src/api/resources。我們也可以像上面的main一樣重新對api的目錄結構進行配置。
Gradle會自動地爲每一個新創建的sourceset創建相應的Task,創建規律爲:對於名爲mySourceSet的source set,Gradle將爲其創建compile<mySourceSet>Java、process<mySourceSet>Resources和<mySourceSet>Classes這3個Task。對於這裏api而言,Gradle會爲其創建名爲compileApiJava、processApiResource和apiClasses Task。我們可以在命令行中執行"gradleapiClasses"。
你可能會注意到,對於main而言,Gradle並沒有相應的compileMainJava,原因在於:由於main是Gradle默認創建的source set,並且又是及其重要的source set,Gradle便省略掉了其中的“Main”,而是直接使用了compileJava作爲main的編譯Task。對於test來說,Gradle依然採用了compileTestJava。
通常的情況是,我們自己創建的名爲api的source set會被其他source set所依賴,比如main中的類需要實現api中的某個接口等。此時我們需要做兩件事情。第一,我們需要在編譯main之前對api進行編譯,即編譯main中Java源文件的Task應該依賴於api中的Task:
classes.dependsOn apiClasses
第二,在編譯main時,我們需要將api編譯生成的class文件放在main的classpath下。此時,我們可以對main和test做以下配置:
sourceSets {
main {
compileClasspath =compileClasspath + files(api.output.classesDir)
}
test {
runtimeClasspath =runtimeClasspath + files(api.output.classesDir)
}
}
之所以需要對test的runtimeClasspath進行設置,是因爲在運行測試時我們也需要加載api中的類。
在下一篇文章中,我們將講到如何管理依賴。
依賴管理
一個Java項目總會依賴於第三方,要麼是一個第三方類庫,比如Apachecommons;要麼是你自己開發的另外一個Java項目,比如你的web項目依賴於另一個核心的業務項目。通常來說,這種依賴的表示形式都是將第三方的Jar文件放在自己項目的classpath下,要麼是編譯時的classpath,要麼是運行時的classpath。
在聲明對第三方類庫的依賴時,我們需要告訴Gradle在什麼地方去獲取這些依賴,即配置Gradle的Repository。在配置好依賴之後,Gradle會自動地下載這些依賴到本地。Gradle可以使用Maven和Ivy的Repository,同時它還可以使用本地文件系統作爲Repository。
在本文中,我們將以Maven的Repository爲例進行講解,要配置Maven的Repository是非常簡單的,我們只需要在build.gradle文件中加入以下代碼即可:
repositories {
mavenCentral()
}
Gradle將對依賴進行分組,比如編譯Java時使用的是這組依賴,運行Java時又可以使用另一組依賴。每一組依賴稱爲一個Configuration,在聲明依賴時,我們實際上是在設置不同的Configuration。值得一提的是,將依賴稱爲Configuration並不是一個好的名字,更好的應該叫作諸如“DependencyGroup”之類的。但是,習慣了就好的。
要定義一個Configuration,我們可以通過以下方式完成:
configurations {
myDependency
}
以上只是定義了一個名爲myDependency的Configuration,我們並未向其中加入依賴。我們可以通過dependencies()方法向myDependency中加入實際的依賴項:
dependencies {
myDependency 'org.apache.commons:commons-lang3:3.0'
}
以上,我們將Apache的commons加入了myDependency中。之後,如果有Task需要將Apache commons加入到classpath中,我們可以通過以下方式進行獲取:
task showMyDependency <<{
println configurations.myDependency.asPath
}
執行“gradle showMyDependency”命令,在筆者的電腦上終端將顯示:
:showMyDependency
/Users/twer/.gradle/caches/artifacts-26/filestore/org.apache.commons/commons-lang3/3.0/jar/8873bd0bb5cb9ee37f1b04578eb7e26fcdd44cb0/commons-lang3-3.0.jar
BUILD SUCCESSFUL
Total time: 4.405 secs
在實際應用時,比如我們需要調用Ant的某個target,而該target在執行時需要設置classpath,那麼我們便可以通過以上方式進行設置。
下面,我們來看一個Java項目,該項目依賴於SLF4J,而在測試時依賴於Junit。在聲明依賴時,我們可以通過以下方式進行設置:
dependencies {
compile 'org.slf4j:slf4j-log4j12:1.7.2'
testCompile 'junit:junit:4.8.2'
}
我們並沒有定義名爲compile和testCompile的Configuration,這是這麼回事呢?原因在於,java Plugin會自動定義compile和testCompile,分別用於編譯Java源文件和編譯Java測試源文件。
另外,java Plugin還定義了runtime和testRuntime這兩個Configuration,分別用於在程序運行和測試運行時加入所配置的依賴。
再舉個來自Gradle官網的例子:在Gradle中調用Ant,首先我們通過Configuration聲明一組依賴,然後在Ant定義中將該Configuration所表示的classpath傳給Ant:
configurations {
pmd
}
dependencies {
pmd group: 'pmd', name: 'pmd',version: '4.2.5'
}
task check << {
ant.taskdef(name: 'pmd',classname: 'net.sourceforge.pmd.ant.PMDTask',classpath: configurations.pmd.asPath)
ant.pmd(shortFilenames: 'true',failonruleviolation: 'true', rulesetfiles: file('pmd-rules.xml').toURI().toString()) {
formatter(type: 'text',toConsole: 'true')
fileset(dir: 'src')
}
}
如果存在依賴衝突,在默認情況下,Gradle會選擇最新版本,這和Maven是不同的,Maven會選擇離依賴樹最近的版本。當然,我們可以通過設置Configuration的resolutionStrategy來
重新設置依賴衝突的處理規則,對此本文將不予講解。
除了可以加入Maven和Ivy的Repository中的依賴之外,Gradle還允許我們聲明對其他Project或者文件系統的依賴。比如,如果ProjectA的compileJava依賴於ProjectB,那麼可以在ProjectA中聲明如下:
dependencies {
compile project(':ProjectB')
}
另外,對於本地文件系統中的Jar文件,我們可以通過以下方式聲明對其的依賴:
dependencies {
compile files('spring-core.jar','spring-aap.jar')
compile fileTree(dir: 'deps',include: '*.jar')
}
構建多個Project
Gradle爲每個build.gradle都會創建一個相應的Project領域對象,在編寫Gradle腳本時,我們實際上是在操作諸如Project這樣的Gradle領域對象。在多Project的項目中,我們會操作多個Project領域對象。Gradle提供了強大的多Project構建支持。
要創建多Project的Gradle項目,我們首先需要在根(Root)Project中加入名爲settings.gradle的配置文件,該文件應該包含各個子Project的名稱。比如,我們有一個根Project名爲root-project,它包含有兩個子Project,名字分別爲sub-project1和sub-project2,此時對應的文件目錄結構如下:
root-project/
sub-project1/
build.gradle
sub-project2/
build.gradle
build.gradle
settings.gradle
root-project本身也有自己的build.gradle文件,同時它還擁有settings.gradle文件位於和build.gradle相同的目錄下。此外,兩個子Project也擁有他們
自己的build.gradle文件。
要將sub-project1和sub-project2加入到root-project的子Project中,我們需要在settings.gradle中加入:
include 'sub-project1', 'sub-project2'
接下來,我們來定義一個Task用於顯示每個Project各自的名稱。我們可以在每個build.gradle進行定義,但是這卻是一種比較笨的方法,此時我們也完全沒有享受到Gradle的多Project構建功能所帶來的好處。在Gradle中,我們可以通過根Project的allprojects()方法將配置一次性地應用於所有的Project,當然也包括定義Task。比如,在root-project的build.gradle中,我們可以做以下定義:
allprojects {
apply plugin: 'idea'
task allTask << {
println project.name
}
}
以上Gradle腳本將閉包中的代碼應用在所有的Project中,包括root-project本身。我們首先將應用了idea Plugin用於生成IntelliJ工程,其次我們定義了名爲allTask的Task,該
Task應用於每個Project,作用是輸出各個Project的名稱。執行“gradle allTask”,命令行輸出如下:
:allTask
root-project
:sub-project1:allTask
sub-project1
:sub-project2:allTask
sub-project2
我們看到,該allTask對於每個Project都執行了一次,在執行時輸出了當前Project的名稱。
除了allprojects()之外,Project還提供了subprojects()方法用於配置所有的子Project(不包含根Project)。比如,我們可以定義Task來只輸出各個子Project的名字:
subprojects {
task subTask << {
println project.name
}
}
執行“gradle subTask”,命令行輸出如下:
:sub-project1:subTask
sub-project1
:sub-project2:subTask
sub-project2
此時的輸出中不再包含root-project的名字。
上文中已經提到,在Gradle腳本中,我們實際上是在操作一些領域對象,因此我們可以將groovy的所有語言特性用在Gradle的領域對象上,比如我們可以對Project進行過濾:
configure(allprojects.findAll {it.name.startsWith('sub') }) {
subTask << {
println 'this is a subproject'
}
}
在上面的代碼中,我們先找到所有Project中名字以“sub”開頭的Project,然後再對這些Project進行配置,在配置中,我們向這些Project的subTask中加入了一條額外的打印語句。
此時如果再執行“gradle subTask”,命令行輸出如下:
:sub-project1:subTask
sub-project1
this is a sub project
:sub-project2:subTask
sub-project2
this is a sub project
到此爲止,我們所有的Task定義工作都是在root-project中進行的,而sub-project1和sub-project2中的build.gradle文件依然什麼都沒有。事實上,我們可以將所有對子Project的配置均放在根Project中進行。在上面的例子中,我們通過allprojects()和subprojects()將所有的子Project都包含在了配置之內,其實我們還可以對單個Project進行單獨配置。比如,在root-project
的build.gradle中加入:
project(':sub-project1') {
task forProject1 << {
println 'for project 1'
}
}
以上腳本向sub-project1中加入了名爲forProject1的Task,在執行“gradle forProject1”時,終端輸出如下:
:sub-project1:forProject1
for project 1
這裏有一個問題:我們是在root-project下執行的命令,因此照理說Gradle會認爲forProject1是定義在所有的Project上,而此時只有sub-project1才擁有該Task,Gradle應該拋出異常指示在root-project和sub-project2上找不到該Task纔對,爲什麼它還是執行成功了呢?原因在於:只有當一個Task沒有在任何Project中定義時,Gradle纔會將其當做異常。否則,Gradle會在所有擁有該Task的Project上執行該Task。
一旦有了多個Project,他們之間便會存在着依賴關係。Gradle的Project之間的依賴關係是基於Task的,而不是整個Project的。
現在,讓我們來看一個Project依賴的例子。比如sub-project1中有taskA和taskB,taskA依賴於taskB:
task taskA << {
println 'this is taskA fromproject 1'
}
task taskB << {
println 'this is taskB fromproject 1'
}
taskA.dependsOn taskB
在執行“gradle taskA”時,終端輸出:
:sub-project1:taskB
this is taskB from project 2
:sub-project1:taskA
this is taskA from project 1
這個很容易理解,兩個Task都是屬於sub-project1的。但是,讓我們再向其中加入一些複雜性。我們在sub-project2中定義taskC和taskD,然後使taskA再依賴於taskC,又使taskB依賴於taskD:
//sub-project1:
taskA.dependsOn ':sub-project2:taskC'
taskB.dependsOn ':sub-project2:taskD'
//sub-project2:
task taskC << {
println 'this is taskC fromproject 2'
}
task taskD << {
println 'this is taskD fromproject 2'
}
此時再執行“gradle taskA”,終端輸出如下:
:sub-project2:taskD
this is taskD from project 2
:sub-project1:taskB
this is taskB from project 1
:sub-project2:taskC
this is taskC from project 2
:sub-project1:taskA
this is taskA from project 1
分析一下:taskA依賴於taskB,而taskB又依賴於taskD,所以sub-project1的taskD首先得到了執行,然後再執行sub-project1的taskB。之後,又由於taskA依賴於taskC,故Gradle
再次轉向sub-project1執行taskC,最後才執行taskA。
自定義Task類型
在前面的文章中我們講到,Gradle本身只是一個架子,真正起作用的是Task和Plugin。要真正瞭解Task和Plugin的工作機制並熟練運用,學會自定義Task類型和Plugin是大有裨益的。
Gradle中的Task要麼是由不同的Plugin引入的,要麼是我們自己在build.gradle文件中直接創建的。在默認情況下,我們所創建的Task是DefaultTask類型,該類型是一個非常通用的Task類型,而在有些時候,我們希望創建一些具有特定功能的Task,比如Copy和Jar等。還有時候,我們希望定義自己創建的Task類型,在本文中,我們以定義一個簡單的HelloWorldTask爲例,講解如何自定義一個Task類型,並且如何對其進行配置。
在Gradle中,我們有3種方法可以自定義Task類型。
(1)在build.gradle文件中直接定義
我們知道,Gradle其實就是groovy代碼,所以在build.gradle文件中,我們便可以定義Task類。
class HelloWorldTask extends DefaultTask {
@Optional
String message = 'I amdavenkin'
@TaskAction
def hello(){
println "hello world$message"
}
}
task hello(type:HelloWorldTask)
task hello1(type:HelloWorldTask){
message ="I am aprogrammer"
}
在上例中,我們定義了一個名爲HelloWorldTask的Task,它需要繼承自DefaultTask,它的作用是向命令行輸出一個字符串。@TaskAction表示該Task要執行的動作,即在調用該Task時,hello()方法將被執行。另外,message被標記爲@Optional,表示在配置該Task時,message是可選的。在定義好HelloWorldTask後,我們創建了兩個Task實例,第一個hello使用了默認的message值,而第二個hello1在創建時重新設置了message的值。
在執行hello時,命令行輸出如下:
:hello
hello world I am davenkin
BUILD SUCCESSFUL
Total time: 2.139 secs
在執行hello1時,命令行輸出如下:
:hello1
hello world I am a programmer
BUILD SUCCESSFUL
(2)在當前工程中定義Task類型
在(1)中,我們在build.gradle中直接定義了Task的類型,這樣將Task的定義和使用混在一起。在需要定義的Task類型不多時,我們可以採用這種方法,但是在項目中存在大量的自定義Task類型時,這就不見得是中好的做法了。一種改進方法是在另外的一個gradle文件中定義這些Task,然後再apply到build.gradle文件中。這裏,我們將使用另一種方法:在buildSrc目錄下定義Task類型,Gradle在執行時,會自動地查找該目錄下所定義的Task類型,並首先編譯該目錄下的groovy代碼以供build.gradle文件使用。
在當前工程的buildSrc/src/main/groovy/davenkin目錄下創建HelloWorldTask.groovy文件,將(1)中對HelloWorldTask的定義轉移到該文件中:
package davenkin
import org.gradle.api.*
import org.gradle.api.tasks.*
class HelloWorldTask extends DefaultTask {
@Optional
String message = 'I amdavenkin'
@TaskAction
def hello(){
println "hello world$message"
}
}
這裏,我們將HelloWorldTask定義在了davenkin包下,因此在build.gradle文件中引用該Task時,我們需要它的全名稱:
task hello(type:davenkin.HelloWorldTask)
task hello1(type:davenkin.HelloWorldTask){
message ="I am aprogrammer"
}
以上的hello和hello1與(1)中的hello和hello1完成的功能相同。
(3)在單獨的項目中定義Task類型
雖然(2)中的Task定義與build.gradle分離開了,但是它依然只能應用在當前工程中。如果我們希望所定義的Task能夠用在另外的項目中,那麼(2)中的方法便不可行的,此時我們可以將Task的定義放在單獨的工程中,然後在所有使用Task的工程中通過聲明依賴的方式引入這些Task。
創建另外一個項目,將(2)中buildSrc目錄下的內容考到新建項目中,由於該項目定義Task的文件是用groovy寫的,因此我們需要在該項目的build.gradle文件中引入groovy Plugin。另外,由於該項目的輸出需要被其他項目所使用,因此我們還需要將其上傳到repository中,在本例中,我們將該項目生成的包含了Task定義的jar文件上傳到了本地的文件系統中。最終的build.gradle文件如下:
apply plugin: 'groovy'
apply plugin: 'maven'
version = '1.0'
group = 'davenkin'
archivesBaseName = 'hellotask'
repositories.mavenCentral()
dependencies {
compile gradleApi()
groovy localGroovy()
}
uploadArchives {
repositories.mavenDeployer {
repository(url: 'file:../lib')
}
}
執行“gradle uploadArchives”,所生成的jar文件將被上傳到上級目錄的lib(../lib)文件夾中。
在使用該HelloWorldTask時,客戶端的build.gradle文件可以做以下配置:
buildscript {
repositories {
maven {
url 'file:../lib'
}
}
dependencies {
classpath group: 'davenkin',name: 'hellotask', version: '1.0'
}
}
task hello(type: davenkin.HelloWorldTask)
首先,我們需要告訴Gradle到何處去取得依賴,即配置repository。另外,我們需要聲明對HelloWorldTask的依賴,該依賴用於當前build文件。之後,對hello的創建與(2)中一樣。
自定義Plugin
在Plugin中,我們可以向Project中加入新的Task,定義configurations和property等。我們3種方法可以自定義Plugin,這些方法和自定義Task類型的3種方法相似。在接下來的例子中,我們將分別通過這3種方法來創建一個DateAndTimePlugin,該Plugin定義了2個Task,分別用於輸出系統當前的日期和時間,另外,我們可以配置日期和時間的輸出格式。
(1)在build.gradle文件中直接定義Plugin
和在build.gradle文件中定義Task類型一樣,我們可以將對Plugin的定義直接寫在build.gradle中:
apply plugin: DateAndTimePlugin
dateAndTime {
timeFormat = 'HH:mm:ss.SSS'
dateFormat = 'MM/dd/yyyy'
}
class DateAndTimePlugin implementsPlugin<Project> {
void apply(Project project) {
project.extensions.create("dateAndTime", DateAndTimePluginExtension)
project.task('showTime') << {
println "Current time is" + newDate().format(project.dateAndTime.timeFormat)
}
project.tasks.create('showDate')<< {
println "Current date is" + newDate().format(project.dateAndTime.dateFormat)
}
}
}
class DateAndTimePluginExtension {
String timeFormat ="MM/dd/yyyyHH:mm:ss.SSS"
String dateFormat ="yyyy-MM-dd"
}
每一個自定義的Plugin都需要實現Plugin<T>接口,事實上,除了給Project編寫Plugin之外,我們還可以爲其他Gradle類編寫Plugin。該接口定義了一個apply()方法,在該方法中,我們可以操作Project,比如向其中加入Task,定義額外的Property等。
在上例中,我們在DateAndTimePlugin中向Project添加了2個Task,一個名爲showTime,一個名爲showDate。請注意創建這2個Task所使用的不同方法,更多的創建Task的方法,參考章節2。
每個Gradle的Project都維護了一個ExtenionContainer,我們可以通過project.extentions進行訪問,比如讀取額外的Property和定義額外的Property等。在DateAndTimePlugin中,我們向Project中定義了一個名爲dateAndTime的extension,並向其中加入了2個Property,分別爲timeFormat和dateFormat,他們又分別用於showTime和showDate。在使用該Plugin時,我們可以通過以下方式對這兩個Property進行重新配置:
dateAndTime {
timeFormat = 'HH:mm:ss.SSS'
dateFormat = 'MM/dd/yyyy'
}
(2)在當前工程中定義Plugin
在當前工程中的buildSrc/src/main/groovy/davenkin目錄下創建DateAndTimePlugin.groovy文件,將build.gradle中定義DateAndTimePlugin的代碼提取到給文件中,但是除去對DateAndTimePluginExtension的定義,因爲我們將在另外一個單獨的文件中定義DateAndTimePluginExtension。
package davenkin
import org.gradle.api.Plugin
import org.gradle.api.Project
class DateAndTimePlugin implementsPlugin<Project> {
void apply(Project project) {
project.extensions.create("dateAndTime", DateAndTimePluginExtension)
project.task('showTime') << {
println "Current time is" + newDate().format(project.dateAndTime.timeFormat)
}
project.tasks.create('showDate')<< {
println "Current date is" + newDate().format(project.dateAndTime.dateFormat)
}
}
}
再創建DateAndTimePluginExtension.groovy:
package davenkin
class DateAndTimePluginExtension {
String timeFormat = "MM/dd/yyyyHH:mm:ss.SSS"
String dateFormat ="yyyy-MM-dd"
}
這裏,我們將2個類文件都放在了davenkin包下。Gradle在執行時,會自動掃描buildSrc目錄,並會在執行Task之前構建該目錄下的內容。在build.gradle文件中,在apply該Plugin時,我們需要聲明對該Plugin的全名稱,即包含報名:
apply plugin: davenkin.DateAndTimePlugin
dateAndTime {
timeFormat = 'HH:mm:ss.SSS'
dateFormat = 'MM/dd/yyyy'
}
執行“gradle showTime”,命令行輸出如下:
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:showTime
Current time is 19:08:35.489
BUILD SUCCESSFUL
Total time: 2.995 secs
可以看到,Gradle會首先構建buildSrc目錄,然後才執行showTime(紅色部分)。
(3)在單獨的項目中創建Plugin
新建一個項目,將(2)中buildSrc目錄下的內容拷貝到該項目下,定義該項目的build.gradle文件如下:
apply plugin: 'groovy'
apply plugin: 'maven'
version = 1.0
group = 'davenkin'
archivesBaseName = 'datetimeplugin'
repositories.mavenCentral()
dependencies {
compile gradleApi()
groovy localGroovy()
}
uploadArchives {
repositories.mavenDeployer {
repository(url: 'file:../lib')
}
}
此外,我們還可以爲該Plugin重新命名,如果我們希望將該Plugin命名爲time,那麼我們需要在src/main/resources/META-INF/gradle-plugins目錄下創建名爲time.properties的文件,內容如下:
implementation-class =davenkin.DateAndTimePlugin
在執行“gradle uploadArchives”時,Gradle會將該Plugin打包成jar文件,然後將其上傳到上級目錄下的lib目錄中(../lib)。之後,在客戶端的build.gradle文件中,我們需要做如下定義:
buildscript {
repositories {
maven {
url 'file:../lib'
} }
dependencies {
classpath group: 'davenkin',name: 'datetimeplugin',
version: '1.0'
}
}
apply plugin: 'time'
dateAndTime {
timeFormat = 'HH:mm:ss.SSS'
dateFormat = 'MM/dd/yyyy'
}
首先我們配置repository以執行lib目錄,然後聲明對DateAndTimePlugin的依賴,再apply該Plugin,此時我們應該使用“time”作爲該Plugin的名稱,最後對該Plugin進行配置。
(本系列完)