将 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 带两个名为 xy 的绑定参数的简单评估
4 带三个名为 xyz 的绑定参数的简单评估

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 可以从多个源(StringReaderFileInputStream)读取
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 从调用者读取结果

重要的是要理解,如果您想写入绑定,则需要使用未声明的变量。像下面的示例一样使用 defexplicit 类型会失败,因为您将创建一个 *局部变量*

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"
}
在多线程环境中使用共享数据时必须非常小心。传递给 GroovyShellBinding 实例 不是 线程安全的,并且由所有脚本共享。

可以通过利用 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 专注于独立 ScriptGroovyClassLoader 处理任何 Groovy 类的动态编译和加载,但 GroovyScriptEngine 将在 GroovyClassLoader 之上添加一层,以处理脚本依赖和重载。

为了说明这一点,我们将创建一个脚本引擎并在无限循环中执行代码。首先,您需要在其中创建一个包含以下脚本的目录

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 创建一个脚本引擎,它将在我们的源目录中查找源
2 执行脚本,它将返回一个 Greeter 实例
3 打印问候语

此时,您应该每秒看到一条消息打印出来

Hello, world!
Hello, world!
...

中断脚本执行的情况下,现在将 ReloadingTest 文件的内容替换为

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

new Greeter()

消息应更改为

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

但也可以依赖另一个脚本。为了说明这一点,在不中断正在执行的脚本的情况下,在同一目录中创建以下文件

Dependency.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 文件,而无需触及 ReloadingTest 文件

Dependency.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 类在编译期间执行更多操作。此类负责确定编译的各个步骤,并允许您引入新步骤甚至在各个阶段停止编译。例如,存根生成就是这样完成的,对于联合编译器。

但是,不建议重写 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 以使用软引用 - 大小写被忽略。任何其他字符串都将导致使用硬引用。