将 Groovy 集成到应用程序中
1. Groovy 集成机制
Groovy 语言提供了几种在运行时将其自身集成到应用程序(Java 甚至 Groovy)中的方法,从最基本、简单的代码执行到最完整、集成缓存和编译器定制的方法。
本节中的所有示例都使用 Groovy 编写,但相同的集成机制也可以从 Java 中使用。 |
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
类使评估简单脚本变得非常容易,但它不具备可伸缩性:脚本没有缓存,并且它不适合评估多行代码。
1.2. GroovyShell
1.2.1. 多个来源
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 方法 |
1.2.2. 在脚本和应用程序之间共享数据
可以使用 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 | 向绑定中添加一个日期(您不限于简单类型) |
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
或 explicit
类型会失败,因为您将创建一个 *局部变量*
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"
}
在多线程环境中使用共享数据时必须非常小心。传递给 GroovyShell 的 Binding 实例 不是 线程安全的,并且由所有脚本共享。 |
可以通过利用 parse
返回的 Script
实例来解决 Binding
的共享实例问题
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 | 会将 x 变量存储在 b1 中 |
2 | 会将 x 变量存储在 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 | 为线程 1 创建一个脚本实例 |
2 | 为线程 2 创建一个脚本实例 |
3 | 将第一个绑定分配给脚本 1 |
4 | 将第二个绑定分配给脚本 2 |
5 | 在单独的线程中启动第一个脚本 |
6 | 在单独的线程中启动第二个脚本 |
7 | 等待完成 |
如果像这里一样需要线程安全,则更建议直接使用 GroovyClassLoader。
1.2.3. 自定义脚本类
我们已经看到 parse
方法返回 groovy.lang.Script
的实例,但可以使用自定义类,只要它本身扩展 Script
。它可用于为脚本提供额外的行为,如下例所示
abstract class MyScript extends Script {
String name
String greet() {
"Hello, $name!"
}
}
自定义类定义了一个名为 name
的属性和一个名为 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 | 创建一个 CompilerConfiguration 实例 |
2 | 指示它将 MyScript 用作脚本的基类 |
3 | 然后,在创建 Shell 时使用编译器配置 |
4 | 脚本现在可以访问新方法 greet |
您不仅限于单一的 scriptBaseClass 配置。您可以使用任何编译器配置调整,包括编译定制器。 |
1.3. GroovyClassLoader
在上一节中,我们已经展示了 GroovyShell
是一个执行脚本的简单工具,但它使得编译除脚本之外的任何东西都变得复杂。在内部,它使用了 groovy.lang.GroovyClassLoader
,它是在运行时编译和加载类的核心。
通过利用 GroovyClassLoader
而不是 GroovyShell
,您将能够加载类,而不是脚本实例
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 | 创建一个新的 GroovyClassLoader |
2 | parseClass 将返回 Class 的实例 |
3 | 您可以检查返回的类是否确实是脚本中定义的类 |
4 | 并且您可以创建该类的新实例,该实例不是脚本 |
5 | 然后调用其上的任何方法 |
GroovyClassLoader 会保留它创建的所有类的引用,因此很容易造成内存泄漏。特别是,如果您两次执行相同的脚本,如果它是字符串,那么您会获得两个不同的类! |
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 | 动态创建一个名为“Foo”的类 |
2 | 使用单独的 parseClass 调用创建一个看起来相同的类 |
3 | 确保两个类具有相同的名称 |
4 | 但它们实际上是不同的! |
原因是 GroovyClassLoader
不会跟踪源文本。如果您想拥有相同的实例,那么源 必须 是一个文件,就像在这个例子中一样
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 | 从一个不同的文件实例解析一个类,但指向同一个物理文件 |
3 | 确保我们的类具有相同的名称 |
4 | 但现在,它们是同一个实例 |
使用 File
作为输入,GroovyClassLoader
能够 缓存 生成的类文件,这避免了在运行时为同一源创建多个类。
1.4. GroovyScriptEngine
groovy.util.GroovyScriptEngine
类为依赖脚本重载和脚本依赖的应用程序提供了灵活的基础。虽然 GroovyShell
专注于独立 Script
,GroovyClassLoader
处理任何 Groovy 类的动态编译和加载,但 GroovyScriptEngine
将在 GroovyClassLoader
之上添加一层,以处理脚本依赖和重载。
为了说明这一点,我们将创建一个脚本引擎并在无限循环中执行代码。首先,您需要在其中创建一个包含以下脚本的目录
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 | 创建一个脚本引擎,它将在我们的源目录中查找源 |
2 | 执行脚本,它将返回一个 Greeter 实例 |
3 | 打印问候语 |
此时,您应该每秒看到一条消息打印出来
Hello, world! Hello, world! ...
在 不 中断脚本执行的情况下,现在将 ReloadingTest
文件的内容替换为
class Greeter {
String sayHello() {
def greet = "Hello, Groovy!"
greet
}
}
new Greeter()
消息应更改为
Hello, world! ... Hello, Groovy! Hello, Groovy! ...
但也可以依赖另一个脚本。为了说明这一点,在不中断正在执行的脚本的情况下,在同一目录中创建以下文件
class Dependency {
String message = 'Hello, dependency 1'
}
并像这样更新 ReloadingTest
脚本
import Dependency
class Greeter {
String sayHello() {
def greet = new Dependency().message
greet
}
}
new Greeter()
这次,消息应更改为
Hello, Groovy! ... Hello, dependency 1! Hello, dependency 1! ...
作为最后一次测试,您可以更新 Dependency.groovy
文件,而无需触及 ReloadingTest
文件
class Dependency {
String message = 'Hello, dependency 2'
}
您应该会观察到依赖文件已重新加载
Hello, dependency 1! ... Hello, dependency 2! Hello, dependency 2!
1.5. CompilationUnit
最终,可以通过直接依赖 org.codehaus.groovy.control.CompilationUnit
类在编译期间执行更多操作。此类负责确定编译的各个步骤,并允许您引入新步骤甚至在各个阶段停止编译。例如,存根生成就是这样完成的,对于联合编译器。
但是,不建议重写 CompilationUnit
,并且只有在没有其他标准解决方案有效时才应这样做。
2. JSR 223 javax.script API
JSR-223 是一个用于在 Java 中调用脚本框架的标准 API。它自 Java 6 起可用,旨在提供一个通用的框架,用于从 Java 调用多种语言。Groovy 提供了自己更丰富的集成机制,如果您不打算在同一个应用程序中使用多种语言,建议您使用 Groovy 集成机制而不是受限的 JSR-223 API。 |
以下是如何初始化 JSR-223 引擎以从 Java 与 Groovy 通信
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
...
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("groovy");
然后您可以轻松执行 Groovy 脚本
Integer sum = (Integer) engine.eval("(1..10).sum()");
assertEquals(Integer.valueOf(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(Integer.valueOf(120), result);
引擎默认会保留对脚本函数的硬引用。要更改此设置,您应该将一个引擎级别作用域属性设置为脚本上下文,名称为 #jsr223.groovy.engine.keep.globals
,其字符串值为 phantom
以使用虚引用,weak
以使用弱引用,或 soft
以使用软引用 - 大小写被忽略。任何其他字符串都将导致使用硬引用。