鍍金池/ 教程/ Java/ Groovy 與應(yīng)用的集成
Grape 依賴管理器
與 Java 的區(qū)別
語法風(fēng)格指南
Groovy 開發(fā)工具包
領(lǐng)域?qū)S谜Z言
安全更新
Groovy 與應(yīng)用的集成
運(yùn)行時(shí)及編譯時(shí)元編程(end)
測試指南
安裝 Groovy
設(shè)計(jì)模式
Groovy 的下載

Groovy 與應(yīng)用的集成

1. Groovy 集成機(jī)制

Groovy 語言提供了幾種在運(yùn)行時(shí)與應(yīng)用(由 Java 或 Groovy 所編寫)相集成的機(jī)制,涉及到了從最基本的簡單代碼執(zhí)行,到最完整的集成緩存和編譯器自定義設(shè)置等諸多方面。

本部分內(nèi)容所有范例都是用 Groovy 編寫的,但這樣的機(jī)制也可以用于 Java 編寫的應(yīng)用程序。

1.1 Eval

groovy.util.Eval 類是最簡單的用來在運(yùn)行時(shí)動(dòng)態(tài)執(zhí)行 Groovy 代碼的類,調(diào)用 me 方法即可。

import groovy.util.Eval

assert Eval.me('33*3') == 99
assert Eval.me('"foo".toUpperCase()') == 'FOO'

Eval 能夠利用多種接受參數(shù)的變體形式來進(jìn)行簡單計(jì)算。

assert Eval.x(4, '2*x') == 8                1??
assert Eval.me('k', 4, '2*k') == 8          2??
assert Eval.xy(4, 5, 'x*y') == 20           3??
assert Eval.xyz(4, 5, 6, 'x*y+z') == 26     4??  

1?? 帶有一個(gè)名為 x 的綁定參數(shù)的簡單計(jì)算。
2?? 帶有一個(gè)名為 k 的自定義綁定參數(shù)的簡單計(jì)算。
3?? 帶有兩個(gè)名為 xy 的綁定參數(shù)的簡單計(jì)算。
4?? 帶有三個(gè)綁定參數(shù)(x、yz)的簡單計(jì)算。

Eval 類方便了簡單腳本的求值計(jì)算,但并不能超出一定的范圍:由于沒有腳本緩存,這意味著不能夠計(jì)算幾行代碼。

1.2 GroovyShell

1.2.1 多數(shù)據(jù)源

groovy.lang.GroovyShell 類是建議采用的腳本計(jì)算方式,因?yàn)樗哂芯彺娼Y(jié)果腳本實(shí)例的能力。雖然 Eval 類能夠返回編譯腳本的執(zhí)行結(jié)果,但 GroovyShell 類卻能提供更多選項(xiàng)。

def shell = new GroovyShell()                  1??         
def result = shell.evaluate '3*5'                2??       
def result2 = shell.evaluate(new StringReader('3*5'))   3??
assert result == result2
def script = shell.parse '3*5'                          4??
assert script instanceof groovy.lang.Script
assert script.run() == 15                                 5??  

1?? 創(chuàng)建一個(gè)新的 GroovyShell 實(shí)例。
2?? 直接執(zhí)行代碼,可被當(dāng)作 Eval 來使用。
3?? 可從多種數(shù)據(jù)源讀?。?code>String、ReaderFile、InputStream)。
4?? 延遲代碼執(zhí)行。parse 返回一個(gè) Script 實(shí)例。
5?? Script 定義了一個(gè) run 方法。

1.2.2 在腳本與程序間共享數(shù)據(jù)

使用 groovy.lang.Binding 可以在程序及腳本間共享數(shù)據(jù):

def sharedData = new Binding()                             1??                         
def shell = new GroovyShell(sharedData)                    2??
def now = new Date()
sharedData.setProperty('text', 'I am shared data!')        3??
sharedData.setProperty('date', now)                        4??

String result = shell.evaluate('"At $date, $text"')        5??  

assert result == "At $now, I am shared data!"

1?? 創(chuàng)建一個(gè)包含共享數(shù)據(jù)的 Binding 對象。
2?? 創(chuàng)建一個(gè)使用共享數(shù)據(jù)的 GroovyShell 對象。
3?? 為綁定對象添加一個(gè)字符串。
4?? 為綁定對象添加一個(gè)日期(并不局限于簡單類型)。
5?? 進(jìn)行腳本計(jì)算。

注意,也可以從腳本寫入綁定對象。

def sharedData = new Binding()                          1??
def shell = new GroovyShell(sharedData)                 2??

shell.evaluate('foo=123')                               3??

assert sharedData.getProperty('foo') == 123             4??

1?? 創(chuàng)建一個(gè)新的 Binding 對象。
2?? 創(chuàng)建使用該共享數(shù)據(jù)的 GroovyShell 對象。
3?? 使用未聲明變量將結(jié)果存儲(chǔ)到綁定對象中。
4?? 從調(diào)用中讀取結(jié)果。

這里重要的一點(diǎn)是,如果想寫入綁定對象,必須要使用未聲明變量。使用 def 或像下例中那樣使用 explicit 類型都是錯(cuò)誤的,會(huì)引起失敗,因?yàn)檫@樣做的結(jié)果等于創(chuàng)建了本地變量

def sharedData = new Binding()
def shell = new GroovyShell(sharedData)

shell.evaluate('int foo=123')

try {
    assert sharedData.getProperty('foo')
} catch (MissingPropertyException e) {
    println "foo is defined as a local variable"
}

在多線程環(huán)境中使用共享數(shù)據(jù)應(yīng)該極為小心。傳入 GroovyShellBinding 實(shí)例并不具有線程安全性,會(huì)被所有腳本所共享。

利用被 parse 返回的 Script 實(shí)例可以解決 Binding 共享實(shí)例的問題:

def shell = new GroovyShell()

def b1 = new Binding(x:3)                  1??         
def b2 = new Binding(x:4)                  2??     
def script = shell.parse('x = 2*x')
script.binding = b1
script.run()
script.binding = b2
script.run()
assert b1.getProperty('x') == 6
assert b2.getProperty('x') == 8
assert b1 != b2

1?? 在 b1 中存儲(chǔ) x 變量。 2?? 在 b2 中存儲(chǔ) x 變量。

但是,必須注意,此時(shí)仍舊共享的是腳本的同一個(gè)實(shí)例。因此,如果兩個(gè)線程都要利用同一腳本,就不能采用這種方法,這時(shí)必須創(chuàng)建兩個(gè)獨(dú)立的腳本實(shí)例。

def shell = new GroovyShell()

def b1 = new Binding(x:3)
def b2 = new Binding(x:4)
def script1 = shell.parse('x = 2*x')             1??     
def script2 = shell.parse('x = 2*x')             2??
assert script1 != script2
script1.binding = b1                             3??
script2.binding = b2                             4??
def t1 = Thread.start { script1.run() }          5??
def t2 = Thread.start { script2.run() }          6??
[t1,t2]*.join()                                  7??
assert b1.getProperty('x') == 6
assert b2.getProperty('x') == 8
assert b1 != b2

1?? 為線程 1 創(chuàng)建一個(gè)腳本實(shí)例。
2?? 為線程 2 創(chuàng)建一個(gè)腳本實(shí)例。
3?? 將第 1 個(gè)綁定對象賦予腳本 1。
4?? 將第 2 個(gè)綁定對象賦予腳本 1。
5?? 在單獨(dú)的一個(gè)線程中運(yùn)行腳本 1。
6?? 在單獨(dú)的一個(gè)線程中運(yùn)行腳本 2。
7?? 等待結(jié)束。

在需要線程安全的場合(比如該例),建議最好直接使用 GroovyClassLoader。

1.2.3 自定義腳本類

如你所見,parse 方法返回了一個(gè) groovy.lang.Script 實(shí)例,但完全可以使用自定義類,只需它擴(kuò)展 Script 即可??梢杂盟鼇頌槟_本(如下例)提供額外的行為:

abstract class MyScript extends Script {
    String name

    String greet() {
        "Hello, $name!"
    }
}

自定義類定義了一個(gè)叫 name 的屬性,以及一個(gè)叫 greet 的新方法。通過使用自定義配置,該類可用作腳本基類。

import org.codehaus.groovy.control.CompilerConfiguration

def config = new CompilerConfiguration()                                   1??                              
config.scriptBaseClass = 'MyScript'                                        2?? 

def shell = new GroovyShell(this.class.classLoader, new Binding(), config)  3??
def script = shell.parse('greet()')                                         4??
assert script instanceof MyScript
script.setName('Michel')
assert script.run() == 'Hello, Michel!'

1?? 創(chuàng)建一個(gè) CompilerConfiguration 實(shí)例。
2?? 讓它使用 MyScript 作為腳本基類。
3?? 然后在創(chuàng)建 shell 時(shí),使用編譯器配置。
4?? 腳本現(xiàn)在可以訪問新方法 greet。

并不局限于只使用 scriptBaseClass 配置??梢允褂萌魏尉幾g器配置微調(diào)選項(xiàng),包括 compilation customizers

1.3 GroovyClassLoader

上一部分內(nèi)容介紹了 GroovyShell,它是一種執(zhí)行腳本的便利工具,但除了腳本之外,編譯其他的內(nèi)容就復(fù)雜多了。它內(nèi)部使用了 groovy.lang.GroovyClassLoader,這是運(yùn)行時(shí)編譯以及執(zhí)行類加載的核心。

通過利用 GroovyClassLoader,而不是 GroovyShell,可以加載類,而不是腳本實(shí)例:

import groovy.lang.GroovyClassLoader

def gcl = new GroovyClassLoader()                                             1??                            
def clazz = gcl.parseClass('class Foo { void doIt() { println "ok" } }')      2??    
assert clazz.name == 'Foo'                                                    3??
def o = clazz.newInstance()                                                   4?? 
o.doIt()                                                                      5??

1?? 創(chuàng)建一個(gè)新的 GroovyClassLoader。
2?? parseClass 能返回一個(gè) Class 的實(shí)例。
3?? 可以看到,返回的類真的是腳本中定義的那一個(gè)。
4?? 你可以創(chuàng)建該類(并不是腳本)的一個(gè)新實(shí)例。
5?? 然后調(diào)用任何其上的方法。

GroovyClassLoader 持有一個(gè)它所創(chuàng)建的所有類的引用,因此很容易造成內(nèi)存泄露,尤其當(dāng)你兩次執(zhí)行同一腳本時(shí),比如一個(gè)字符串,那么你將獲得兩個(gè)不同的類:

import groovy.lang.GroovyClassLoader

def gcl = new GroovyClassLoader()
def clazz1 = gcl.parseClass('class Foo { }')                   1??
def clazz2 = gcl.parseClass('class Foo { }')                   2??           
assert clazz1.name == 'Foo'                                    3??             
assert clazz2.name == 'Foo'
assert clazz1 != clazz2                                        4??                

1?? 動(dòng)態(tài)創(chuàng)建一個(gè)名為 Foo 的類。
2?? 創(chuàng)建一個(gè)看起來一樣的類,使用一個(gè)單獨(dú)的 parseClass 調(diào)用。
3?? 確保兩個(gè)類擁有同一名稱。
4?? 但它們其實(shí)是不同的。

原因在于,GroovyClassLoader 并不跟蹤源文本。如果想要同一實(shí)例,源必須是一個(gè)文件,比如下例:

def gcl = new GroovyClassLoader()
def clazz1 = gcl.parseClass(file)                                   1??                         
def clazz2 = gcl.parseClass(new File(file.absolutePath))            2??        
assert clazz1.name == 'Foo'                                         3??        
assert clazz2.name == 'Foo'
assert clazz1 == clazz2                                             4??                                                    

1?? 從 File 中解析類。
2?? 從不同的一個(gè)文件實(shí)例中解析一個(gè)類,但指向同一實(shí)際文件。
3?? 確保類的名字相同。
4?? 但現(xiàn)在它們就是同一個(gè)實(shí)例了。

File 作為輸入,GroovyClassLoader 能夠捕獲生成的類文件,從而避免在運(yùn)行時(shí)對同一數(shù)據(jù)源創(chuàng)建多個(gè)類。

1.4 GroovyScriptEngine

groovy.util.GroovyScriptEngine 類能夠?yàn)槟切┮蕾嚹_本重載及依賴的應(yīng)用程序提供一種靈活的基礎(chǔ)。盡管 GroovyShell 聚焦單獨(dú)的腳本,GroovyClassLoader 能夠處理任何 Groovy 類的動(dòng)態(tài)編譯與加載,然而 GroovyScriptEngine 能夠?yàn)?GroovyClassLoader 其上再增添一個(gè)能夠處理腳本依賴及重新加載的功能層。

為了說明這一點(diǎn),下面來創(chuàng)建腳本引擎,用無限循環(huán)來執(zhí)行腳本。首先需要?jiǎng)?chuàng)建一個(gè)目錄,將下列腳本(ReloadingTest.groovy)放入其中。

ReloadingTest.groovy

class Greeter {
    String sayHello() {
        def greet = "Hello, world!"
        greet
    }
}

new Greeter()

然后使用 GroovyScriptEngine 來執(zhí)行代碼:

def binding = new Binding()
def engine = new GroovyScriptEngine([tmpDir.toURI().toURL()] as URL[])               1??        

while (true) {
    def greeter = engine.run('ReloadingTest.groovy', binding)                        2??
    println greeter.sayHello()                                                       3??
    Thread.sleep(1000)
}   

1?? 創(chuàng)建一個(gè)腳本引擎,在源目錄中尋找數(shù)據(jù)源。
2?? 執(zhí)行腳本,返回 Greeter 實(shí)例。
3?? 打印問候信息。

然后,你就會(huì)發(fā)現(xiàn)每秒都會(huì)輸出問候信息,如下所示:

Hello, world!
Hello, world!
...   

不用打斷腳本執(zhí)行過程,現(xiàn)在用下面的內(nèi)容來替代 ReloadingTest 文件:

ReloadingTest.groovy

class Greeter {
    String sayHello() {
        def greet = "Hello, Groovy!"
        greet
    }
}

new Greeter()

于是,輸出信息就變?yōu)椋?

Hello, world!
...
Hello, Groovy!
Hello, Groovy!
...  

但它還可能會(huì)依賴其他腳本。接下來在同一目錄中創(chuàng)建下面這個(gè)文件,同樣不用干擾上述腳本執(zhí)行:

Depencency.groovy

class Dependency {
    String message = 'Hello, dependency 1'
}

然后像下面這樣來更新 ReloadingTest 腳本:

ReloadingTest.groovy

import Dependency

class Greeter {
    String sayHello() {
        def greet = new Dependency().message
        greet
    }
}

new Greeter()

這時(shí),輸出消息應(yīng)變?yōu)椋?

Hello, Groovy!
...
Hello, dependency 1!
Hello, dependency 1!
...

作為最后一項(xiàng)測試,下面我們在不改動(dòng) ReloadingTest 文件的前提下,更新 Dependency.groovy 文件。

Depencency.groovy

class Dependency {
    String message = 'Hello, dependency 2'
}  

可以看到重新加載了依賴文件:

Hello, dependency 1!
...
Hello, dependency 2!
Hello, dependency 2!  

1.5 CompilationUnit

最后,我們直接依靠 org.codehaus.groovy.control.CompilationUnit 類在編譯時(shí)執(zhí)行更多的指令。該類負(fù)責(zé)確定編譯的各種步驟,可以讓我們引入更多新的步驟,或者甚至停止各種編譯階段。比如說在聯(lián)合編譯器中如何生成存根。

但是,不建議重寫 CompilationUnit,如果沒有其他的辦法時(shí)才應(yīng)該這樣做。

2. Bean 腳本框架

Bean 腳本框架(BSF,Bean Scripting Framework) 試圖通過一個(gè) API 來調(diào)用 Java 中的腳本語言。它已經(jīng)很長時(shí)間沒有更新過了,但由于支持標(biāo)準(zhǔn)的 JSR-223 API,所以還沒有被遺棄。

BSF 引擎由 org.codehaus.groovy.bsf.GroovyEngine 類所實(shí)現(xiàn)。但這一事實(shí)常常被 BSF 的 API 所掩蓋。通過 BSF API,只會(huì)把 Groovy 看成其他同樣的腳本語言。

由于 Groovy 對 Java 集成有著原生的支持,所以你只需要注意下面兩種情形中的 BSF 應(yīng)用:還想調(diào)用其他語言時(shí)(如 JRuby),或者想與你所使用的腳本語言保持一種非常松散的耦合。

2.1 入門

假設(shè)在類路徑中有 Groovy 和 BSF 的 jar 文件,那么可以使用下列代碼運(yùn)行簡單的 Groovy 腳本。

String myScript = "println('Hello World')\n  return [1, 2, 3]";
BSFManager manager = new BSFManager();
List answer = (List) manager.eval("groovy", "myScript.groovy", 0, 0, myScript);
assertEquals(3, answer.size());

2.2 傳入變量

BSF 可以使你在 Java 和腳本語言間傳入 bean。可以注冊/不注冊 bean,使其被 BSF 所知曉,然后利用 BSF 方法在需要時(shí)查找 bean。另外,還可以聲明/不聲明 bean。這將注冊它們,但也使其能夠直接用于腳本語言。第二種方法是 Groovy 通常所習(xí)慣采用的方法,如下所示:

BSFManager manager = new BSFManager();
manager.declareBean("xyz", 4, Integer.class);
Object answer = manager.eval("groovy", "test.groovy", 0, 0, "xyz + 1");
assertEquals(5, answer);

2.3 其他調(diào)用選項(xiàng)

上面的范例使用了 eval 方法。BSF 提供了多種方法(詳情參見 BSF 文檔),其中可用的另一種方法是 apply。它能讓你用腳本語言定義一種匿名函數(shù),然后把該函數(shù)應(yīng)用到參數(shù)中。Groovy 利用閉包支持這種函數(shù),如下所示:

BSFManager manager = new BSFManager();
Vector<String> ignoreParamNames = null;
Vector<Integer> args = new Vector<Integer>();
args.add(2);
args.add(5);
args.add(1);
Integer actual = (Integer) manager.apply("groovy", "applyTest", 0, 0,
        "def summer = { a, b, c -> a * 100 + b * 10 + c }", ignoreParamNames, args);
assertEquals(251, actual.intValue());

2.4 訪問腳本引擎

雖然不是很常用,但 BSF 還是提供了一種鉤子,以便直接訪問腳本引擎。引擎所執(zhí)行的一個(gè)函數(shù)是在對象上調(diào)用一個(gè)方法。如下所示:

BSFManager manager = new BSFManager();
BSFEngine bsfEngine = manager.loadScriptingEngine("groovy");
manager.declareBean("myvar", "hello", String.class);
Object myvar = manager.lookupBean("myvar");
String result = (String) bsfEngine.call(myvar, "reverse", new Object[0]);
assertEquals("olleh", result);

3 JSR 223 javax.script API

JSR-223 是 Java 中標(biāo)準(zhǔn)的腳本框架調(diào)用 API。從 Java 6 開始引入進(jìn)來,主要目用來提供一種常用框架,以便從 Java 中調(diào)用多種語言。由于 Groovy 自身已經(jīng)提供了更豐富的集成機(jī)制,所以如果不想在同一應(yīng)用中使用多種語言,那么建議使用 Groovy 的集成機(jī)制,而不是功能受限的 JSR-223 API。

下面展示的是如何初始化 JSR-223 引擎,從而在 Java 中與 Groovy 建立聯(lián)系:

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");

然后執(zhí)行起 Groovy 腳本就方便多了:

Integer sum = (Integer) engine.eval("(1..10).sum()");
assertEquals(new Integer(55), sum);

也可以共享變量:

engine.put("first", "HELLO");
engine.put("second", "world");
String result = (String) engine.eval("first.toLowerCase() + ' ' + second.toUpperCase()");
assertEquals("hello WORLD", result);

下例展示了如何調(diào)用一個(gè)可調(diào)用的方法:

import javax.script.Invocable;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
String fact = "def factorial(n) { n == 1 ? 1 : n * factorial(n - 1) }";
engine.eval(fact);
Invocable inv = (Invocable) engine;
Object[] params = {5};
Object result = inv.invokeFunction("factorial", params);
assertEquals(new Integer(120), result);

引擎持有腳本函數(shù)的每一個(gè)默認(rèn)的硬編碼引用。要想改變這一點(diǎn),可以為名為 ##jsr223.groovy.engine.keep.globals 的腳本上下文設(shè)置引擎級別的范圍屬性:用 phantom 字符串使用虛引用,用 weak 來使用弱引用,用 soft 來使用軟引用,忽略大小寫問題。任何其他的字符串都會(huì)導(dǎo)致使用硬編碼引用。