嵌入簡單的、易于編寫的腳本,從而利用 Groovy 的簡單性
Andrew Glover
CTO, Vanward Technologies
2005 年 6 月 13 日
您有沒有想過在自己相對復雜的 Java 程序中嵌入 Groovy 簡單的、易于編寫的腳本呢?在這一期 實戰 Groovy 系列文章中,Andrew Glover 將介紹把 Groovy 集成到 Java 代碼中的多種方法,并解釋在什么地方、什么時候適合這么做。
如果您一直在閱讀這個系列,那么您應該已經看到有各種各樣使用 Groovy 的有趣方式,Groovy 的主要優勢之一就是它的生產力。Groovy 代碼通常要比 Java 代碼更容易編寫,而且編寫起來也更快,這使得它有足夠的資格成為開發工作包中的一個附件。在另一方面,正如我在這個系列中反復強調的那樣,Groovy 并不是 —— 而且也不打算成為 —— Java 語言的替代。所以,這里存在的問題是,能否把 Groovy 集成到 Java 的編程實踐中?或者說這樣做有什么用?什么時候 這樣做有用?
這個月,我將嘗試回答這個問題。我將從一些熟悉的事物開始,即從如何將 Groovy 腳本編譯成與 Java 兼容的類文件開始,然后進一步仔細研究 Groovy 的編譯工具(groovyc
)是如何讓這個奇跡實現的。了解 Groovy 在幕后做了什么是在 Java 代碼中使用 Groovy 的第一步。
注意,本月示例中演示的一些編程技術是 Groovlets
框架和 Groovy 的 GroovyTestCase
的核心,這些技術我在前面的文章中已經討論過。
關于本系列 把任何工具集成到自己的開發實踐的關鍵就是知道什么時候使用它,而什么時候應當把它留在箱子里。腳本語言能夠成為工具箱中極為強大的附件,但只在將它恰當應用到合適場景時才這樣。為此,實戰 Groovy 的一系列文章專門探索了 Groovy 的實際應用,并告訴您什么時候應用它們,以及如何成功地應用它們。 |
天作之合?
在本系列中以前的文章中,當我介紹如何 用 Groovy 測試普通 Java 程序 的時候,您可能已經注意到一些奇怪的事:我 編譯了 那些 Groovy 腳本。實際上,我將 groovy 單元測試編譯成普通的 Java .class 文件,然后把它們作為 Maven 構建的一部分來運行。
這種編譯是通過調用 groovyc
命令進行的,該命令將 Groovy 腳本編譯成普通的 Java 兼容的 .class 文件。例如,如果腳本聲明了一個類,那么調用 groovyc
會生成至少三個 .class 。文件本身會遵守標準的 Java 規則:.class 文件名稱要和聲明的類名匹配。
作為示例,請參見清單 1,它創建了一個簡單的腳本,腳本聲明了幾個類。然后,您自己就可以看出 groovyc
命令生成的結果:
清單 1. Groovy 中的類聲明和編譯
package com.vanward.groovy
class Person {
fname
lname
age
address
contactNumbers
String toString(){
numstr = new StringBuffer()
if (contactNumbers != null){
contactNumbers.each{
numstr.append(it)
numstr.append(" ")
}
}
"first name: " + fname + " last name: " + lname +
" age: " + age + " address: " + address +
" contact numbers: " + numstr.toString()
}
}
class Address {
street1
street2
city
state
zip
String toString(){
"street1: " + street1 + " street2: " + street2 +
" city: " + city + " state: " + state + " zip: " + zip
}
}
class ContactNumber {
type
number
String toString(){
"Type: " + type + " number: " + number
}
}
nums = [new ContactNumber(type:"cell", number:"555.555.9999"),
new ContactNumber(type:"office", number:"555.555.5598")]
addr = new Address(street1:"89 Main St.", street2:"Apt #2",
city:"Utopia", state:"VA", zip:"34254")
pers = new Person(fname:"Mollie", lname:"Smith", age:34,
address:addr, contactNumbers:nums)
println pers.toString()
|
在清單 1 中,我聲明了三個類 —— Person
、Address
和 ContactNumber
。之后的代碼根據這些新定義的類型創建對象,然后調用 toString()
方法。迄今為止,Groovy 中的代碼還非常簡單,但現在來看一下清單 2 中 groovyc
產生什么樣的結果:
清單 2. groovyc 命令生成的類
aglover@12d21 /cygdrive/c/dev/project/target/classes/com/vanward/groovy
$ ls -ls
total 15
4 -rwxrwxrwx+ 1 aglover user 3317 May 3 21:12 Address.class
3 -rwxrwxrwx+ 1 aglover user 3061 May 3 21:12 BusinessObjects.class
3 -rwxrwxrwx+ 1 aglover user 2815 May 3 21:12 ContactNumber.class
1 -rwxrwxrwx+ 1 aglover user 1003 May 3 21:12
Person$_toString_closure1.class
4 -rwxrwxrwx+ 1 aglover user 4055 May 3 21:12 Person.class
|
哇!五個 .class 文件!我們了解 Person
、Address
和 ContactNumber
文件的意義,但是其他兩個文件有什么作用呢?
研究發現,Person$_toString_closure1.class
是 Person
類的 toString()
方法中發現的閉包的結果。它是 Person
的一個內部類,但是 BusinessObjects.class
文件是怎么回事 —— 它可能是什么呢?
對 清單 1 的深入觀察指出:我在腳本主體中編寫的代碼(聲明完三個類之后的代碼)變成一個 .class 文件,它的名稱采用的是腳本名稱。在這個例子中,腳本被命名為 BusinessObjects.groovy
,所以,類定義中沒有包含的代碼被編譯到一個名為 BusinessObjects
的 .class 文件。
反編譯
反編譯這些類可能會非常有趣。由于 Groovy 處于代碼頂層,所以生成的 .java 文件可能相當巨大;不過,您應當注意的是 Groovy 腳本中聲明的類(如 Person
) 與類之外的代碼(比如 BusinessObjects.class
中找到的代碼)之間的區別。在 Groovy 文件中定義的類完成了 GroovyObject
的實現,而在類之外定義的代碼則被綁定到一個擴展自 Script
的類。
例如,如果研究由 BusinessObjects.class 生成的 .java 文件,可以發現:它定義了一個 main()
方法和一個 run()
方法。不用驚訝, run()
方法包含我編寫的、用來創建這些對象的新實例的代碼,而 main()
方法則調用 run()
方法。
這個細節的全部要點再一次回到了:對 Groovy 的理解越好,就越容易把它集成到 Java 程序中。有人也許會問:“為什么我要這么做呢?”好了,我們想說您用 Groovy 開發了一些很酷的東西;那么如果能把這些東西集成到 Java 程序中,那不是很好嗎?
只是為了討論的原因,我首先試圖 用 Groovy 創建一些有用的東西,然后我再介紹把它嵌入到普通 Java 程序中的各種方法。
再制作一個音樂 Groovy
我熱愛音樂。實際上,我的 CD 收藏超過了我計算機圖書的收藏。多年以來,我把我的音樂截取到不同的計算機上,在這個過程中,我的 MP3 收藏亂到了這樣一種層度:只是表示品種豐富的音樂目錄就有一大堆。
最近,為了讓我的音樂收藏回歸有序,我采取了第一步行動。我編寫了一個快速的 Groovy 腳本,在某個目錄的 MP3 收藏上進行迭代,然后把每個文件的詳細信息(例如藝術家、專輯名稱等)提供給我。腳本如清單 3 所示:
清單 3. 一個非常有用的 Groovy 腳本
package com.vanward.groovy
import org.farng.mp3.MP3File
import groovy.util.AntBuilder
class Song {
mp3file
Song(String mp3name){
mp3file = new MP3File(mp3name)
}
getTitle(){
mp3file.getID3v1Tag().getTitle()
}
getAlbum(){
mp3file.getID3v1Tag().getAlbum()
}
getArtist(){
mp3file.getID3v1Tag().getArtist()
}
String toString(){
"Artist: " + getArtist() + " Album: " +
getAlbum() + " Song: " + getTitle()
}
static getSongsForDirectory(sdir){
println "sdir is: " + sdir
ant = new AntBuilder()
scanner = ant.fileScanner {
fileset(dir:sdir) {
include(name:"**/*.mp3")
}
}
songs = []
for(f in scanner){
songs << new Song(f.getAbsolutePath())
}
return songs
}
}
songs = Song.getSongsForDirectory(args[0])
songs.each{
println it
}
|
正如您所看到的,腳本非常簡單,對于像我這樣的人來說特別有用。而我要做的全部工作只是把某個具體的目錄名傳遞給它,然后我就會得到該目錄中每個 MP3 文件的相關信息(藝術家名稱、歌曲名稱和專輯) 。
現在讓我們來看看,如果要把這個干凈的腳本集成到一個能夠通過數據庫組織音樂甚至播放 MP3 的普通 Java 程序中,我需要做些什么。
Class 文件是類文件
正如前面討論過的,我的第一個選項可能只是用 groovyc
編譯腳本。在這個例子中,我期望 groovyc
創建 至少 兩個 .class 文件 —— 一個用于 Song
類,另一個用于 Song
聲明之后的腳本代碼。
實際上,groovyc
可能創建 5 個 .class 文件。這是與 Songs.groovy
包含三個閉包有關,兩個閉包在 getSongsForDirectory()
方法中,另一個在腳本體中,我在腳本體中對 Song
的集合進行迭代,并調用 println
。
因為 .class 文件中有三個實際上是 Song.class 和 Songs.class 的內部類,所以我只需要把注意力放在兩個 .class 文件上。Song.class 直接映射到 Groovy 腳本中的 Song
聲明,并實現了 GroovyObject
,而 Songs.class 則代表我在定義 Song
之后編寫的代碼,所以也擴展了 Script
。
此時此刻,關于如何把新編譯的 Groovy 代碼集成到 Java 代碼,我有兩個選擇:可以通過 Songs.class 文件中的 main()
方法運行代碼 (因為它擴展了 Script
),或者可以將 Song.class 包含到類路徑中,就像在 Java 代碼中使用其他對象一樣使用它。
變得更容易些
通過 java
命令調用 Songs.class 文件非常簡單,只要您記得把 Groovy 相關的依賴關系和 Groovy 腳本需要的依賴關系包含進來就可以。把 Groovy 需要的類全都包含進來的最簡單方法就是把包含全部內容的 Groovy 可嵌入 jar 文件添加到類路徑中。在我的例子中,這個文件是 groovy-all-1.0-beta-10.jar。要運行 Songs.class,需要記得包含將要用到的 MP3 庫(jid3lib-0.5.jar>),而且因為我使用 AntBuilder
,所以我還需要在類路徑中包含 Ant
。清單 4 把這些放在了一起:
清單 4. 通過 Java 命令行調用 Groovy
c:\dev\projects>java -cp ./target/classes/;c:/dev/tools/groovy/
groovy-all-1.0-beta-10.jar;C:/dev/tools/groovy/ant-1.6.2.jar;
C:/dev/projects-2.0/jid3lib-0.5.jar
com.vanward.groovy.Songs c:\dev09\music\mp3s
Artist: U2 Album: Zooropa Song: Babyface
Artist: James Taylor Album: Greatest Hits Song: Carolina in My Mind
Artist: James Taylor Album: Greatest Hits Song: Fire and Rain
Artist: U2 Album: Zooropa Song: Lemon
Artist: James Taylor Album: Greatest Hits Song: Country Road
Artist: James Taylor Album: Greatest Hits Song: Don't Let Me
Be Lonely Tonight
Artist: U2 Album: Zooropa Song: Some Days Are Better Than Others
Artist: Paul Simon Album: Graceland Song: Under African Skies
Artist: Paul Simon Album: Graceland Song: Homeless
Artist: U2 Album: Zooropa Song: Dirty Day
Artist: Paul Simon Album: Graceland Song: That Was Your Mother
|
把 Groovy 嵌入 Java 代碼
雖然命令行的解決方案簡單有趣,但它并不是所有問題的最終解決方案。如果對更高層次的完善感興趣,那么可能將 MP3 歌曲工具直接導入 Java 程序。在這個例子中,我想導入 Song.class ,并像在 Java 語言中使用其他類那樣使用它。類路徑的問題與上面相同 :我需要確保包含了 uber-Groovy jar 文件、Ant
和 jid3lib-0.5.jar 文件。在清單 5 中,可以看到如何將 Groovy MP3 工具導入簡單的 Java 類中:
清單 5. 嵌入的 Groovy 代碼
package com.vanward.gembed;
import com.vanward.groovy.Song;
import java.util.Collection;
import java.util.Iterator;
public class SongEmbedGroovy{
public static void main(String args[]) {
Collection coll = (Collection)Song.getSongsForDirectory
("C:\\music\\temp\\mp3s");
for(Iterator it = coll.iterator(); it.hasNext();){
System.out.println(it.next());
}
}
}
|
Groovy 類加載器
就在您以為自己已經掌握全部的時候,我要告訴您的是,還有更多在 Java 語言中使用 Groovy 的方法。除了通過直接編譯把 Groovy 腳本集成到 Java 程序中的這個選擇之外,當我想直接嵌入腳本時,還有其他一些選擇。
例如,我可以用 Groovy 的 GroovyClassLoader
,動態地 加載一個腳本并執行它的行為,如清單 6 所示:
清單 6. GroovyClassLoader 動態地加載并執行 Groovy 腳本
package com.vanward.gembed;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import groovy.lang.MetaMethod;
import java.io.File;
public class CLEmbedGroovy{
public static void main(String args[]) throws Throwable{
ClassLoader parent = CLEmbedGroovy.class.getClassLoader();
GroovyClassLoader loader = new GroovyClassLoader(parent);
Class groovyClass = loader.parseClass(
new File("C:\\dev\\groovy-embed\\src\\groovy\\
com\\vanward\\groovy\\Songs.groovy"));
GroovyObject groovyObject = (GroovyObject)
groovyClass.newInstance();
Object[] path = {"C:\\music\\temp\\mp3s"};
groovyObject.setProperty("args", path);
Object[] argz = {};
groovyObject.invokeMethod("run", argz);
}
}
|
Meta,寶貝 如果您屬于那群瘋狂的人中的一員,熱愛反射,喜歡利用它們能做的精彩事情,那么您將熱衷于 Groovy 的 Meta 類。就像反射一樣,使用這些類,您可以發現 GroovyObject 的各個方面(例如它的方法),這樣就可以實際地 創建 新的行為并執行它。而且,這是 Groovy 的核心 —— 想想運行腳本時它將如何發威吧! |
注意,默認情況下,類加載器將加載與腳本名稱對應的類 —— 在這個例子中是 Songs.class,而不是 Song.class>。因為我(和您)知道 Songs.class 擴展了 Groovy 的 Script 類,所以不用想也知道接下來要做的就是執行 run()
方法。
您記起,我的 Groovy 腳本也依賴于運行時參數。所以,我需要恰當地配置 args
變量,在這個例子中,我把第一個元素設置為目錄名。
更加動態的選擇
對于使用編譯好的類,而且,通過類加載器來動態加載 GroovyObject
的替代,是使用 Groovy 優美的 GroovyScriptEngine
和 GroovyShell
動態地執行 Groovy 腳本。
把 GroovyShell
對象嵌入普通 Java 類,可以像類加載器所做的那樣動態執行 Groovy 腳本。除此之外,它還提供了大量關于控制腳本運行的選項。在清單 7 中,可以看到 GroovyShell
嵌入到普通 Java 類的方式:
清單 7. 嵌入 GroovyShell
package com.vanward.gembed;
import java.io.File;
import groovy.lang.GroovyShell;
public class ShellRunEmbedGroovy{
public static void main(String args[]) throws Throwable{
String[] path = {"C:\\music\\temp\\mp3s"};
GroovyShell shell = new GroovyShell();
shell.run(new File("C:\\dev\\groovy-embed\\src\\groovy\\
com\\vanward\\groovy\\Songs.groovy"),
path);
}
}
|
可以看到,運行 Groovy 腳本非常容易。我只是創建了 GroovyShell
的實例,傳遞進腳本名稱,然后調用 run()
方法。
還可以做其他事情。如果您喜歡,那么也可以得到自己腳本的 Script
類型的 GroovyShell
實例。使用 Script
類型,您就可以傳遞進一個 Binding
對象,其中包含任何運行時值,然后再繼續調用 run()
方法,如清單 8 所示:
清單 8. 有趣的 GroovyShell
package com.vanward.gembed;
import java.io.File;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
public class ShellParseEmbedGroovy{
public static void main(String args[]) throws Throwable{
GroovyShell shell = new GroovyShell();
Script scrpt = shell.parse(
new File("C:\\dev\\groovy-embed\\src\\groovy\\
com\\vanward\\groovy\\Songs.groovy"));
Binding binding = new Binding();
Object[] path = {"C:\\music\\temp\\mp3s"};
binding.setVariable("args",path);
scrpt.setBinding(binding);
scrpt.run();
}
}
|
Groovy 的腳本引擎
GroovyScriptEngine
對象動態運行腳本的時候,非常像 GroovyShell
。區別在于:對于 GroovyScriptEngine
,您可以在實例化的時候給它提供一系列目錄,然后讓它根據要求去執行多個腳本,如清單 9 所示:
清單 9. GroovyScriptEngine 的作用
package com.vanward.gembed;
import java.io.File;
import groovy.lang.Binding;
import groovy.util.GroovyScriptEngine;
public class ScriptEngineEmbedGroovy{
public static void main(String args[]) throws Throwable{
String[] paths = {"C:\\dev\\groovy-embed\\src\\groovy\\
com\\vanward\\groovy"};
GroovyScriptEngine gse = new GroovyScriptEngine(paths);
Binding binding = new Binding();
Object[] path = {"C:\\music\\temp\\mp3s"};
binding.setVariable("args",path);
gse.run("Songs.groovy", binding);
gse.run("BusinessObjects.groovy", binding);
}
}
|
在清單 9 中,我向實例化的 GroovyScriptEngine
傳入了一個數組,數據中包含我要處理的路徑,然后創建大家熟悉的 Binding
對象,然后再執行仍然很熟悉的 Songs.groovy
腳本。只是為了好玩,我還執行了BusinessObjects.groovy
腳本,您或許還能回憶起來,它在開始這次討論的時候出現過。
Bean 腳本框架
最后,當然并不是最不重要的,是來自 Jakarta 的古老的 Bean 腳本框架( Bean Scripting Framework —— BSF)。BSF 試圖提供一個公共的 API,用來在普通 Java 應用程序中嵌入各種腳本語言(包括 Groovy)。這個標準的、但是有爭議的最小公因子方法,可以讓您毫不費力地將 Java 應用程序嵌入 Groovy 腳本。
還記得前面的 BusinessObjects
腳本嗎?在清單 10 中,可以看到 BSF 可以多么容易地讓我把這個腳本插入普通 Java 程序中:
清單 10. BSF 開始工作了
package com.vanward.gembed;
import org.apache.bsf.BSFManager;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import java.io.File;
import groovy.lang.Binding;
public class BSFEmbedGroovy{
public static void main(String args[]) throws Exception {
String fileName = "C:\\dev\\project\\src\\groovy\\
com\\vanward\\groovy\\BusinessObjects.groovy";
//this is required for bsf-2.3.0
//the "groovy" and "gy" are extensions
BSFManager.registerScriptingEngine("groovy",
"org.codehaus.groovy.bsf.GroovyEngine", new
String[] { "groovy" });
BSFManager manager = new BSFManager();
//DefaultGroovyMethods.getText just returns a
//string representation of the contents of the file
manager.exec("groovy", fileName, 0, 0,
DefaultGroovyMethods.getText(new File(fileName)));
}
}
|
結束語
如果在本文中有一件事是清楚的話,那么只件事就是 Groovy 為了 Java 代碼內部的重用提供了一堆選擇。從把 Groovy 腳本編譯成普通 Java .class 文件,到動態地加載和運行腳本,這些問題需要考慮的一些關鍵方面是靈活性和耦合。把 Groovy 腳本編譯成普通 .class 文件是 使用 您打算嵌入的功能的最簡單選擇,但是動態加載腳本可以使添加或修改腳本的行為變得更容易,同時還不必在編譯上犧牲時間。(當然,這個選項只是在接口不變的情況下才有用。)
把腳本語言嵌入普通 Java 不是每天都發生,但是機會確實不斷出現。這里提供的示例把一個簡單的目錄搜索工具嵌入到基于 Java 的應用程序中,這樣 Java 應用程序就可以很容易地變成 MP3 播放程序或者其他 MP3 播放工具。雖然我 可以 用 Java 代碼重新編寫 MP3 文件搜索器,但是我不需要這么做:Groovy 極好地兼容 Java 語言,而且,我很有興趣去擺弄所有選項!