【Groovy翻译系列三】Groovy应用集成

原文档链接:​​https://groovy-lang.org/integrating.html​

Groovy是一门基于JVM的语言,堪称动态语言版Java,其各种动态语言特性填补了Java的各种空缺,让人拍案叫绝......有幸接触Groovy,遂手动翻译三篇官方文档,以便于读者从Java到Groovy的快速迁移。


Groovy 语言提出了几种在运行时将自身集成到应用程序(Java 甚至 Groovy)中的方法,从最基本的简单代码执行到最完整的集成缓存和编译器定制。

本节中编写的所有示例都使用 Groovy,但可以在 Java 中使用相同的集成机制。

1.1. Eval

这个​​groovy.util.Eval​​​类是最简单的方式在运行时动态的执行Groovy。可以使用​​me​​方法:

java 复制代码
import groovy.util.Evalassert Eval.me('33*3') == 99
assert Eval.me('"foo".toUpperCase()') == 'FOO'

​Eval​​支持多种参数接收变体来支持简易的表达式:

java 复制代码
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​​可以提供更多的选择。

ini 复制代码
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​​去在脚本和应用间共享数据:

scss 复制代码
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 执行脚本

注意,也有可以在脚本中写入绑定:

scss 复制代码
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​​类型将会失败,因为你将会创建一个本地变量:

java 复制代码
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​​的共享实例:

java 复制代码
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​​中

但是,你必须知道你仍在共享同一脚本实例。因此,如果你有两个线程处理同一个脚本,则无法使用此技术。在这种情况下,你必须确保创建两个不同的脚本实例:

scss 复制代码
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​​本身。它可用于为脚本提供额外的行为,如下例所示:

scala 复制代码
abstract class MyScript extends Script {
    String name

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

这个自定义类定义了一个叫做​​name​​​的参数和一个叫做​​greet​​的方法。这个类可以通过一个自定义的配置来被用作脚本的基础类:

java 复制代码
import org.codehaus.groovy.control.CompilerConfigurationdef 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 配置。你能够调整任意的编译器配置,包括​​compilation customizers​​。

1.3. GroovyClassLoader

在上一节,我们已经展示了​​GroovyShell​​​是一个执行脚本的简单工具,但是除了脚本之外,编译任何东西都变得很复杂。在内部,它使用​​groovy.lang.GroovyClassLoader​​ ,这是运行时编译和加载类的核心。

通过使用​​GroovyClassLoader​​​替代​​GroovyShell​​,你将能够加载类,而不是脚本实例:

ini 复制代码
import groovy.lang.GroovyClassLoaderdef 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保留了它创建的所有类的引用,因此很容易造成内存泄漏。特别是,如果你执行两次相同的脚本,如果它是一个字符串,那么你将获得两个不同的类!

ini 复制代码
import groovy.lang.GroovyClassLoaderdef 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​​不跟踪源文本。如果你想拥有相同的实例,则源必须是一个文件,如下例所示:

ini 复制代码
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 从​​文件​​中解析一个类
2 从不同的文件实例解析一个类,但指向同一个物理文件
3 确保我们的类具有相同的名称
4 但现在,它们是同一个实例

使用一个​​File​​​作为输入,​​GroovyClassLoader​​是有能力缓存生成的类文件的,这样能够避免在运行时创建多个同源类。

1.4. GroovyScriptEngine

​groovy.util.GroovyScriptEngine​​​类为有脚本依赖的应用程序提供了灵活的脚本重载基础。虽然​​GroovyShell​​​专注于独立脚本,而​​GroovyClassLoader​​​处理任何 Groovy 类的动态编译和加载,但是​​GroovyScriptEngine​​​将在​​GroovyClassLoader​​之上添加一个层来处理脚本依赖关系和重新加载。

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

ReloadingTest.groovy

javascript 复制代码
class Greeter {
    String sayHello() {
        def greet = "Hello, world!"
        greet
    }
}new Greeter()

然后你可以使用​​GroovyScriptEngine​​执行此代码:

scss 复制代码
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

javascript 复制代码
class Greeter {
    String sayHello() {
        def greet = "Hello, Groovy!"
        greet
    }
}new Greeter()

消息应更改为:

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

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

Dependency.groovy

arduino 复制代码
class Dependency {
    String message = 'Hello, dependency 1'
}

并像这样更新​​ReloadingTest​​脚本:

ReloadingTest.groovy

javascript 复制代码
import Dependencyclass Greeter {
    String sayHello() {
        def greet = new Dependency().message
        greet
    }
}new Greeter()

这一次,消息应该变为:

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

作为最后一个测试,你可以更新​​Dependency.groovy​​​文件,而无需触及​​ReloadingTest​​文件:

Dependency.groovy

arduino 复制代码
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​​,只有在没有其他标准解决方案有效的情况下才应该这样做。

相关推荐
勤劳打代码13 天前
isar_flutter_libs 引发 Namespace not specified
android·flutter·groovy
安冬的码畜日常17 天前
【JUnit实战3_20】第十一章:用 Gradle 运行 JUnit 测试实战
测试工具·junit·单元测试·gradle·软件构建·groovy·junit5
ClassOps17 天前
Gradle Groovy 和 Kotlin kts 语法对比
android·kotlin·gradle·groovy
little_fat_sheep1 个月前
【Groovy】类和对象
groovy
little_fat_sheep2 个月前
【Groovy】函数、闭包、泛型
groovy
little_fat_sheep2 个月前
【Groovy】流程控制
groovy
little_fat_sheep2 个月前
【Groovy】变量和基本数据类型
groovy
邵皮皮5 个月前
Groovy 入门
groovy