8. 应用集成 Groovy

8.1. Groovy 集成机制

Groovy 语言提供了几种在运行时集成至应用程序中的方法,包括从最基础的代码执行,到集成缓存及自定义编译器。 本章节的实例使用 Groovy 编写,其同样集成机制也适用于 Java 。

8.1.1. Eval

groovy.util.Eval 类在运行时,动态执行 Groovy 最简单的方式。可以直接通过方法 me 实现调用:

import groovy.util.Eval

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

Eval 支持接受参数的多变种执行:

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> 绑定参数命名为 x

<2> 自定义绑定参数命名为 k

<3> 两个绑定参数分别命名为 x 和 y

<4> 三个绑定参数分别命名为 x , y 和 z

Eval 可以执行简单脚本,对于复杂的脚本将无能为力:由于其无法缓存脚本,但并不意味着只能执行单行脚本。

8.1.2. GroovyShell

8.1.2.1. Multiple sources

groovy.lang.GroovyShell 适用于需要缓存脚本实例的执行过程。 Eval 类可以返回脚本执行结果, GroovyShell 类可以提供更多选择。

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> 创建一个 GroovyShell 实例

<2> 与 Eval 一样可以直接执行代码

<3> 可以从多个数据源读取数据 (String, Reader, File, InputStream)

<4> 可以延缓脚本执行。parse 返回 Script 实例

<5> Script 定义了一个 run 方法

8.1.2.2. Sharing data between a script and the application

使用 groovy.lang.Binding 可以在应用程序与脚本之间共享数据:

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> 创建 Binding 对象,用于存储共享数据

<2> 创建 GroovyShell 对象,使用共享数据

<3> 添加字符串数据共享绑定

<4> 添加一个 date 类型数据绑定(共享数据不局限于基本数据类型)

<5> 执行脚本

注解

脚本中也将数据写入共享数据对象。

def sharedData = new Binding()                          //<1>
def shell = new GroovyShell(sharedData)         //<2>

shell.evaluate('foo=123')                       //<3>

assert sharedData.getProperty('foo') == 123     //<4>

<1> 创建 Binding 对象

<2> 创建 GroovyShell 对象,使用共享数据

<3> 使用未声明的变量存储数据结果至共享数据对象中

<4> 读取共享结果数据

从脚本中写入的共享数据,必须为未声明的变量。像下面例子中,使用定义或明确类型将会失败,其原因是这样你就已定义了一个本地变量:

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"
}

在多线程环境中使用共享数据,需要更加的小心。Binding 实例传递给 GroovyShell 是非线程安全的,其在所有脚本中共享。

It is possible to work around the shared instance of Binding by leveraging the Script instance which is returned by parse:

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> will store the x variable inside b1

<2> will store the x variable inside b2

然而,你需要注意的是你仍旧在同一个共享的脚本实例上。如果有两个线程在同一个脚本上工作,这项技术将不可使用。在那种情况下,就必须创建两个独立的脚本实例:

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> 为 thread 1 创建 script 实例

<2> 为 thread 2 创建 script 实例

<3> 第一个共享对象分配给 script 1

<4> 第二个共享对象分配给 script 2

<5> 在 thread 1 中启动 script 1

<6> 在 thread 2 中启动 script 2

<7> 等待执行结束

如果你需要线程安全,建议直接使用 GroovyClassLoader

8.1.2.3. Custom script class

我们已经知道 parse 方法返回 groovy.lang.Script 实例,还能够通过自定义扩展 Script 类来增强 script 的处理能力,例如:

abstract class MyScript extends Script {
    String name

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

扩展类自定义了一个 name 属性以及 greet 方法。通过配置这个类可以按照 script 类方式使用。

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> 创建 CompilerConfiguration 实例

<2> 指定 scripts 执行的基类名称为 MyScript

<3> 创建 shell 是指定当前定义的编译器配置信息

<4> 此 script 可以访问 greet 方法

并不限于使用唯一的 scriptBaseClass 配置。可以使用任意多个编译器配置,甚至自定义。

8.1.3. GroovyClassLoader

前面章节,我看到 GroovyShell 是用来执行 scripts 的一个简单工具。在其内部,它使用 groovy.lang.GroovyClassLoader 在运行时编译并加载 classes.

通过使用 GroovyClassLoader 替代 GroovyShell ,你可以加载 classes 替代 scripts 实例。

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> create a new GroovyClassLoader

<2> parseClass will return an instance of Class

<3> you can check that the class which is returns is really the one defined in the script

<4> and you can create a new instance of the class, which is not a script

<5> then call any method on it

GroovyClassLoader 中维护它创建的所有 classes , 所有存储溢出会比较容易出现。尤其是, 当你执行两次相同的脚本,你将得到两个不同的类!

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> dynamically create a class named “Foo”

<2> create an identical looking class, using a separate parseClass call

<3> make sure both classes have the same name

<4> but they are actually different!

如果你希望获得相同的实例, 源数据必须为文件,例如:

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> parse a class from a File

<2> parse a class from a distinct file instance, but pointing to the same physical file

<3> make sure our classes have the same name

<4> but now, they are the same instance

使用文件作为数据源, GroovyClassLoader 可以缓存生成的 class 文件, 这样可以避免在运行时创建多个类从单一源中。 Using a File as input, the GroovyClassLoader is capable of caching the generated class file, which avoids creating multiple classes at runtime for the same source.

8.2. GroovyScriptEngine

groovy.util.GroovyScriptEngine 依靠 script 重载和依赖,为应用的灵活扩展提供基础设施。GroovyShell 关注独立的 ScriptGroovyClassLoader 用于动态编译及加载 Groovy 类, GroovyScriptEngineGroovyClassLoader 之上建立一层用于处理 script 的依赖及重新加载。

为说明这点,我们将创建一个 script engine 并在无限循环中执行代码。 首先创建下面代码:

ReloadingTest.groovy

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

new Greeter()

然后使用 GroovyScriptEngine 执行代码:

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> create a script engine which will look for sources into our source directory

<2> execute the script, which will return an instance of Greeter

<3> print the greeting message

每秒钟将看到下面的打印信息:

Hello, world!
Hello, world!
...

在不终止 script 执行,修改 ReloadingTest 文件,如:

//ReloadingTest.groovy
class Greeter {
    String sayHello() {
        def greet = "Hello, Groovy!"
        greet
    }
}
new Greeter()

打印内容将会变化:

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

下面将演示 script 的依赖,新建下面文件,同样不中断 script 的执行:

//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()

打印内容将会有如下变化:

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

你可以更新 Dependency.groovy 文件:

//Depencency.groovy
class Dependency {
    String message = 'Hello, dependency 2'
}

你将看到 dependency 文件被从新加载:

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

8.2.1. CompilationUnit

Ultimately, it is possible to perform more operations during compilation by relying directly on the org.codehaus.groovy.control.CompilationUnit class. This class is responsible for determining the various steps of compilation and would let you introduce new steps or even stop compilation at various phases. This is for example how stub generation is done, for the joint compiler.

However, overriding CompilationUnit is not recommended and should only be done if no other standard solution works.

8.3. Bean Scripting Framework

Bean Scripting Framework 用于创建 Java 调用脚本语言的 APIBSF 已经有很长时间没有更新,并且在 JSR-223 中已经废弃。

Groovy 中的 BSF 引擎使用 org.codehaus.groovy.bsf.GroovyEngine 实现。事实上, BSF APIs 已经将其隐藏。通过 BSF API 使用 Groovy 和其他脚本语言一样。

由于 Groovy 其原生的支持 Java ,你所需要关心的 BSF 如何调用其他语言,例如 : JRuby

8.3.1. Getting started

GroovyBSFjars 加入到 classpath。你可以像下面 Java 代码中调用 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());

8.3.2. 传递参数

BSF 可以在 Java 与脚本语言中传递参数。你可以在 BSF 中注册/注销 beans,之后可以在 BSF 方法中调用。注册的内容可以直接在脚本中使用。 例如:

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

8.3.3. Other calling options

前面例子中使用 eval 方法。BSF 中有多种方法可以使用,详细可以查看 BSF 文档 。这里介绍另一个方法 apple,其可以使用脚本语言定义匿名函数,并使用其参数。Groovy 上可以使用闭包支持这种函数,如下:

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());

8.3.4. Access to the scripting engine

BSF 中提供勾子,用于直接获取脚本引擎。如下:

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);

8.3.5. JSR 223 javax.script API

JSR-223Java 中调用脚本语言框架的标准接口。从 Java 6 开始,其目标是为了提供一套通用框架来调用脚本语言。 Groovy 提供了丰富的集成机制,我们也建议使用 Groovy 集成机制替代 JSR-223 API

这里有关于使用 JSR-223 引擎的实例:

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

接着下面可以开始执行 Groovy 脚本: Then you can execute Groovy scripts easily:

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);

下面演示调用可执行方法:

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);