在本文中將會涉及到:
- 使用 CliBuilder 來實現對命令行選項的支持,腳本執行時所需要的參數將通過命令行選項的方式傳遞。
- 使用 GroovyClassLoader 加載 Groovy class。
- 使用 AntBuilder 來構建 Jar 包。
開始之前
關於本文
也許您寫了一些有趣或實用的 Groovy 腳本並希望與您的朋友分享,可是您並不想強迫他們安裝 Groovy,所以您也許想要做一個小工具讓您的朋友們能夠用它編譯您的 Groovy 腳本並且生成一個可執行的 Jar 包。本文將介紹如何制作一個小工具讓您的 Groovy 腳本能夠在沒有安裝 Groovy 的環境下也能被即時編譯和打包成為可執行的 Jar,並通過此過程介紹 Groovy 的一些有趣的特性。
在本文中,我們將設計並實現一個命令行工具,並通過它來完成對 Groovy 腳本的即時編譯和打包。我們將使用 CliBuilder 來實現程序對命令行的處理;使用 AntBuilder 來處理打包的問題;使用 FileSystemCompiler 類來實現對 Groovy 腳本文件的編譯。
目標
通過本示例了解 Groovy 中 CliBuilder 的使用方法,以及 AntBuilder 在 Groovy 中的應用。
先決條件
- Eclipse IDE
- Groovy plugin for Eclipse
- Apache Ant Java library(您可以在這里找到下載地址 http://ant.apache.org/bindownload.cgi)
系統要求
由於 Eclipse IDE 和 Groovy 語言都是跨平台的,所以您可以在任何平台上編寫本示例中的程序,並將它運行在任何平台上。
利用 CliBuilder 設計命令行程序
用 Groovy 來編寫腳本是開發跨平台工具的一個不錯的途徑。隨着腳本復雜程度的不斷增長,您可能需要在您的腳本中處理命令行選項。而處理這些選項或參數並且根據情況顯示相應的幫助信息可能會是件麻煩事。 Groovy 捆綁了 Apache Commons CLI 庫作為它的一部分,然而它同時也提供了一個使用起來簡單得多的 CliBuilder。那么,接下來我們看看該如何使用它。
為程序設置命令行選項
清單 1. 創建 CliBuilder 實例及定義命令行選項
// 創建 CliBuilder 實例,並定義命令行選項 def cmdline = new CliBuilder(usage: 'GroovyPackman -[chflms] [date] [prefix]') cmdline.h( longOpt: 'help', required: false, 'show usage information' ) cmdline.d( longOpt: 'destfile', argName: 'destfile', required: false, args: 1, 'jar destintation filename' ) cmdline.m( longOpt: 'mainclass', argName: 'mainclass', required: true, args: 1, 'fully qualified main class' ) cmdline.c( longOpt: 'groovyc', required: false, 'Run groovyc' ) cmdline.s( longOpt: 'sourcepath', argName: 'sourcepath', required: true, args: 1, 'fully path to the mainclass. Must be specified with .groovy file path when -c option used.')
在以上代碼段中,我們首先創建 CliBuilder 的實例。CliBuilder 擁有一個叫做 Usage 的屬性,可以用它來顯示程序的使用說明等信息。在這里,它作為參數傳遞給構造函數將可以告訴程序的使用者有哪些選項可以使用。接着,我們使用名字為一個字母的方法來定義命令行選項,longOpt 屬性允許選項被指定為 -h 或 --help 均可。argName 屬性用來指定在使用說明中顯示的選項的參數名。而 args 屬性用來指定選項所需的參數個數。required 屬性用來告訴命令行解析器當前選項是否是必須的。函數中最后一個參數用來指定該選項的描述信息。
Groovy 語言中閉包(closure)是一個非常重要的概念,它更象是一個“代碼塊”或者方法指針,代碼在某處被定義然后在其后的調用處執行。關於閉包本文不多做介紹,更多詳細內容請參考以下文章:
實戰 Groovy: 使用閉包、ExpandoMetaClass 和類別進行元編程
GroovyDoc 中對 with(Object self, Closure closure) 方法的描述
Groovy 為我們提供了 with 方法,它允許閉包被對象的引用本身所調用,這是通過把對象賦值給閉包的 delegate 屬性並且作為參數傳遞給閉包來實現的。使用 with 方法有時可以幫助我們減少代碼量,在此我們看看使用 with() 方法定義各項參數與代碼清單 1 所使用的方法有何不同。
清單 2. 使用 with 方法后的代碼
def cmdline = new CliBuilder(usage: 'GroovyPackman -[chflms] [date] [prefix]') // 使用 Object 上的 with 方法省去 cmdline 對象的限定 cmdline.with { h longOpt: 'help', required: false, 'show usage information'; d longOpt: 'destfile', argName: 'destfile', optionalArg: true, args: 1, 'jar destintation filename'; m longOpt: 'mainclass', argName: 'mainclass', required: true, args: 1, 'fully name of main class'; c longOpt: 'groovyc', required: false, 'Run groovyc'; s longOpt: 'sourcepath', argName: 'sourcepath', required: true, args: 1, 'fully path to the mainclass. Must be specified with .groovy file path when -c option used.'; }
這樣,我們為程序設置了命令行入口。其中:
- 使用“-h “或”--help”來顯示使用幫助信息。
- 使用”-d”或”--destfile”來目標文件名用來作為生成的 .jar 文件名,這是一個可選項。
- 用”-m”或”--mainclass”來指定主類的全名,未指定”-d”選項時將被作為目標 Jar 包的文件名。
- 通過使用"-c"選項來控制是否執行編譯過程,是一個可選項。
- 當命令行中指定了"-c"時,"-s"或"--sourcepath"需指向要被編譯的腳本(包)所在的目錄;未指定"-c"時需指向 .class 文件所在的目錄。
在本文的最后一章中您將看到通過這個命令行工具(在本文中被命名為:GroovyPackman)編譯打包腳本的實例。
解析命令行輸入
清單 3. 使用 parse 方法解析命令行選項
def opt = cmdline.parse(args) if (!opt) { return } if (opt.h) { cmdline.usage() return }
Parse() 方法用來解析本程序執行時命令行輸入的參數。通過 Parser 解析命令行參數后,我們可以得到 OptionAccessor 對象的一個實例,當輸入的參數不合法時,這個對象為空。
進一步處理命令行參數
通過 opt 對象我們可以輕松的獲取到所有已經在命令行中指定的選項,我們將處理各個選項的值,使其符合我們的要求。
清單 4. 獲取命令行選項並賦值給變量
// 將命令行選項賦值給變量 def mainClass = opt.m def sourcePath = opt.s def destFile = mainClass + '.jar' if (!(sourcePath.endsWith("\\"))||!(sourcePath.endsWith("/")))( sourcePath = sourcePath + "/" ) if (opt.d) { destFile = opt.d }
使用 AntBuilder 進行 Ant 腳本編程
在處理完程序的命令行選項之后,我們將進入本示例另一個重點:使用 AntBuilder 實現腳本的編譯和打包。首先,我們來認識一下生成器。
生成器 (Builder)
生成器 (Builder) 是 Groovy 中的一個特性,它可以很方便地在 Groovy 中構造如 XML 文檔一般的樹形數據結構。而 AntBuilder 就是眾多生成器中的一員,通過它您可以毫不費力地構造 Ant 基於 XML 結構的構建文件 (build.xml),不需要處理 XML 就可以執行構建。更加令人興奮的是,與以往費力地用復制粘貼來創建 build.xml 文件不同,您不但可以編寫循環、條件語句,甚至可以利用面向對象語言的優勢。
以下代碼段展示了使用 AntBuilder 的一個非常簡單的例子。在本示例中,當用戶在命令行指定了 -c 選項時將在命令行窗口輸出其指定需要編譯的 Groovy 腳本文件。
清單 5. 創建 AntBuilder 實例以及處理編譯對象
def ant = new AntBuilder() if (opt.c) { // 檢查腳本文件是否存在 def scriptBase = mainClass.replace( '.', '/' ) def scriptFile = new File(sourcePath + scriptBase +'.groovy' ) if (!scriptFile.canRead()) { println "Cannot read script file: '${scriptFile}'" return } ant.echo( "Compiling ${scriptFile}" ) }
調用 FileSystemCompiler 即時編譯 Groovy 腳本
在本示例中,用戶在命令行中使用 -c 選項指定了有效的 Groovy 腳本文件之后,程序將對其進行編譯。我們將使用 FileSystemCompiler 類來實現腳本文件的編譯,代碼段 6 展示了這一過程。
清單 6. 用 GroovyClassLoader 的實例實現編譯過程
try{ FileSystemCompiler fc= new FileSystemCompiler() fc.commandLineCompile( [ scriptFile ] as String[] ) } catch (org.codehaus.groovy.control.MultipleCompilationErrorsException e){ println e.getMessage() println "*** Possible solution: Please copy GroovyPackman.jar to dir \"${sourcePath}\" and try again. ***" return }
FileSystemCompiler 類的實例相當於是 Groovy 的命令行編譯器 (groovyc)。在 Groovy 中,您可以直接調用 Groovy 編譯器來編譯腳本文件,其命令一般為 groovyc *.groovy。這個過程將生成一個或多個 *.class 文件,這些文件可以使用 java 命令執行(但在執行 Groovy 生成的 .class 文件時,需保證 ClassPath 中指向了 Groovy 的庫文件,例如:goovy-1.x.x.jar 等)。
在安裝有 Groovy 的系統中您也可使用命令 groovy *.groovy,同時完成編譯和運行 Groovy 腳本。與 groovyc 命令不同的是,groovy 命令不是在文件系統而是在內存中生成 *.class 文件並立即執行。
使用 Groovy 中的 Ant 任務來進行流控制
在實現了對 Groovy 腳本文件編譯的功能之后,接下來將利用 Groovy 中的 Ant 任務進行打包工作。
清單 7. 通過生成器的語法來構建類似於 XML 的結構
def GROOVY_SUPPORT = (System.getenv('GROOVY_SUPPORT'))? (new File( System.getenv('GROOVY_SUPPORT'))) : "" // 如果系統中安裝了 Groovy,打包時我們將從系統中獲取程序運行所必須的 Jar 包 if (GROOVY_SUPPORT!=""){ if (GROOVY_SUPPORT.canRead()) { // 構建可執行的 Jar 包並指定 mainclass ant.echo("Processing *.class files in ${sourcePath}...") ant.jar( destfile: destFile, compress: true ) { fileset( dir: sourcePath, includes: '**/*.class' ) zipgroupfileset( dir: GROOVY_SUPPORT, includes: 'groovy-all-*.jar' ) zipgroupfileset( dir: GROOVY_SUPPORT, includes: 'commons*.jar' ) zipgroupfileset( dir: GROOVY_SUPPORT, includes: 'ant*.jar' ) // 您可以在此添加更多的 Jar 包,這取決於您的需求 manifest { attribute( name: 'Main-Class', value: mainClass ) attribute( name: 'Class-Path', value: '.' ) } } } } // 如果系統未檢測到 Groovy 環境,打包時將使用 GroovyPackman.jar 中獲取程序運行所必須的 Jar 包 else { ant.echo( "Missing environment variable GROOVY_SUPPORT: '${GROOVY_SUPPORT}'" ) def PACKMAN = URLDecoder.decode( GroovyPackman.class.getProtectionDomain().getCodeSource().getLocation().getFile(), "UTF-8") PACKMAN = PACKMAN.toString().replaceFirst("/", "") ant.echo( "Using Packman: '${PACKMAN}'" ) ant.jar( destfile: destFile, compress: true) { fileset( dir: sourcePath, includes: '**/*.class' ) zipfileset( excludes: 'GroovyPackman*.*,org/apache/tools/**,images/**', src: PACKMAN) // 您可以根據具體需要增加更多的 Jar manifest { attribute( name: 'Main-Class', value: mainClass ) attribute( name: 'Class-Path', value: '.' ) } } }
在代碼清單 7 中,首先檢查環境變量 GROOVY_SUPPORT 是否存在,該環境變量通常在安裝 Groovy Eclipse Plugin 之后會被設置,指向包含 Groovy 運行庫文件所在的目錄。接着再分別針對 GROOVY_SUPPORT 存在與否來完成不同的打包過程。當 GROOVY_SUPPORT 可用時我們將從 GROOVY_SUPPORT 指向的目錄中拾取相關的運行庫(如:groovy-all-*.jar,commons*.jar,ant*.jar 等);而當 GROOVY_SUPPORT 不可用即系統中未安裝 Groovy 時,程序將從自身拾取相應的運行庫並將其與編譯后的腳本一同打包。
最后,編譯我們的程序
第一步,我們需要在 Eclipse 中編譯本腳本文件,生成 *.class。要在 Eclipse 中編譯 Groovy 腳本,可以通過點擊菜單 Run -> Run As -> Groovy Script。
圖 1. 通過 Eclipse 編譯后生成的 .class 文件
為了便於分享同時執行起來更加方便,第二步,我們將用 Java 命令執行生成的 .class 文件並將其自身打包成可執行的 Jar 包。
清單 8. 運行 GroovyPackman.class 將其自身打包
C:\groovy\GroovyPackman\bin>set GROOVY_SUPPORT= d:\eclipse\plugins\org.codehaus.groovy_1.7.5.20101020-2100-e35-release\lib C:\groovy\GroovyPackman\bin>java -cp .\;%groovy_support%\;%groovy_support%\ groovy-all-1.7.5.jar;%groovy_support%\commons-cli-1.2.jar;%groovy_support%\ant.jar; %groovy_support%\ant-launcher.jar GroovyPackman -m GroovyPackman -s c:\Groovy\GroovyPackman\bin
圖 2. 執行 GroovyPackman.class 將其自身打包
看看這個小程序的效果如何
即時編譯和打包一組示例 Groovy 腳本
在這個例子中,我們使用一個非常簡單的 helloworld 程序來做演示,看看如何用我們剛才制作的小工具來進行編譯和打包。這個 helloworld 程序包含兩個 *.groovy 腳本文件,本身為三層目錄結構的包,如圖 3 所示:
圖 3. 演示程序中所包含的兩個 groovy 腳本
在這里,我們在命令行中執行以下命令:
清單 9. 測試 GroovyPackman.jar
java -jar GroovyPackman.jar
測試一下之前打包好的小工具能否正常運行。如圖 4 所示:
圖 4. GroovyPackman.jar 在命令行中執行
如上圖命令行中的輸出結果,GroovyPackman.jar 運行正常。接下來開始編譯和打包 hellowworld 示例程序。
將 GroovyPackman.jar 拷貝到 com.test.helloworld 包所在的根目錄。在命令行中執行以下命令:
清單 10. 運行 GroovyPackman.jar 打包 helloworld 程序
java -jar GroovyPackman.jar -m com.test,helloworld.helloworld -s c:\groovy -c
helloworld 將被自動編譯並打包,如圖 5 所示:
圖 5. Helloworld 程序被打包
從圖 6 中可以看到,*.groovy 腳本文件已經被編譯成多個 *.class 文件。
圖 6. 打包過程中生成的 .class 文件
從圖 7 中可以看到已經打包好的 helloworld 程序。
圖 7. 打包后的 helloworld 程序
接下來我們在命令行中運行剛剛生成的 helloworld 程序,結果如圖 8 所示:
圖 8. 打包后的 Helloworld 運行結果
參考資料
學習
- 想了解更多關於在 Groovy 中使用 Ant 的技巧請參考 用 Gant 構建軟件(developerWorks)。
- 您還可以訪問 精通 Groovy(developerWorks)來學習更多更精彩的 Groovy 知識。
- “演化架構和緊急設計:使用 Groovy 構建 DSL”(developerWorks,2010 年 9 月):內部特定領域語言(DSL)是可行的,但是由於 Java 語言的限制性語法使其使用並不靈活。JVM 中其他語言更適合構建它們。這一期的演化架構和緊急設計涵蓋了許多您可以利用的功能,以及在您使用 Groovy 構建內部 DSL 時將要遇到的問題。
- “函數式思維:Groovy 中的函數式特性,第 1 部分”(developerWorks,2011 年 12 月):隨着時間的推移,語言和運行時為我們處理了越來越多瑣碎的細節。函數式語言在這方面體現了它的趨勢,而且現代的動態語言也采用了許多函數式特性,讓開發者的工作變得更輕松。這期文章將介紹 Groovy 中的一些函數式特性,並將展示如何用遞歸隱藏狀態和構建惰性列表。
- “函數式思維:Groovy 中的函數式特性,第 2 部分”(developerWorks,2012 年 2 月):憑借 Groovy,元編程 (metaprogramming) 和函數式編程形成了一個強有力的組合。了解元編程如何支持您為 Integer 數據類型添加方法,從而使您能夠利用 Groovy 的內置函數式特性。學習如何使用元編程將 Functional Java 框架的豐富函數式特性集無縫地整合到 Groovy 中。
- “函數式思維:Groovy 中的函數式特性,第 3 部分”(developerWorks,2012 年 3 月):現代的動態語言整合了許多函數式特性,以幫助開發人員完成平常的任務。本文將使用 Groovy 探索在函數級別應用緩存的好處,並將這種緩存方法與一種命令式方法進行對比。本文將演示兩種類型的緩存,即方法內緩存和外部緩存,還將探討命令式和函數式版本的優缺點。
- developerWorks Java 技術專區:這里有數百篇關於 Java 編程各個方面的文章。
討論
- 加入 developerWorks 中文社區。查看開發人員推動的博客、論壇、組和維基,並與其他 developerWorks 用戶交流。
http://www.ibm.com/developerworks/cn/java/j-lo-groovypackage/index.html
http://www.ibm.com/developerworks/cn/java/j-pg06239.html