嵌入簡單的、易于編寫的腳本,從而利用 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 中,我聲明了三個類 —— PersonAddressContactNumber。之后的代碼根據(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 文件!我們了解 PersonAddressContactNumber 文件的意義,但是其他兩個文件有什么作用呢?

研究發(fā)現(xiàn),Person$_toString_closure1.classPerson 類的 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)美的 GroovyScriptEngineGroovyShell 動態(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)!