Groovy 語言提供了幾種在運(yùn)行時(shí)與應(yīng)用(由 Java 或 Groovy 所編寫)相集成的機(jī)制,涉及到了從最基本的簡單代碼執(zhí)行,到最完整的集成緩存和編譯器自定義設(shè)置等諸多方面。
本部分內(nèi)容所有范例都是用 Groovy 編寫的,但這樣的機(jī)制也可以用于 Java 編寫的應(yīng)用程序。
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è)名為 x
與 y
的綁定參數(shù)的簡單計(jì)算。
4?? 帶有三個(gè)綁定參數(shù)(x
、y
和 z
)的簡單計(jì)算。
Eval
類方便了簡單腳本的求值計(jì)算,但并不能超出一定的范圍:由于沒有腳本緩存,這意味著不能夠計(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、Reader
、File
、InputStream
)。
4?? 延遲代碼執(zhí)行。parse
返回一個(gè) Script
實(shí)例。
5?? Script
定義了一個(gè) run
方法。
使用 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)該極為小心。傳入 GroovyShell
的 Binding
實(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。
如你所見,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。
上一部分內(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è)類。
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!
最后,我們直接依靠 org.codehaus.groovy.control.CompilationUnit
類在編譯時(shí)執(zhí)行更多的指令。該類負(fù)責(zé)確定編譯的各種步驟,可以讓我們引入更多新的步驟,或者甚至停止各種編譯階段。比如說在聯(lián)合編譯器中如何生成存根。
但是,不建議重寫 CompilationUnit
,如果沒有其他的辦法時(shí)才應(yīng)該這樣做。
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),或者想與你所使用的腳本語言保持一種非常松散的耦合。
假設(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());
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);
上面的范例使用了 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());
雖然不是很常用,但 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);
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)致使用硬編碼引用。