嵌入簡單的、易于編寫的腳本,從而利用 Groovy 的簡單性
Andrew Glover
CTO, Vanward Technologies
2005 年 6 月 13 日
您有沒有想過在自己相對復(fù)雜的 Java 程序中嵌入 Groovy 簡單的、易于編寫的腳本呢?在這一期 實(shí)戰(zhàn) Groovy 系列文章中,Andrew Glover 將介紹把 Groovy 集成到 Java 代碼中的多種方法,并解釋在什么地方、什么時(shí)候適合這么做。
如果您一直在閱讀這個系列,那么您應(yīng)該已經(jīng)看到有各種各樣使用 Groovy 的有趣方式,Groovy 的主要優(yōu)勢之一就是它的生產(chǎn)力。Groovy 代碼通常要比 Java 代碼更容易編寫,而且編寫起來也更快,這使得它有足夠的資格成為開發(fā)工作包中的一個附件。在另一方面,正如我在這個系列中反復(fù)強(qiáng)調(diào)的那樣,Groovy 并不是 —— 而且也不打算成為 —— Java 語言的替代。所以,這里存在的問題是,能否把 Groovy 集成到 Java 的編程實(shí)踐中?或者說這樣做有什么用?什么時(shí)候 這樣做有用?
這個月,我將嘗試回答這個問題。我將從一些熟悉的事物開始,即從如何將 Groovy 腳本編譯成與 Java 兼容的類文件開始,然后進(jìn)一步仔細(xì)研究 Groovy 的編譯工具(groovyc
)是如何讓這個奇跡實(shí)現(xiàn)的。了解 Groovy 在幕后做了什么是在 Java 代碼中使用 Groovy 的第一步。
注意,本月示例中演示的一些編程技術(shù)是 Groovlets
框架和 Groovy 的 GroovyTestCase
的核心,這些技術(shù)我在前面的文章中已經(jīng)討論過。
關(guān)于本系列 把任何工具集成到自己的開發(fā)實(shí)踐的關(guān)鍵就是知道什么時(shí)候使用它,而什么時(shí)候應(yīng)當(dāng)把它留在箱子里。腳本語言能夠成為工具箱中極為強(qiáng)大的附件,但只在將它恰當(dāng)應(yīng)用到合適場景時(shí)才這樣。為此,實(shí)戰(zhàn) Groovy 的一系列文章專門探索了 Groovy 的實(shí)際應(yīng)用,并告訴您什么時(shí)候應(yīng)用它們,以及如何成功地應(yīng)用它們。 |
天作之合?
在本系列中以前的文章中,當(dāng)我介紹如何 用 Groovy 測試普通 Java 程序 的時(shí)候,您可能已經(jīng)注意到一些奇怪的事:我 編譯了 那些 Groovy 腳本。實(shí)際上,我將 groovy 單元測試編譯成普通的 Java .class 文件,然后把它們作為 Maven 構(gòu)建的一部分來運(yùn)行。
這種編譯是通過調(diào)用 groovyc
命令進(jìn)行的,該命令將 Groovy 腳本編譯成普通的 Java 兼容的 .class 文件。例如,如果腳本聲明了一個類,那么調(diào)用 groovyc
會生成至少三個 .class 。文件本身會遵守標(biāo)準(zhǔn)的 Java 規(guī)則:.class 文件名稱要和聲明的類名匹配。
作為示例,請參見清單 1,它創(chuàng)建了一個簡單的腳本,腳本聲明了幾個類。然后,您自己就可以看出 groovyc
命令生成的結(jié)果:
清單 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
。之后的代碼根據(jù)這些新定義的類型創(chuàng)建對象,然后調(diào)用 toString()
方法。迄今為止,Groovy 中的代碼還非常簡單,但現(xiàn)在來看一下清單 2 中 groovyc
產(chǎn)生什么樣的結(jié)果:
清單 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
文件的意義,但是其他兩個文件有什么作用呢?
研究發(fā)現(xiàn),Person$_toString_closure1.class
是 Person
類的 toString()
方法中發(fā)現(xiàn)的閉包的結(jié)果。它是 Person
的一個內(nèi)部類,但是 BusinessObjects.class
文件是怎么回事 —— 它可能是什么呢?
對 清單 1 的深入觀察指出:我在腳本主體中編寫的代碼(聲明完三個類之后的代碼)變成一個 .class 文件,它的名稱采用的是腳本名稱。在這個例子中,腳本被命名為 BusinessObjects.groovy
,所以,類定義中沒有包含的代碼被編譯到一個名為 BusinessObjects
的 .class 文件。
反編譯
反編譯這些類可能會非常有趣。由于 Groovy 處于代碼頂層,所以生成的 .java 文件可能相當(dāng)巨大;不過,您應(yīng)當(dāng)注意的是 Groovy 腳本中聲明的類(如 Person
) 與類之外的代碼(比如 BusinessObjects.class
中找到的代碼)之間的區(qū)別。在 Groovy 文件中定義的類完成了 GroovyObject
的實(shí)現(xiàn),而在類之外定義的代碼則被綁定到一個擴(kuò)展自 Script
的類。
例如,如果研究由 BusinessObjects.class 生成的 .java 文件,可以發(fā)現(xiàn):它定義了一個 main()
方法和一個 run()
方法。不用驚訝, run()
方法包含我編寫的、用來創(chuàng)建這些對象的新實(shí)例的代碼,而 main()
方法則調(diào)用 run()
方法。
這個細(xì)節(jié)的全部要點(diǎn)再一次回到了:對 Groovy 的理解越好,就越容易把它集成到 Java 程序中。有人也許會問:“為什么我要這么做呢?”好了,我們想說您用 Groovy 開發(fā)了一些很酷的東西;那么如果能把這些東西集成到 Java 程序中,那不是很好嗎?
只是為了討論的原因,我首先試圖 用 Groovy 創(chuàng)建一些有用的東西,然后我再介紹把它嵌入到普通 Java 程序中的各種方法。
再制作一個音樂 Groovy
我熱愛音樂。實(shí)際上,我的 CD 收藏超過了我計(jì)算機(jī)圖書的收藏。多年以來,我把我的音樂截取到不同的計(jì)算機(jī)上,在這個過程中,我的 MP3 收藏亂到了這樣一種層度:只是表示品種豐富的音樂目錄就有一大堆。
最近,為了讓我的音樂收藏回歸有序,我采取了第一步行動。我編寫了一個快速的 Groovy 腳本,在某個目錄的 MP3 收藏上進(jìn)行迭代,然后把每個文件的詳細(xì)信息(例如藝術(shù)家、專輯名稱等)提供給我。腳本如清單 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 文件的相關(guān)信息(藝術(shù)家名稱、歌曲名稱和專輯) 。
現(xiàn)在讓我們來看看,如果要把這個干凈的腳本集成到一個能夠通過數(shù)據(jù)庫組織音樂甚至播放 MP3 的普通 Java 程序中,我需要做些什么。
Class 文件是類文件
正如前面討論過的,我的第一個選項(xiàng)可能只是用 groovyc
編譯腳本。在這個例子中,我期望 groovyc
創(chuàng)建 至少 兩個 .class 文件 —— 一個用于 Song
類,另一個用于 Song
聲明之后的腳本代碼。
實(shí)際上,groovyc
可能創(chuàng)建 5 個 .class 文件。這是與 Songs.groovy
包含三個閉包有關(guān),兩個閉包在 getSongsForDirectory()
方法中,另一個在腳本體中,我在腳本體中對 Song
的集合進(jìn)行迭代,并調(diào)用 println
。
因?yàn)?.class 文件中有三個實(shí)際上是 Song.class 和 Songs.class 的內(nèi)部類,所以我只需要把注意力放在兩個 .class 文件上。Song.class 直接映射到 Groovy 腳本中的 Song
聲明,并實(shí)現(xiàn)了 GroovyObject
,而 Songs.class 則代表我在定義 Song
之后編寫的代碼,所以也擴(kuò)展了 Script
。
此時(shí)此刻,關(guān)于如何把新編譯的 Groovy 代碼集成到 Java 代碼,我有兩個選擇:可以通過 Songs.class 文件中的 main()
方法運(yùn)行代碼 (因?yàn)樗鼣U(kuò)展了 Script
),或者可以將 Song.class 包含到類路徑中,就像在 Java 代碼中使用其他對象一樣使用它。
變得更容易些
通過 java
命令調(diào)用 Songs.class 文件非常簡單,只要您記得把 Groovy 相關(guān)的依賴關(guān)系和 Groovy 腳本需要的依賴關(guān)系包含進(jìn)來就可以。把 Groovy 需要的類全都包含進(jìn)來的最簡單方法就是把包含全部內(nèi)容的 Groovy 可嵌入 jar 文件添加到類路徑中。在我的例子中,這個文件是 groovy-all-1.0-beta-10.jar。要運(yùn)行 Songs.class,需要記得包含將要用到的 MP3 庫(jid3lib-0.5.jar>),而且因?yàn)槲沂褂?AntBuilder
,所以我還需要在類路徑中包含 Ant
。清單 4 把這些放在了一起:
清單 4. 通過 Java 命令行調(diào)用 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 歌曲工具直接導(dǎo)入 Java 程序。在這個例子中,我想導(dǎo)入 Song.class ,并像在 Java 語言中使用其他類那樣使用它。類路徑的問題與上面相同 :我需要確保包含了 uber-Groovy jar 文件、Ant
和 jid3lib-0.5.jar 文件。在清單 5 中,可以看到如何將 Groovy MP3 工具導(dǎo)入簡單的 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 類加載器
就在您以為自己已經(jīng)掌握全部的時(shí)候,我要告訴您的是,還有更多在 Java 語言中使用 Groovy 的方法。除了通過直接編譯把 Groovy 腳本集成到 Java 程序中的這個選擇之外,當(dāng)我想直接嵌入腳本時(shí),還有其他一些選擇。
例如,我可以用 Groovy 的 GroovyClassLoader
,動態(tài)地 加載一個腳本并執(zhí)行它的行為,如清單 6 所示:
清單 6. GroovyClassLoader 動態(tài)地加載并執(zhí)行 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 類。就像反射一樣,使用這些類,您可以發(fā)現(xiàn) GroovyObject 的各個方面(例如它的方法),這樣就可以實(shí)際地 創(chuàng)建 新的行為并執(zhí)行它。而且,這是 Groovy 的核心 —— 想想運(yùn)行腳本時(shí)它將如何發(fā)威吧! |
注意,默認(rèn)情況下,類加載器將加載與腳本名稱對應(yīng)的類 —— 在這個例子中是 Songs.class,而不是 Song.class>。因?yàn)槲遥ê湍┲?Songs.class 擴(kuò)展了 Groovy 的 Script 類,所以不用想也知道接下來要做的就是執(zhí)行 run()
方法。
您記起,我的 Groovy 腳本也依賴于運(yùn)行時(shí)參數(shù)。所以,我需要恰當(dāng)?shù)嘏渲?args
變量,在這個例子中,我把第一個元素設(shè)置為目錄名。
更加動態(tài)的選擇
對于使用編譯好的類,而且,通過類加載器來動態(tài)加載 GroovyObject
的替代,是使用 Groovy 優(yōu)美的 GroovyScriptEngine
和 GroovyShell
動態(tài)地執(zhí)行 Groovy 腳本。
把 GroovyShell
對象嵌入普通 Java 類,可以像類加載器所做的那樣動態(tài)執(zhí)行 Groovy 腳本。除此之外,它還提供了大量關(guān)于控制腳本運(yùn)行的選項(xiàng)。在清單 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);
}
}
|
可以看到,運(yùn)行 Groovy 腳本非常容易。我只是創(chuàng)建了 GroovyShell
的實(shí)例,傳遞進(jìn)腳本名稱,然后調(diào)用 run()
方法。
還可以做其他事情。如果您喜歡,那么也可以得到自己腳本的 Script
類型的 GroovyShell
實(shí)例。使用 Script
類型,您就可以傳遞進(jìn)一個 Binding
對象,其中包含任何運(yùn)行時(shí)值,然后再繼續(xù)調(diào)用 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
對象動態(tài)運(yùn)行腳本的時(shí)候,非常像 GroovyShell
。區(qū)別在于:對于 GroovyScriptEngine
,您可以在實(shí)例化的時(shí)候給它提供一系列目錄,然后讓它根據(jù)要求去執(zhí)行多個腳本,如清單 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 中,我向?qū)嵗?GroovyScriptEngine
傳入了一個數(shù)組,數(shù)據(jù)中包含我要處理的路徑,然后創(chuàng)建大家熟悉的 Binding
對象,然后再執(zhí)行仍然很熟悉的 Songs.groovy
腳本。只是為了好玩,我還執(zhí)行了BusinessObjects.groovy
腳本,您或許還能回憶起來,它在開始這次討論的時(shí)候出現(xiàn)過。
Bean 腳本框架
最后,當(dāng)然并不是最不重要的,是來自 Jakarta 的古老的 Bean 腳本框架( Bean Scripting Framework —— BSF)。BSF 試圖提供一個公共的 API,用來在普通 Java 應(yīng)用程序中嵌入各種腳本語言(包括 Groovy)。這個標(biāo)準(zhǔn)的、但是有爭議的最小公因子方法,可以讓您毫不費(fèi)力地將 Java 應(yīng)用程序嵌入 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)));
}
}
|
結(jié)束語
如果在本文中有一件事是清楚的話,那么只件事就是 Groovy 為了 Java 代碼內(nèi)部的重用提供了一堆選擇。從把 Groovy 腳本編譯成普通 Java .class 文件,到動態(tài)地加載和運(yùn)行腳本,這些問題需要考慮的一些關(guān)鍵方面是靈活性和耦合。把 Groovy 腳本編譯成普通 .class 文件是 使用 您打算嵌入的功能的最簡單選擇,但是動態(tài)加載腳本可以使添加或修改腳本的行為變得更容易,同時(shí)還不必在編譯上犧牲時(shí)間。(當(dāng)然,這個選項(xiàng)只是在接口不變的情況下才有用。)
把腳本語言嵌入普通 Java 不是每天都發(fā)生,但是機(jī)會確實(shí)不斷出現(xiàn)。這里提供的示例把一個簡單的目錄搜索工具嵌入到基于 Java 的應(yīng)用程序中,這樣 Java 應(yīng)用程序就可以很容易地變成 MP3 播放程序或者其他 MP3 播放工具。雖然我 可以 用 Java 代碼重新編寫 MP3 文件搜索器,但是我不需要這么做:Groovy 極好地兼容 Java 語言,而且,我很有興趣去擺弄所有選項(xiàng)!