【認識 Gradle】(4)看懂 Gradle Script
【認識 Gradle】(3)Gradle 起手式 << 前情 當讀者開始學習 Gradle,輔助日常的 Java 應用程式或 Java Web 應用程式開發前,我們期待您至少在 Gradle 起手式 的文章已經安裝完成 Gradle,並能在任何路徑執行 Gradle 日常開發活動,主要就是修改 Build Script 來配合專案的需要,可能的情境如下:
『經歷』這些活動之前,我們先回頭審視『起手式』裡寫過的 Gradle Build Script,下列即為 /* 引用 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 時,常覺得抄範例能組合出期望的效果,但卻不知道為何能使用 Gradle 是以 Groovy 機制實作的 DSL,多數的 Groovy DSL 開發習慣與 Groovy 本身的寫法都能沿用。那麼未曾寫過 Groovy 的人會在心中質疑,沒學過 Groovy 的人不就被這篇教學放生了?其實我們還用不到 Groovy 太多的知識,現在先知道二件事就好:
Closure 是一段被 即使你暫時還不能接受它,索性將它當成用 /* def 是 Groovy 內的萬用型別,不管是物件還是原生變數甚至 void 都能用它代替 */ /* 如果看不慣 def 的寫法,用原先 java 的宣告方式也行 */ String message = "hello groovy closure" def codeBlock = { println message } /* 加上小括號就當成 method 般呼叫 */ codeBlock() /* 也能呼叫 Closure 物件的 call() 方法 */ codeBlock.call() 在 codeBlock 這個 Closure 宣告時,它的 scope 如同過去我們寫 Java 時用的 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 內不存在 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 的處理機制,那就是 /* 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 找到下列的描述:
建立起 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 能使用 看著另一個宣告知道 apply 可以寫成接收 Closure 的形式: apply { /* do something */ } 那麼寫成 Closure 能做什麼呢?據文件的說明:
單純看這描述可能沒意會過來,它是一個 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 接下來的 Gradle DSL 文件導讀簡單實驗與 Javadoc 查找後,讀者已經建立的足夠的先備知識,現在回頭閱讀官網的文件就比較能理解它的描述。DSL Reference 是 Gradle Build Script 語言的參考文件。 在開頭的 接著的 先能看懂這些內容,隨後的學習都能順藤摸瓜地將概念連結在一起。這也是為什麼在正式進入 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 後,卻找不到 對 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 內容,至少有 課程回顧
|