将 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 侧重于独立的 Script,而 GroovyClassLoader 处理任何 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以使用软引用 - 大小写不敏感。其他任何字符串都会导致使用硬引用。