【翻译】Java 集成 Groovy 的几种方式
Java 集成 Groovy 的几种方式
翻译自:http://www.groovy-lang.org/integrating.html
1.1 Eval
可以使用 groovy.util.Eval 这个类中的 me 方法来执行 Groovy 脚本
import groovy.util.Eval
assert Eval.me('33*3') == 99
assert Eval.me('"foo".toUpperCase()')=='FOO'
Eval 也支持多种传递参数的方式
// 简单绑定一个参数到 x 上
assert Eval.x(4, '2*x') == 8
// 将一个参数绑定到一个字段上
assert Eval.me('k', 4, '2*k') == 8
// 绑定两个参数,分别到 x,y 上
assert Eval.xy(4, 5, 'x*y') == 20
// 绑定两个参数,分别到 x,y,z 上
assert Eval.xyz(4, 5, 6, 'x*y+z') == 26
Eval 使得执行一个简单脚本变得非常容易,要注意的是,它没有缓存,不要执行超过一行的脚本
1.2 GroovyShell
1.2.1 执行不同来源的脚本
groovy.lang.GroovyShell 类是一个更好的方式去执行脚本,并且可以缓存脚本实例。尽管 Eval 可以返回执行的结果,但是 GroovyShell 提供了更多选项
// 定义实例
def shell = new GroovyShell()
// 直接执行脚本片段
def result = shell.evaluate('3*5')
// 执行从其它来源读入的脚本片段,可以是 String、Reader、File、InputStream
def result2 = shell.evaluate(new StringReader('3*5'))
assert result == result2
// 解析脚本,并创建脚本实例,但是不执行
def script = shell.parse '3*5'
assert script instanceof groovy.lang.Script
// 调用 run 方法执行脚本
assert script.run() == 15
1.2.2 共享应用程序数据
可以使用 groovy.lang.Binding 来进行脚本和应用程序之间的数据
// 创建一个 Binding
def shareData = new Binding()
// 创建 shell
def shell = new GroovyShell(shareData)
def now = new Date()
// 将 text、date 对象传递到 Binding 中
shareData.setProperty('text', 'i am shared data')
shareData.setProperty('date', now)
// 执行脚本,通过 $ 符号,在脚本中获取变量值
String result = shell.evaluate('"At $date,$text"')
printf result
也可以在脚本中写入变量到 Binding
// 在脚本中对变量赋值
shell.evaluate('foo=123')
// 通过 Binding 获取标量的值
printf shareData.getProperty('foo') as String
在脚本中写入变量到 Binding 中,这个变量必须是未声明的,使用 def 或者显示类型的变量就会报错,像下面的例子一样
// 创建一个 Binding
def shareData = new Binding()
// 创建 shell
def shell = new GroovyShell(shareData)
// 执行脚本,注意 foo 的定义使用了 def 是本地变量
shell.evaluate('def foo=123')
// 通过 Binding 获取标量的值
try {
printf shareData.getProperty('foo') as String
} catch (MissingPropertyException e) {
println "foo is defined as a local variable"
}
必须非常小心的使用共享数据,在多线程的环境下,传递给 GroovyShell 的 Binding 实例并不是线程安全的,
解决 Binding 的共享实例问题,可以通过使用 Script 的 parse 方法实现
def shell = new GroovyShell()
def b1 = new Binding(x:3)
def b2 = new Binding(x:4)
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
然而必须意识到任然在使用同一个脚本的共享实例,如果使用两个线程共用了一个相同的 Script,则必须创建两个 Script
如果确实需要线程安全,则使用 GroovyClassLoader 更好一些
1.2.3 自定义脚本类
我们可以看到 parse 方法返回了一个 Script 类型,我们可以使用一个自定义的类来几次它,这样可以在我们自定义的类中增加额外的方法,例如:
abstract class MyScript extends Script {
String name
String greet() {
"hellp,$name!"
}
}
在自定义类中,定义了一个 name 字段,并且有一个 greet 的方法
- 类加载器,通过 GroovyClassLoader
- 使用 ScriptEngine
- 使用 GroovyShell
1.3 GroovyClassLoader
在之前的内容中,可以看到 GroovyShell 可以非常简单的去执行一个脚本,但是除了编译脚本之外的内容会非常复杂,在 GroovyShell 的内部,其实是使用了 groovy.lang.GroovyClassLoader,它是运行时编译和加载类的核心
通过使用 GroovyClassLoader 来代替 GroovyShell,可以来加载一个 Class 代替 Script 实例
def gcl = new GroovyClassLoader()
def clazz = gcl.parseClass('class Foo { void doIt() { println "ok" } }')
assert clazz.name == 'Foo'
def o = clazz.newInstance()
o.doIt()
因为 GroovyClassLoader 持有它创建的类的引用,非常容易造成内存泄露,典型的例子,如果你执行相同的代码两次,如果它是一个字符串,这样你就获得了两个类。
def gcl = new GroovyClassLoader()
def clazz1 = gcl.parseClass('class Foo { }')
def clazz2 = gcl.parseClass('class Foo { }')
assert clazz1.name == 'Foo'
assert clazz2.name == 'Foo'
assert clazz1 != clazz2
原因是 GroovyClassLoader 不会记录脚本字符串的来源,如果想要相同的实例,来源必须是一个 file,例如:
def gcl = new GroovyClassLoader()
def clazz1 = gcl.parseClass(file)
def clazz2 = gcl.parseClass(new File(file.absolutePath))
assert clazz1.name == 'Foo'
assert clazz2.name == 'Foo'
assert clazz1 == clazz2
使用一个 File 作为输入,GroovyClassLoader 能够缓存生成的类文件,从而避免在运行时为同一源创建多个类。
1.4 GroovyScriptEngine
groovy.util.GroovyScriptEngine 类为依赖脚本重新加载和脚本依赖项的应用程序提供了灵活的基础。GroovyShell 专注于独立脚本,而 GroovyClassLoader 处理任何 Groovy 类的动态编译和加载。GroovyScriptEngine 将在 GroovyClassLoader 之上添加一个层来处理脚本依赖项和重新加载。
为了说明这一点,我们将创建一个脚本引擎并在无限循环中执行代码。首先,你需要创建一个目录,里面包含以下脚本
个人理解:GroovyScriptEngine:
GroovyScriptEngine 是 GroovyClassLoader 的一个扩展,提供了更高级的功能。
主要用于处理脚本之间的依赖关系、脚本的重新加载和管理脚本引擎。
适用于依赖于脚本重载和脚本之间依赖关系的复杂应用场景。
GroovyClassLoader:
GroovyClassLoader 是负责动态编译和加载 Groovy 类的核心类。
允许在运行时编译 Groovy 代码,并将其加载为 Java 字节码,然后可以使用这些类。
适用于加载和执行 Groovy 类,无论是独立的脚本还是以类的形式存在。
简而言之,GroovyScriptEngine 提供了更高级的功能,主要用于管理脚本引擎、处理脚本的依赖和重载等高级场景。而 GroovyClassLoader 则是一个更基本的工具,用于加载和运行 Groovy 类。你可以根据具体的需求选择使用哪个类。如果你只需要加载和执行 Groovy 类,GroovyClassLoader 足够了。如果你需要处理更复杂的脚本重载和依赖关系,可以考虑使用 GroovyScriptEngine
定义一个 ReloadingTest.groovy
class Greeter {
String sayHello() {
def greet = "Hello, world2!"
greet
}
}
new Greeter()
再定义另一个 GroovyScriptEngineTest.groovy.使用 GroovyScriptEngine 进行执行
def binding = new Binding()
def tmpDir = new File("")
def engine = new GroovyScriptEngine([tmpDir.toURI().toURL()] as URL[])
while (true) {
def greeter = engine.run('ReloadingTest.groovy', binding)
println greeter.sayHello()
Thread.sleep(1000)
}
执行后可以看到,不断的循环输出:
Hello, world!
Hello, world!
不打断执行程序修改 ReloadingTest.groovy 的输出内容为,Hello, world2!控制台输出改变:
Hello, world!
...
Hello, world2!
也可以动态修改依赖的类,为了说明这一点,在同一个目录下创建一个 Dependency.groovy
class Dependency {
String message = 'Hello, dependency 1'
}
然后更新 ReloadingTest script
class Greeter {
String sayHello() {
def greet = new Dependency().message
greet
}
}
new Greeter()
在此刻,可以看到控制台的内容也随之发生了改变
Hello, world2!
Hello, dependency 1
Hello, dependency 1
1.5. CompilationUnit
终极的,通过依赖 org.codehaus.groovy.control.CompilationUnit 可以在编译期间执行更多操作。该类负责确定编译的各个步骤,并允许您引入新步骤,甚至在各个阶段停止编译。这就是联合编译器的实现方式。
但是,不建议重写 CompilationUnit,并且只有在没有其他标准解决方案有效的情况下才应该这样做。
2 JSR223 javax.script API
JSR-223 是一个脚本的 Java 脚本执行 API。从 Java 6 开始提供的一个框架,目标是为了通用的脚本调用框架。Groovy 提供了集成,如果你计划在一个程序中实现多语言调用,建议您使用 Groovy 集成机制而不是有限的 JSR-223 API。
下面的示例演示如果使用 Groovy 的 JSR-223 引擎和 Java 进行对话
ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName("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 表示使用软引用 - 大小写忽略。任何其他字符串都默认强引用。

浙公网安备 33010602011771号