# 實戰 Groovy: 用 Groovy 進行 Ant 腳本編程
_為更具表現力、更可控的構建而組合使用 Ant 和 Groovy_
Ant 和 Maven 兩者在構建處理工具的世界中占統治地位。但是 XML 卻湊巧是一種非常沒有表現力的配置格式。在“實戰 Groovy”這個新系列的第 2 期中,Andrew Glover 將介紹 Groovy 的生成器實用工具,這個工具能夠極其容易地把 Groovy 與 Ant 和 Maven 結合在一起,形成更具表現力、更可控的構建。
Ant 作為 Java 項目構建工具的普遍性和實用性是無法超越的。即使是 Maven 這個構建領域的新銳工具,也要把自己的許多強大能力歸功于從 Ant 學到的經驗。但是,這兩個工具有共同的不足之處:擴展性。即使 XML 的可移植性在促進 Ant 和 Maven 走向開發前端上扮演了主要角色,XML 作為構建配置格式的角色仍然或多或少地限制了構建過程的表現力。
例如,雖然在 Ant 和 Maven 中都有條件邏輯,但是用 XML 表示有些繁瑣。而且,雖然可以定義定制任務來擴展 Ant 的構建過程,但是這么做通常會把應用程序的行為局限在有限的沙箱中。因此,在本文中,我將向您演示如何把 Groovy 和 Ant _在_ Maven _內部_ 結合起來,形成更強大的表現力,從而對構建過程進行更好的行為控制。
很快您自己就會看到,Groovy 給 Ant 和 Maven 都帶來了令人矚目的增強,Groovy 的優點在于它常常能恰到好處地彌補 XML 的遺漏!實際上,看起來 Groovy 的創建者肯定也曾經歷過用 XML 實施 Ant 和 Maven 構建過程的痛苦,因為他們引入了 `AntBuilder`,這是一個強大的新工具,支持在 Groovy 腳本中利用 Ant。
在 _實戰 Groovy_ 的這一期中,我將向您展示不用 XML,轉用 Groovy 作為您的構建配置格式,對構造過程進行增強是多么容易。閉包(closure)是 Groovy 的一個重要特性,而且是這門語言之所以與眾不同的核心,所以我在轉到下一節之前,先要對閉包做一個快速回顧。
## 快速回顧閉包
就像 Groovy 自己的一些非常著名的祖先一樣,Groovy 也支持 _匿名函數_ 或 _閉包(closure)_ 的概念。如果您曾經用 Python 或 Jython 編寫過代碼,那么您可能對閉包比較熟悉,在這兩種語言中,都是用 `lambda` 關鍵字引入閉包。在 Ruby 中,如果 _沒有_ 利用塊或閉包,那么您就不得不實際地編寫腳本。即使是 Java 語言,也要通過它的 _匿名內部類_,對匿名函數提供了有限形式的支持。
在 Groovy 中,閉包是能夠封裝行為的第一級匿名函數。當 Ruby 的締造者 Yukihiro Matsumoto 注意到這些強大的第一類對象可以“傳遞給另一個函數,然后函數可以調用傳入的 [閉包]”時,也開始染指閉包的應用(請參閱 [參考資料](#resources),查看完整的采訪)。當然,親自操作 Groovy 是了解閉包對于這門美好的令人激動的語言是一筆多么大的財富的最好方法。
## 關于這一系列的教程
把任何一個工具集成進您的開發實踐的關鍵是,知道什么時候使用它而什么時候把它留在箱子中。腳本語言可以是您的工具箱中極為強大的附件,但是只有在恰當地應用到適當地場景中才是這樣。為了這個目標, _實戰 Groovy_ 是一系列文章,專門介紹 Groovy 的實際應用,并教給您什么時候、如何成功地應用它們。
* * *
## 閉包實例
在清單 1 中,我又使用了一些本系列的第 1 篇文章曾經用過的代碼;但是這一次的重點是閉包。如果您已經讀過那篇文章(請參閱 [參考資料](#resources)),那么您可以回憶起我用來演示用 Groovy 進行單元測試的基于 Java 的包過濾對象。這次,我還用相同的示例開始,但是用閉包對它進行了極大的增強。下面是基于 Java 的 `Filter` 接口。
##### 清單 1\. 還記得這個簡單的 Java Filter 接口嗎?
```
public interface Filter {
void setFilter(String fltr);
boolean applyFilter(String value);
}
```
上次,在定義了 `Filter` 類型之后,我接著定義了兩個實現,這兩個實現名為 `RegexPackageFilter` 和 `SimplePackageFilter`,它們分別使用了正則表達式和簡單的 `String` 操作。
對于沒用閉包寫成的代碼來說,到現在為止還算不錯。在清單 2 中,您會開始看到只有一點語法上的變化,代碼就不同了(更好了!)。我將通過定義通用的 `Filter` 類型(如下所示)開始,但是這次是用 Groovy 定義的。請注意與 `Filter` 類關聯的 `strategy` 屬性。這個屬性是閉包的一個實例,在執行 `applyFilter` 方法的時候可以調用它。
##### 清單 2\. 更加 Groovy 的過濾器 —— 使用閉包
```
class Filter{
strategy
boolean applyFilter(str){
return strategy.call(str)
}
}
```
添加閉包意味著我不用像在原來的 `Filter` 接口中那樣必須定義一個接口類型,或者依賴特定的實現才能得到期望的行為。相反,我可以定義一個通用的 `Filter` 類型,并給它提供一個閉包,讓它在執行 `applyFilter` 方法期間應用。
下一步是定義兩個閉包。第一個通過對指定參數應用簡單的 `String` 操作來模擬 `SimplePackageFilter`(來自前一篇文章)。當創建新的 `Filter` 類型時,對應的名為 `simplefilter` 的閉包會被傳遞到構造器中。第二個閉包(在幾個進行代碼驗證的 `assert` 之后出現)對指定的 `String` 應用正則表達式。這次我還是要創建一個新的 `Filter` 類型,向正則表達式傳遞一個名為 `rfilter` 的閉包,并執行一些 `assert`,以確保每件事都正常。所有細節如清單 3 所示:
##### 清單 3\. 使用 Groovy 閉包的簡單魔術
```
simplefilter = { str |
if(str.indexOf("java.") >= 0){
return true
}else{
return false
}
}
fltr = new Filter(strategy:simplefilter)
assert !fltr.apply("test")
assert fltr.apply("java.lang.String")
rfilter = { istr |
if(istr =~ "com.vanward.*"){
return true
}else{
return false
}
}
rfltr = new Filter(strategy:rfilter)
assert !rfltr.apply("java.lang.String")
assert rfltr.apply("com.vanward.sedona.package")
```
非常令人難忘,對吧?使用閉包,我能夠把對期望行為的定義推遲到運行時 —— 所以不必像在以前的設計中要做的那樣再定義新的 `Filter` 類型并編譯它。雖然我用 Java 代碼時能用匿名內部類做類似的事情,但是用 Groovy 更容易、神秘性更少一些。
閉包確實是非常強大的家伙。它們還代表處理行為的另外一種方式 —— 而 Groovy(以及它的遠親 Ruby)對它有非常嚴重的依賴。所以閉包會是我們下一個主題的要點,下一個主題是用生成器進行構建。
* * *
## 用生成器進行構建
使 Groovy 中的 Ant 更迷人的核心之處是 _生成器_。實際上,生成器允許您很方便地在 Groovy 中表示樹形數據結構,例如 XML 文檔。而且,女士們先生們請看,秘密在這:使用生成器,特別是 `AntBuilder`,您可以毫不費力地構造 Ant 的 XML 構建文件,不必處理 XML 就可以 _執行生成的行為_。而這并不是在 Groovy 中使用 Ant 的惟一優點。與 XML 不同,Groovy 是非常有表現力的開發環境,在這個環境中,您可以容易地編寫循環結構、條件選擇代碼,甚至可以利用“重用”的威力,而不必像以前那樣,費力地用剪切-粘貼操作來創建新 build.xml 文件。而且您做這些工作時,完全是在 Java 平臺中!
生成器的優點,尤其是 Groovy 的 `AntBuilder`,在于它們的語法表示完全體現了它們所代表的 XML 文件的邏輯進程。被附加在 `AntBuilder` 實例上的方法與對應的 Ant 任務匹配;同樣的,這些方法可以接收參數(以 `map` 的形式),參數對應著任務的屬性。而且,嵌套標簽(例如 `include` 和 `fileset`)也定義成閉包。
### 構建塊:示例 1
我要用一個超級簡單的示例向您介紹生成器:一個叫做 `echo` 的 Ant 任務。在清單 4 中,我創建了一個普通的、每天都會用到的 Ant 的 `echo` 標記的 XML 版本(用在這不要奇怪):
##### 清單 4\. Ant 的 Echo 任務
```
<echo message="This was set via the message attribute"/>
<echo>Hello World!</echo>
```
事情在清單 5 中變得更有意思了:我用相同的 Ant 標簽,并在 Groovy 中用 `AntBuilder` 類型重新定義了它。注意,我可以使用 `echo` 的屬性 `message`,也可以只傳遞一個期望的 `String`。
##### 清單 5\. 用 Groovy 表示的 Ant 的 Echo 任務
```
ant = new AntBuilder()
ant.echo(message:"mapping it via attribute!")
ant.echo("Hello World!")
```
生成器特別吸引人的地方是它可以讓我把普通的 Groovy 特性與生成器語法混合,從而創建豐富的行為集。在清單 6 中,您應當開始看出可能性是無窮的:
##### 清單 6\. 用 Groovy 和 Ant 進行流控制(flow control)
```
ant = new AntBuilder()
ant.mkdir(dir:"/dev/projects/ighr/binaries/")
try{
ant.javac(srcdir:"/dev/projects/ighr/src",
destdir:"/dev/projects/ighr/binaries/" )
}catch(Throwable thr){
ant.mail(mailhost:"mail.anywhere.com", subject:"build failure"){
from(address:"buildmaster@anywhere.com", name:"buildmaster")
to(address:"dev-team@anywhere.com", name:"Development Team")
message("Unable to compile ighr's source.")
}
}
```
在這個示例中,我要捕獲源代碼編譯時的錯誤條件。注意 `catch` 塊中定義的 `mail` 對象如何接受定義了 `from`、 `to` 和 `message` 屬性的閉包。
_哎呀!_Groovy 的功能真多!當然,知道什么時候應用這么聰明的特性是問題的關鍵,而我們都在不斷地為之努力。幸運的是,實踐出真知,一旦您開始使用 Groovy(或者為了這個原因使用任何腳本語言),您就會找到許多在實際工作中使用它的機會;從中了解它在哪里才真正適用。在下一節中,我將查看一個典型的、現實的示例,并在其中使用一些在這里介紹的特性。
* * *
## 應用 Groovy
對于這個示例,我們假設需要為我的代碼定期建立校驗和(checksum)報告。一旦實現了這個報告,就可以用它在我需要的時候檢驗文件的完整性。下面是校驗和報告的高級技術用例:
1. 編譯所有的源代碼。
2. 對二進制類文件運行 md5 算法。
3. 創建簡單的報告,列出每個類文件及其對應的校驗和。
完全擯棄 Ant 或 Maven,整個構建過程都使用 Groovy,在這個例子中,這樣做有點極端。但實際上,正如我前面解釋的,Groovy 是這些工具的極大 _增強_,而 _不是_ 替代。所以,用 Groovy 的表現力處理后兩項任務,而把第一步信托給 Ant 或 Maven,這樣做才有意義。
實際上,我們只要假設我在第一步中用的是 Maven,因為坦白地說,它是我個人偏愛的構建平臺。在 Maven 中可以很容易地用 `java:compile` 和 `test:compile` 目標編譯源文件;所以我保持這部分內容原封不動,讓我的新目標引用前面的目標,把前面的目標作為前提條件。實際就是這樣 —— 使用編譯好的源文件,我準備繼續運行校驗和工具(checksum utility);但是首先還需要做一些簡單的設置。
* * *
## 設置 Md5ReportBuilder
為了通過 Ant 運行這個漂亮的校驗和工具,我需要兩條信息:哪個目錄包含要進行校驗和處理的文件,報告要寫到哪個目錄。對于工具的第一個參數,我希望它是一個用逗號分隔的目錄列表。對后一個參數,我希望是報告目錄。
我決定調用工作類 `Md5ReportBuilder`。清單 7 中定義了它的 `main` 方法:
##### 清單 7\. Md5ReportBuilder 的 Main 方法
```
static void main(args) {
assert args[0] && args[1] != null
dirs = args[0].split(",")
todir = args[1]
report = new Md5ReportBuilder()
report.runCheckSum(dirs)
report.buildReport(todir)
}
```
上述步驟中的第一步是檢查這兩個參數是不是傳遞到工具中。然后我把第一個參數用逗號進行分割,創建了一個數組,保存要在上面運行 Ant 的 `checksum` 任務的目錄。最后,我創建 `Md5ReportBuilder` 類的新實例,調用兩個方法處理所需要的功能。
* * *
## 添加校驗和
Ant 包含一個 `checksum` 任務,調用起來非常容易,但需要傳遞一個 `fileset` 給它,其中包含目標文件的集合。在這個例子中,目標文件是包含編譯后的源文件的目錄,以及對應的單元測試文件。我可以通過使用 `for` 循環中的迭代得到這些文件,在這個例子中,是在目錄集合中進行迭代。對于每個目錄,調用 `checksum` 任務;而且 `checksum` 任務只在 .class 文件上運行,如清單 8 所示:
##### 清單 8\. runCheckSum 方法
```
/**
* runs checksum task for each dir in collection passed in
*/
runCheckSum(dirs){
ant = new AntBuilder()
for(idir in dirs){
ant.checksum(fileext:".md5.txt" ){
fileset(dir:idir) {
include(name:"**/*.class")
}
}
}
}
```
* * *
## 構建報告
從這一點起,構建報告就變成了循環練習。讀取每個新生成的校驗和文件,把該文件對應的信息送到 `PrintWriter` 方法,然后將 XML 寫入文件 —— 雖然用的是最丑陋的形式,如清單 9 所示:
##### 清單 9\. 構建報告
```
buildReport(bsedir){
ant = new AntBuilder()
scanner = ant.fileScanner {
fileset(dir:bsedir) {
include(name:"**/*class.md5.txt")
}
}
rdir = bsedir + File.separator + "xml" + File.separator
file = new File(rdir)
if(!file.exists()){
ant.mkdir(dir:rdir)
}
nfile = new File(rdir + File.separator + "checksum.xml")
nfile.withPrintWriter{ pwriter |
pwriter.println("<md5report>")
for(f in scanner){
f.eachLine{ line |
pwriter.println("<md5 class='" + f.path + "' value='" + line + "'/>")
}
}
pwriter.println("</md5report>")
}
}
```
那么,如何處理這個報告呢?首先,我用 `FileScanner` 找到清單 8 中的 `checksum` 方法創建的每個校驗和文件。然后,我創建一個新目錄,在這個目錄中再創建一個新文件。(如果我能通過一個簡單的 `if` 語言檢查目錄是否已經存在,會不會更好一些?)然后我打開對應的文件,并用一個漂亮的閉包從 `scanner` 集合讀取每個對應的 `File`。示例最后用寫入 XML 元素的報告內容進行總結。
## Goal 就是您的目標!
不熟悉 Maven 的讀者可能想知道 `goal` 談的都是什么。把 Maven 中的 `goal` 當成 Ant 中的 `target` 就可以了。 _goal_ 就是組織行為的一種方式。Maven 的 `goal` 被賦予名稱,可以通過 `maven` 命令調用它們。在調用時,在指定 `goal` 找到的任務都會被執行。
我敢打賭您立刻就注意到在 `File` 的那些實例上的 `withPrintWriter` 方法是多么強大。我不需要考慮異常或關閉文件,因為它替我處理了每件事。我只是把我希望的行為通過閉包傳遞給它,然后就準備就緒了!
* * *
## 運行工具
這個 Groovy 巡演中的下一站是把它裝配進構建過程,特別是放在我的 maven.xml 文件中。非常幸運的是,這是這個相對容易的練習中最容易的部分,如清單 10 所示:
##### 清單 10\. 在 Maven 中運行 Md5ReportBuilder
```
<goal name="gmd5:run" prereqs="java:compile,test:compile">
<path id="groovy.classpath">
<ant:pathelement path="${plugin.getDependencyClasspath()}"/>
<ant:pathelement location="${plugin.dir}"/>
<ant:pathelement location="${plugin.resources}"/>
</path>
<java classname="groovy.lang.GroovyShell" fork="yes">
<classpath refid="groovy.classpath"/>
<arg value="${plugin.dir}/src/groovy/com/vanward/md5builder/Md5ReportBuilder.groovy"/>
<arg value="${maven.test.dest},${maven.build.dest}"/>
<arg value="${maven.build.dir}/md5-report"/>
</java>
</goal>
```
正如前面所解釋過的,我已經規定必須在完整的編譯之前運行校驗和工具,所以,我的目標有兩個前提條件: `java:compile` 和 `test:compile`。類路徑 Classpath 總是很重要,所以我專門為了讓 Groovy 運行特別創建了一個正確的 classpath。最后清單 10 所示的目標調用 Groovy 的外殼,把要運行的腳本和腳本對應的兩個參數傳給外殼 —— 第一個是要在其上運行校驗和(用逗號分隔的)目錄,第二個是報告要寫入的目錄。
* * *
## 最后一步
完成對 maven.xml 文件的編碼之后,就差不多完成了所有要做的事,但是還有最后一步:我需要用所需的依賴關系來更新 `project.xml` 文件。沒什么好奇怪的,Maven 需要具有一些必要的 Groovy 依賴項才能工作。這些依賴項是匯編代碼、字節碼操縱庫、公共命令行(commons-cli-處理命令行解析)Ant、以及對應的 ant 啟動器。這個示例的依賴項如清單 11 所示:
##### 清單 11\. Groovy 必需的依賴項
```
<dependencies>
<dependency>
<groupId>groovy</groupId>
<id>groovy</id>
<version>1.0-beta-6</version>
</dependency>
<dependency>
<groupId>asm</groupId>
<id>asm</id>
<version>1.4.1</version>
</dependency>
<dependency>
<id>commons-cli</id>
<version>1.0</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant-launcher</artifactId>
<version>1.6.1</version>
</dependency>
</dependencies>
```
* * *
## 結束語
在 _實戰 Groovy_ 的第 2 期中,您看到了 Groovy 的表現力和靈活性與 Ant 和 Maven 無與倫比的應用結合在一起時發生了什么。對于任何一個工具,Groovy 都提供了有吸引力的替代 XML 的構建格式。它讓您用循環構造和條件邏輯控制程序流,極大地增強了構建過程。
在向您展示 Groovy 的實用一面的同時,這個系列還專門介紹了它最適當的應用。應用任何技術的要點之一是認真思考它要應用的環境。在這個案例中,我向您展示了如何用 Groovy _增強_ 而不是 _替換_ 已經非常強大的工具:Ant。
下個月,我要介紹 Groovy 的另外一項依賴閉包的特性。GroovySql 是個超級方便的小工具,可以使數據庫查詢、更新、插入以及所有對應的邏輯管理起來特別容易!下期再見!
- 實戰 Groovy
- 實戰 Groovy: SwingBuilder 和 Twitter API,第 2 部分
- 實戰 Groovy: SwingBuilder 和 Twitter API,第 1 部分
- 實戰 Groovy: @Delegate 注釋
- 實戰 Groovy: 使用閉包、ExpandoMetaClass 和類別進行元編程
- 實戰 Groovy: 構建和解析 XML
- 實戰 Groovy: for each 剖析
- 實戰 Groovy: Groovy:Java 程序員的 DSL
- 實戰 Groovy: 關于 MOP 和迷你語言
- 實戰 Groovy: 用 curry 過的閉包進行函數式編程
- 實戰 Groovy: Groovy 的騰飛
- 實戰 Groovy: 在 Java 應用程序中加一些 Groovy 進來
- 實戰 Groovy: 用 Groovy 生成器作標記
- 實戰 Groovy: 用 Groovy 打造服務器端
- 實戰 Groovy: 使用 Groovy 模板進行 MVC 編程
- 實戰 Groovy: 用 Groovy 進行 JDBC 編程
- 實戰 Groovy: 用 Groovy 進行 Ant 腳本編程
- 實戰 Groovy: 用 Groovy 更迅速地對 Java 代碼進行單元測試
- alt.lang.jre: 感受 Groovy