返回介绍

1. Groovy 集成机制

发布于 2025-01-04 00:44:54 字数 10546 浏览 0 评论 0 收藏 0

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⃣️ 从调用中读取结果。

这里重要的一点是,如果想写入绑定对象,必须要使用未声明变量。使用 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"
}

在多线程环境中使用共享数据应该极为小心。传入 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⃣️ 在 b1 中存储 x 变量。 2⃣️ 在 b2 中存储 x 变量。

但是,必须注意,此时仍旧共享的是脚本的 同一个实例 。因此,如果两个线程都要利用同一脚本,就不能采用这种方法,这时必须创建两个独立的脚本实例。

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 个绑定对象赋予脚本 1。
4⃣️ 将第 2 个绑定对象赋予脚本 1。
5⃣️ 在单独的一个线程中运行脚本 1。
6⃣️ 在单独的一个线程中运行脚本 2。
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 配置。可以使用任何编译器配置微调选项,包括 compilation customizers

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 聚焦单独的脚本, GroovyClassLoader 能够处理任何 Groovy 类的动态编译与加载,然而 GroovyScriptEngine 能够为 GroovyClassLoader 其上再增添一个能够处理脚本依赖及重新加载的功能层。

为了说明这一点,下面来创建脚本引擎,用无限循环来执行脚本。首先需要创建一个目录,将下列脚本(ReloadingTest.groovy)放入其中。

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!
...  

但它还可能会依赖其他脚本。接下来在同一目录中创建下面这个文件,同样不用干扰上述脚本执行:

Depencency.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!
...

作为最后一项测试,下面我们在不改动 ReloadingTest 文件的前提下,更新 Dependency.groovy 文件。

Depencency.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 ,如果没有其他的办法时才应该这样做。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文