前言
當讀者開始學習 Gradle,輔助日常的 Java 應用程式或 Java Web 應用程式開發前,我們期待您至少在 Gradle 起手式 的文章已經安裝完成 Gradle,並能在任何路徑執行 gradle 指令。Gradle 日常開發活動,主要就是修改 Build Script 來配合專案的需要,可能的情境如下:
『經歷』這些活動之前,我們先回頭審視『起手式』裡寫過的 Gradle Build Script,下列即為 build.gradle:
- 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'
- }
概念導讀
在初學 Gradle 時,常覺得抄範例能組合出期望的效果,但卻不知道為何能使用 apply、repositories 或 dependencies 這些看起來像『關鍵字』或『宣告』的語法、語意。在未能理解它是怎麼運作前,撰寫 build.gradle 總有種不踏實的感覺,因為無法自我肯定我寫的是對的,是我期望的效果。另一種情況,寫不想期望的結果而抱著挫折感,繼續土法煉鋼地處理事務。
Gradle 是以 Groovy 機制實作的 DSL,多數的 Groovy DSL 開發習慣與 Groovy 本身的寫法都能沿用。那麼未曾寫過 Groovy 的人會在心中質疑,沒學過 Groovy 的人不就被這篇教學放生了?其實我們還用不到 Groovy 太多的知識,現在先知道二件事就好:
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()
- 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()
- /* JavaHello 這件事就交給你吧! */
- codeBlock.delegate=new JavaHello()
- /* 於是印出了 Hello Java */
- codeBlock()
- /* GroovyHello 這件事就交給你吧! */
- codeBlock.delegate=new GroovyHello()
- /* 於是印出了 Hello Groovy */
- codeBlock()
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 找到下列的描述:
建立起 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'
- }
在 Javadoc 內,我們看到了三種 apply 的宣告,一個接受的參數是 Closure,另一個接受的參數是 Map。以我們的例子來說,它是使用 Map 的那一組,同時得知有 3 種 key 能使用 from、plugin 與 to,我們在這裡填寫的是 plugin 加上一個已註冊 Plugin 的 id。from 也是相當常見的,它可以填一個網址或一個路徑通常被作為 include 或 import 另一個 Build Script 的功用。
看著另一個宣告知道 apply 可以寫成接收 Closure 的形式:
- apply { /* do something */ }
單純看這描述可能沒意會過來,它是一個 Closure 在執行時會將 delegate 設成 ObjectConfigurationAction 物件,所以當我們讓它接收 Closure 參數時,就能藉由委派的機制使用 ObjectConfigurationAction 提供的 method。立馬做個小實驗,請建立下列 build.gradle 並執行它。這簡單的範例驗證 Gradle 文件的說法與 Closure 的 delegate 指定的物件:
- apply {
- println delegate.class.name
- println delegate instanceof ObjectConfigurationAction
- }
接下來的 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'
- }
- 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;
- }
Supplement
* 認識 Gradle - (5)Gradle Task 觀念導讀
沒有留言:
張貼留言