【認識 Gradle】(4)看懂 Gradle Script
- gradle-series
當讀者開始學習 Gradle,輔助日常的 Java 應用程式或 Java Web 應用程式開發前,我們期待您至少在 Gradle 起手式 的文章已經安裝完成 Gradle,並能在任何路徑執行 gradle 指令。
Gradle 日常開發活動,主要就是修改 Build Script 來配合專案的需要,可能的情境如下:
- 設定 plugin 讓專案能使用特定的 task
- 設定 plugin 讓專案能產生 IDE 需要的設定
- 管理函式庫相依性
- 建立 task 客製 Build Script 輔助專案建構時的需求
- 依環境、條件產生不同的編譯結果
『經歷』這些活動之前,我們先回頭審視『起手式』裡寫過的 Gradle Build Script,下列即為 build.gradle:
/* 引用 java plugin 獲得編譯 java 專案相關的 task $ */
apply plugin: 'java'
/* 引用 application plugin 獲得執行 java 專案相關的 task $ */
apply plugin: 'application'
/* 執行 application plugin 用到的參數 $ */
mainClassName = "tw.com.codedata.HelloWorld"
/* 設定 maven repository server $ */
repositories {
mavenCentral()
}
/* 宣告專案的相依函式庫 $ */
dependencies {
compile group: 'commons-logging', name: 'commons-logging', version: '1.1.1'
compile group: 'log4j', name: 'log4j', version: '1.2.16'
}
這回的主要目標為幫助讀者瞭解為何 Build Script 能這麼寫?我們會開始接觸一些 Gradle 文件 與一點的 Groovy。
概念導讀
在初學 Gradle 時,常覺得抄範例能組合出期望的效果,但卻不知道為何能使用 apply
、 repositories
或 dependencies
這些看起來像『關鍵字』或『宣告』的語法、語意。在未能理解它是怎麼運作前,撰寫 build.gradle
總有種不踏實的感覺,因為無法自我肯定我寫的是對的,是我期望的效果。另一種情況,寫不想期望的結果而抱著挫折感,繼續土法煉鋼地處理事務。
Gradle 是以 Groovy 機制實作的 DSL,多數的 Groovy DSL 開發習慣與 Groovy 本身的寫法都能沿用。那麼未曾寫過 Groovy 的人會在心中質疑,沒學過 Groovy 的人不就被這篇教學放生了?其實我們還用不到 Groovy 太多的知識,現在先知道二件事就好:
- Groovy 有提供 Closure,我們會一直不斷地寫 Closure。實作 Groovy DSL 能大量運用 Closure 的機制。
- Gradle 的實作大量使用 Delegation Pattern
Closure 是一段被 { }
大括號包圍起來的程式片段。由於在語法上,直接支援把一段程式『打包』起來,在 Groovy 寫作時幾乎不需要像寫 Java 時,額外生一個物件來裝載特定的程式片段,例如 java.util.concurrent.Callable 或 java.lang.Runnable,儘管將程式打包起來即可。
即使你暫時還不能接受它,索性將它當成用 {} 劃出一個新的 scope,只是這個區域它是可以被移動的,被指定到變數上也是沒有問題的,下面就是一個合法的 Groovy 程式:
/* def 是 Groovy 內的萬用型別,不管是物件還是原生變數甚至 void 都能用它代替 */
/* 如果看不慣 def 的寫法,用原先 java 的宣告方式也行 */
String message = "hello groovy closure"
def codeBlock = { println message }
/* 加上小括號就當成 method 般呼叫 */
codeBlock()
/* 也能呼叫 Closure 物件的 call() 方法 */
codeBlock.call()
在 codeBlock 這個 Closure 宣告時,它的 scope 如同過去我們寫 Java 時用的 {},能往外找到其他變數的 scope。這例子充其量只是一段能被搬來搬去的『區塊』看起來沒有特別稀奇,除了語法上的看似奇妙之外它應該要有更多的功能。我們將 Closure 的實作改成這樣:
def codeBlock = { sayHello() }
將這個例子再擴充為:
/* closure.groovy */
class JavaHello {
def sayHello(){ println "Hello Java" }
}
class GroovyHello {
def sayHello(){ println "Hello Groovy" }
}
/* 在可見的 scope 內依然沒有 sayHello() 方法 */
def codeBlock = { sayHello() }
codeBlock()
改寫後它依然是一個可以被任意移動的程式區塊,但能執行嗎?至少在 codeBlock 可見的 scope 內不存在 sayHello() 方法,執行錯誤是正常的:
qty:groovyLab qrtt1$ groovy closure.groovy
Caught: groovy.lang.MissingMethodException: No signature of method: closure.sayHello() is applicable for argument types: () values: []
groovy.lang.MissingMethodException: No signature of method: closure.sayHello() is applicable for argument types: () values: []
at closure$_run_closure1.doCall(closure.groovy:11)
at closure$_run_closure1.doCall(closure.groovy)
at closure.run(closure.groovy:13)
qty:groovyLab qrtt1$ cat closure.groovy
Groovy 除了提供一個能搬來搬去的區塊之外,也提供在 Closure 找不到 method 或 property 的處理機制,那就是 delegate,中文書多將它譯為委派,也就是它知道實在不在它的 scope 內,若是有指定可以委派的物作,那就向那個物件要求『這件事就交給你辦吧!』。
/* JavaHello 這件事就交給你吧! */
codeBlock.delegate=new JavaHello()
/* 於是印出了 Hello Java */
codeBlock()
/* GroovyHello 這件事就交給你吧! */
codeBlock.delegate=new GroovyHello()
/* 於是印出了 Hello Groovy */
codeBlock()
我們稍為跳脫 Gradle 的範圍,談到 Groovy Closure 與它的 delegate 機制(當 Closure 內無法識別方法或參數時,會透過委派物件處理),這簡單的範例也是 Gradle Build Script 內的實作手法,還沒能理解 Groovy 沒關係的,但需意識到在 Gradle Build Script 內使用到的功能,多是利用 Closure 描述或定義一些事實,而在 Closure 內用到的方法、參數、變數多半是來自委派的物件。當有疑惑時,我們得翻閱文件本身與委派物件的 Javadoc 文件,而這個查閱的動作我們很快就會遇到。
Build Script 與 Project 物件
在前一篇我們寫了第一個 build.gradle
檔,它是 Gradle Build Script 的預設檔名。使用 Gradle 作為專案編譯工具的主要工作就是在維護這個檔案。
Build Script 檔案被 gradle 載入後轉換成 BuildScript 物件,本質上它是一個 Groovy Script。Groovy Script 可以設定 Base Class,它的效果就如同替 Closure 指定 delegate 一般,任何你在 Build Script 內使用的方法、屬性都會交給 Base Class 處理,對 Gradle 來說它將這個 Base Class 封裝成 Project 物件。我們能在 Project 物件的 Javadoc 找到下列的描述:
There is a one-to-one relationship between a Project and a “build.gradle" file. During build initialisation, Gradle assembles a Project object for each project which is to participate in the build
建立起 Build Script 與 Project 的關係後,就能正式地回頭看 build.gradle
該如何解讀:
apply plugin: 'java'
apply plugin: 'application'
mainClassName = "tw.com.codedata.HelloWorld"
repositories {
mavenCentral()
}
dependencies {
compile group: 'commons-logging', name: 'commons-logging', version: '1.1.1'
compile group: 'log4j', name: 'log4j', version: '1.2.16'
}
這東西本質上是一個 Java 物件,所以它就會有對應的 method 能使用,現在我們要練習的就是由 Project 的 Javadoc 內找出對應的 method 說明。
先來查查 apply 是否有定義在 Project 內:
apply plugin: 'java'
在 Javadoc 內,我們看到了二種 apply 的宣告,一個接受的參數是 Closure,另一個接受的參數是 Map。以我們的例子來說,它是使用 Map 的那一組,同時得知有 3 種 key 能使用 from
、 plugin
與 to
,我們在這裡填寫的是 plugin 加上一個已註冊 Plugin 的 id。from 也是相當常見的,它可以填一個網址或一個路徑通常被作為 include 或 import 另一個 Build Script 的功用。
看著另一個宣告知道 apply 可以寫成接收 Closure 的形式:
apply { /* do something */ }
那麼寫成 Closure 能做什麼呢?據文件的說明:
The given closure is used to configure an ObjectConfigurationAction which is then used to configure this project.
單純看這描述可能沒意會過來,它是一個 Closure 在執行時會將 delegate 設成 ObjectConfigurationAction 物件,所以當我們讓它接收 Closure 參數時,就能藉由委派的機制使用 ObjectConfigurationAction 提供的 method。立馬做個小實驗,請建立下列 build.gradle 並執行它。這簡單的範例驗證 Gradle 文件的說法與 Closure 的 delegate 指定的物件:
apply {
println delegate.class.name
println delegate instanceof ObjectConfigurationAction
}
qty:gradleLab qrtt1$ gradle
org.gradle.api.internal.plugins.DefaultObjectConfigurationAction
true
:help
Welcome to Gradle 1.8.
To run a build, run gradle <task> ...
To see a list of available tasks, run gradle tasks
To see a list of command-line options, run gradle --help
BUILD SUCCESSFUL
Total time: 4.13 secs
接下來的 repositories
與 dependencies
也是同樣的,不過它利用 Groovy 的特性簡化寫法,還原成 Java 的方法名稱應為 getRepositories() 與 getDependencies(),相信讀者都可以依循同樣的套路,找出委派物件的說明文件。
Gradle DSL 文件導讀
簡單實驗與 Javadoc 查找後,讀者已經建立的足夠的先備知識,現在回頭閱讀官網的文件就比較能理解它的描述。DSL Reference 是 Gradle Build Script 語言的參考文件。
在開頭的 Some Basics
提到有三種不同型別的 Script,其中的 Project 是我們已經認識,其他二種它會在執行 gradle 的不同時間點被引用。想知道它的用法,除了依著官網的教學與範例去拼湊外,當然就是去查詢委派物件提供的功能有哪些。這手法與研究 Project 物件提供的功能是一致的。
接著的 Build script structure 都是屬於 Project 物件提供的方法,現在讀者應該具有敏感度,看到內文描述的 Script Block 或 { }
符號就聯想起 Groovy Closure,並能繼續聯結至 delegate 物件。在這些 structure 內呼叫的方法,都是呼叫 delegate 物件提供的方法,要查詢有哪些的功能,當然就是查詢 delegate 物件的 Javadoc。當你繼續點選要查詢的項目,它最重要的資訊,就是跟你說這個 block 是委派哪一種物件處理的。例如在 allprojects
block 內,它指出委派的物件即為 Project 物件:
先能看懂這些內容,隨後的學習都能順藤摸瓜地將概念連結在一起。這也是為什麼在正式進入 Gradle 日常工作教學前,必需多安排一堂 Groovy DSL 與 Gradle 文件導讀的講解。
Groovy DSL 與 Dynamic Method
有些情況是找出派委物件後,卻還是不知道它怎麼呼叫的。因為在文件或原始碼內根本沒有同樣的 method 名稱! DependencyHandler 就是一個案例:
dependencies {
compile group: 'commons-logging', name: 'commons-logging', version: '1.1.1'
compile group: 'log4j', name: 'log4j', version: '1.2.16'
}
打開 DependencyHandler 的 Javadoc 後,卻找不到 compile 與 testCompile 方法,這是因為它們是動態產生的。這是屬於 Groovy Metaprogramming 的範圍。
對 Gradle 來說,它建立了一個 DependencyHandler 類別並覆寫 methodMissing 方法,這個方法會在呼叫一組不存在的 method 時觸發:
public Object methodMissing(String name, Object args) {
Configuration configuration = configurationContainer.findByName(name)
if (configuration == null) {
if (!getMetaClass().respondsTo(this, name, args.size())) {
throw new MissingMethodException(name, this.getClass(), args);
}
}
Object[] normalizedArgs = GUtil.collectionize(args)
if (normalizedArgs.length == 2 && normalizedArgs[1] instanceof Closure) {
return doAdd(configuration, normalizedArgs[0], (Closure) normalizedArgs[1])
} else if (normalizedArgs.length == 1) {
return doAdd(configuration, normalizedArgs[0], (Closure) null)
}
normalizedArgs.each {notation ->
doAdd(configuration, notation, null)
}
return null;
}
單純由這簡短的實作可知,它是針對專案的 Configuration 進行操作,顧名思義是在修改組態設定,並回頭對照 Script 內容,至少有 compile 組態與 testCompile 組態。這些組態設定也就是承襲至 Maven 的慣例優先於設定部分,透過 script block 影響預設值。
其他 plugin 參考目前設定運作,例如 Java Plugin 取得相依性設定,去合成適當的 CLASSPATH。此外,你可以再由 Configuration 以線索,繼續至 User Guide、DSL Reference 或 Javadoc 找到更多的說明。
內容回顧
- 本次介紹 Groovy 的特性來輔助讀者更容易看懂 Gradle Build Script
- 此次介紹的 Groovy 便於製作 DSL 的特性有
- Closure 與 delegate 物件
- Groovy Script 與 Base Class
- Dynamic Method (Metaprogramming)
- 看懂 Gradle Build Script 的要點在於提供一套概念與『自我學習』的路徑,讓讀者能自行挖掘需要的部分
- 由 DSL Reference 尋找 delegate 物件或是 Base Class
- 由 Gradle Javadoc 查詢委派物件的功能
- 當無法找到對應的功能時,試著由 Groovy 特性推敲 Gradle 是如何達到這個目標的