加入收藏 | 设为首页 | 会员中心 | 我要投稿 李大同 (https://www.lidatong.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

Groovy 与应用的集成

发布时间:2020-12-14 16:40:02 所属栏目:大数据 来源:网络整理
导读:1. Groovy 集成机制 Groovy 语言提供了几种在运行时与应用(由 Java 或 Groovy 所编写)相集成的机制,涉及到了从最基本的简单代码执行,到最完整的集成缓存和编译器自定义设置等诸多方面。 本部分内容所有范例都是用 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,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,如果没有其他的办法时才应该这样做。

2. Bean 脚本框架

Bean 脚本框架(BSF,Bean Scripting Framework) 试图通过一个 API 来调用 Java 中的脚本语言。它已经很长时间没有更新过了,但由于支持标准的 JSR-223 API,所以还没有被遗弃。

BSF 引擎由 org.codehaus.groovy.bsf.GroovyEngine 类所实现。但这一事实常常被 BSF 的 API 所掩盖。通过 BSF API,只会把 Groovy 看成其他同样的脚本语言。

由于 Groovy 对 Java 集成有着原生的支持,所以你只需要注意下面两种情形中的 BSF 应用:还想调用其他语言时(如 JRuby),或者想与你所使用的脚本语言保持一种非常松散的耦合。

2.1 入门

假设在类路径中有 Groovy 和 BSF 的 jar 文件,那么可以使用下列代码运行简单的 Groovy 脚本。

String myScript = "println('Hello World')n  return [1,2,3]";
BSFManager manager = new BSFManager();
List answer = (List) manager.eval("groovy","myScript.groovy",myScript);
assertEquals(3,answer.size());

2.2 传入变量

BSF 可以使你在 Java 和脚本语言间传入 bean。可以注册/不注册 bean,使其被 BSF 所知晓,然后利用 BSF 方法在需要时查找 bean。另外,还可以声明/不声明 bean。这将注册它们,但也使其能够直接用于脚本语言。第二种方法是 Groovy 通常所习惯采用的方法,如下所示:

BSFManager manager = new BSFManager();
manager.declareBean("xyz",Integer.class);
Object answer = manager.eval("groovy","test.groovy","xyz + 1");
assertEquals(5,answer);

2.3 其他调用选项

上面的范例使用了 eval 方法。BSF 提供了多种方法(详情参见 BSF 文档),其中可用的另一种方法是 apply。它能让你用脚本语言定义一种匿名函数,然后把该函数应用到参数中。Groovy 利用闭包支持这种函数,如下所示:

BSFManager manager = new BSFManager();
Vector<String> ignoreParamNames = null;
Vector<Integer> args = new Vector<Integer>();
args.add(2);
args.add(5);
args.add(1);
Integer actual = (Integer) manager.apply("groovy","applyTest","def summer = { a,b,c -> a * 100 + b * 10 + c }",ignoreParamNames,args);
assertEquals(251,actual.intValue());

2.4 访问脚本引擎

虽然不是很常用,但 BSF 还是提供了一种钩子,以便直接访问脚本引擎。引擎所执行的一个函数是在对象上调用一个方法。如下所示:

BSFManager manager = new BSFManager();
BSFEngine bsfEngine = manager.loadScriptingEngine("groovy");
manager.declareBean("myvar","hello",String.class);
Object myvar = manager.lookupBean("myvar");
String result = (String) bsfEngine.call(myvar,"reverse",new Object[0]);
assertEquals("olleh",result);

3 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(new Integer(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(new Integer(120),result);

引擎持有脚本函数的每一个默认的硬编码引用。要想改变这一点,可以为名为 ##jsr223.groovy.engine.keep.globals 的脚本上下文设置引擎级别的范围属性:用 phantom 字符串使用虚引用,用 weak 来使用弱引用,用 soft 来使用软引用,忽略大小写问题。任何其他的字符串都会导致使用硬编码引用。

(编辑:李大同)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读