Groovy 教程 - 整合 Groovy 至应用程序
这是一篇译文,读者可前往 Groovy Getting Started - Integrating Groovy into applications 阅读原文。
1 Groovy 整合机制
Groovy 语言提供了多种在运行时将其整合至(Java 甚至 Groovy)应用程序中的方法,包括了从最简单代码的执行到完整的应用程序整合缓存和编译器定制化。
本章中所有的示例都使用 Groovy 编写而成,但这些整合机制同样可用于 Java。
1.1 Eval
在运行时动态执行 Groovy 代码最简单的方式莫过于使用 groovy.util.Eval
类了,我们只需调用该类的 me
方法即可:
1 | import groovy.util.Eval |
Eval
类还提供了许多其他方法来允许用户传入参数进行简单的运算:
1 | assert Eval.x(4, '2*x') == 8 // 注1 |
- 包含一个名为
x
的参数的简单运算 - 包含一个自定义的名为
k
的参数的简单运算 - 包含两个分别名为
x
和y
的参数的简单运算 - 包含三个分别名为
x
、y
和z
的参数的简单运算
尽管 Eval
类使得我们可以很方便地运行简单的脚本,但它并不具备很好的横向扩展性:它不会对脚本进行任何缓存,也并不是设计来用于执行长度超过一行的脚本的。
1.2 GroovyShell
1.2.1 多种代码来源
比起 Eval
,groovy.lang.GroovyShell
类提供了更好的执行脚本的方式,同时还提供了对脚本实例运行结果进行缓存的支持。比起像 Eval
一般运行脚本并返回结果,GroovyShell
类还提供了更多的做法:
1 | def shell = new GroovyShell() // 注1 |
- 创建了一个
GroovyShell
实例 - 可以像
Eval
那样直接执行脚本代码 - 也可以从多种不同的来源中读取代码(
String
、Reader
、File
、InputStream
) parse
方法返回一个Script
实例,可以此延迟脚本的执行Script
类提供了run
方法
1.2.2 在脚本与应用程序间共享数据
我们可以通过 groovy.lang.Binding
类来实现脚本与应用程序间的数据共享:
1 | def sharedData = new Binding() // 注1 |
- 创建
Binding
实例用于存储共享数据 - 创建即将使用这些共享数据的
GroovyShell
实例 - 将一个
String
添加到了Binding
中 - 讲一个
Date
添加到了Binding
中(你可以放入除基本类型外的其他类型的数据 - 执行脚本
值得注意的是我们还可以在脚本中向 Binding
写入数据:
1 | def sharedData = new Binding() // 注1 |
- 创建
Binding
实例 - 创建即将使用这些共享数据的
GroovyShell
实例 - 通过使用一个未声明的变量来讲数据存储到
Binding
中 - 从应用程序中获取数据
值得注意的是,如果你想要将数据写入到 Binding
中,你需要使用未声明的变量。像下面的例子那样使用 def
或 explicit
类型是不会将数据写入到 Binding
中的,因为这样做实际上是创建了一个局部变量:
1 | def sharedData = new Binding() |
当你想要在多线程环境中使用共享数据时必须提高警惕:你所传递给 GroovyShell
的 Binding
实例不是线程安全的,而且它被所有脚本所共享。
我们倒是可以通过利用由 parse
方法返回的 Script
实例来绕过共享的 Binding
实例:
1 | def shell = new GroovyShell() |
- 将变量
x = 3
保存到b1
中 - 将变量
x = 4
保存到b2
中
然而,你仍该意识到,这样做的时候你则是在共享同一个 Script
实例的使用,因此如果你想要让两个线程同时使用同样的脚本的话,这样的做法并不合适。在这种情况下,你应创建两个不同的 Script
实例:
1 | def shell = new GroovyShell() |
- 创建用于 1 号线程的
Script
实例 - 创建用于 2 号线程的
Script
实例 - 将第一个
Binding
赋予第一个Script
- 将第二个
Binding
赋予第二个Script
- 在一个独立的线程中启动第一个
Script
- 在一个独立的线程中启动第二个
Script
- 等待运行结束
除非你需要像上述案例那样的线程安全性,否则我们更推荐你直接使用 GroovyClassLoader
。
1.2.3 自定义脚本类
我们了解到 parse
方法可以返回 groovy.lang.Script
实例,但它同样可以返回自定义的类,只要该类扩展了 Script
类。这么做能像下述的案例那样让 Script
实例支持更多的操作:
1 | abstract class MyScript extends Script { |
这个自定义类定义了一个叫做 name
的属性以及一个叫做 greet
的新方法。通过一些自定义设置,我们可以使用这个类作为脚本的基类:
1 | import org.codehaus.groovy.control.CompilerConfiguration |
- 创建
CompilerConfiguration
实例 - 令其使用
MyScript
类作为脚本基类 - 然后在创建
GroovyShell
时使用该CompilerConfiguration
实例 - 现在返回的脚本可以访问新方法
greet
了
你可以进行的设置当然不止 scriptBaseClass
。你可以使用任意 CompilerConfiguration
设置,包括编译定制器。
1.3 GroovyClassLoader
在之前的章节中,我们看到 GroovyShell
可以很方便地执行脚本,但它并不适合用于编译除脚本以外的东西。在 GroovyShell
内部它实际上使用了 groovy.lang.GroovyClassLoader
,而后者则是运行时编译并载入类的核心所在。
通过使用 GroovyClassLoader
,你可以载入类而不是脚本实例:
1 | import groovy.lang.GroovyClassLoader |
- 创建一个
GroovyClassLoader
实例 parseClass
方法会返回一个Class
实例- 可以看到返回的类确实是在脚本中定义的类
- 你也可以创建一个该类的实例,可见返回的确实是类而不是脚本
- 你也可以调用所创建实例的方法
GroovyClassLoader
会维持对所有由其所创建的类的引用,而这很容易导致内存泄漏。具体来说,如果你使用一个 String
对象来让 GroovyClassLoader
对同样的脚本进行两次处理,你实际上会获得两个不同的类!
1 | import groovy.lang.GroovyClassLoader |
- 动态创建一个名为
Foo
的类 - 使用第二次
parseClass
方法调用创建一个一模一样的类 - 两个类拥有相同的名称
- 但它们是两个不同的类!
原因在于 GroovyClassLoader
不会记录源代码文本。如果你希望它返回相同的 Class
实例,你则必须像下面的示例那样使用文件作为代码来源:
1 | def gcl = new GroovyClassLoader() |
- 从一个
File
中解析类 - 使用不同的
File
实例进行类解析,但两个File
在物理上指向同一个文件 - 两个类有相同的名称
- 现在,它们确实是相同的
Class
实例了
使用 File
作为输入时,GroovyClassLoader
能够对生成的类文件进行缓存,这就避免了在运行时对同样的代码生成多个不同的类了。
1.4 GroovyScriptEngine
对于那些需要处理脚本重载与脚本依赖的应用程序来说,groovy.util.GroovyScriptEngine
提供了扩展性强的良好基础。前面我们看到,GroovyShell
专注于处理各个独立的 Script
对象,GroovyClassLoader
负责处理任意 Groovy 类的动态编译与载入,而接下来你将看到,GroovyScriptEngine
是在 GroovyClassLoader
之上添加了新的一层封装,可用于处理脚本的依赖与重载。
为此,我们会在接下来的案例中先创建一个 GroovyScriptEngine
并在一个无限循环中运行它。首先,你需要创建一个文件夹并在里面放入如下脚本文件:
ReloadingTest.groovy
1 | class Greeter { |
然后你就能用 GroovyScriptEngine
运行这个代码了:
1 | def binding = new Binding() |
- 创建一个
GroovyScriptEngine
并指定其在我们的源文件夹中寻找源文件 - 运行脚本,返回一个
Greeter
实例 - 打印信息
这样,每秒你都会看到其打印一行信息:
1 | Hello, world! |
不要 中断脚本的执行,现在我们将 ReloadingTest.groovy
文件的内容修改至如下:
1 | class Greeter { |
你应该能看到打印的信息发生了如下的改变:
1 | Hello, world! |
我们还能依赖另一个脚本。为此,我们先不要中断刚才正在执行的脚本,并在刚刚的文件夹中创建文件如下:
Dependency.groovy
1 | class Dependency { |
然后更新 ReloadingTest.groovy
脚本如下:
1 | import Dependency |
这次,你会看到打印信息变成了这样:
1 | Hello, Groovy! |
最后,你还能在不修改 ReloadingTest.groovy
文件的情况下对 Dependency.groovy
文件进行修改:
1 | class Dependency { |
之后你应该能观察到依赖文件被重新载入了:
1 | Hello, dependency 1! |
1.5 CompilationUnit
最后,我们还可以通过直接使用 org.codehaus.groovy.control.CompilationUnit
类来进行更多的操作。该类负责确定编译各个步骤的具体行为,还能让你在编译中加入新的步骤甚至在指定的步骤中停止编译。
然而,我们不推荐你重载 CompilationUnit
,除非其他标准的做法都无法满足你的需求。
2 Bean 脚本框架
Bean 脚本框架(Bean Scripting Framework,BSF)尝试为 Java 创建一套 API 用以调用脚本语言。可惜的是,它已经被最新的 JSR-223 API 所替代而且很长一段时间没有更新了。
Groovy 的 BSF 引擎由 org.codehaus.groovy.bsf.GroovyEngine
类所实现。然而,BSF 的 API 通常会将这个细节所遮蔽。你只需要在 BSF API 中像处理其他脚本语言那样使用 Groovy 即可。
由于 Groovy 本身有对与 Java 应用程序整合的原生支持,大多数情况下你不需要为 BSF 操心太多,除非你还想要调用如 JRuby 等其他语言,或者你希望你的应用程序与你所使用的脚本语言之间保持极度松耦合的关系。
2.1 热身入门
假设你已经把 Groovy 和 BSF 的 JAR 包放到了类路径中,你可以使用如下 Java 代码来运行一段 Groovy 脚本样例了:
1 | String myScript = "println('Hello World')\n return [1, 2, 3]"; |
2.2 传递参数
BSF 还允许你在 Java 应用程序和脚本语言之间传递 Bean 对象。你可以通过注册/注销 Bean 类的方式使 BSF 得知其存在。之后你可以通过 BSF 提供的方法来对 Bean 类进行检索。除此之外,你还可以声明/反声明 Bean 类,如此一来便能注册该 Bean 类且使得脚本语言也能直接使用它们。当我们使用 Groovy 时通常会使用第二种方法,示例如下:
1 | BSFManager manager = new BSFManager(); |
2.3 其他调用选项
上面的案例中均使用了 eval
方法。除此之外 BSF 还提供了很多其他方法功能使用(详情可参阅 BSF 文档)。其中包括 apply
方法,其允许你在脚本语言中定义一个匿名函数并将其应用于给定的参数。Groovy 则通过闭包来支持该功能。示例如下:
1 | BSFManager manager = new BSFManager(); |
2.4 访问脚本引擎
尽管在一般情况下你不会用到,但 BSF 提供了一些方法使你可以直接访问脚本引擎。脚本引擎的其中一个功能为对给定的对象调用方法。示例如下:
1 | BSFManager manager = new BSFManager(); |
3 JSR-223 javax.script
API
JSR-223 为一套从 Java 中调用脚本框架的标准 API。它从 Java6 开始加入 Java 平台,并企图向开发者提供一套从 Java 中调用多种语言的通用框架。Groovy 本身就提供了功能丰富的整合机制,所以如果你并不打算在同一个应用程序中使用其他脚本语言,我们更推荐你使用 Groovy 的整合机制而不是功能有限的 JSR-223 API。
你需要通过如下代码来初始化 JSR-223 引擎使其能从 Java 访问 Groovy:
1 | import javax.script.ScriptEngine; |
然后你就能很轻松地运行 Groovy 脚本了:
1 | Integer sum = (Integer) engine.eval("(1..10).sum()"); |
你还能在 Java 和 Groovy 间共享变量:
1 | engine.put("first", "HELLO"); |
如下示例展示了如何调用一个可调用函数:
1 | import javax.script.Invocable; |
默认情况下脚本引擎会对脚本函数维持强引用。你可以通过将一个名为 #jsr223.groovy.engine.keep.globals
的引擎属性设置到脚本上下文中来改变此行为。将该变量设置为 phantom
来使用虚引用、设置为 weak
来使用弱引用、设置为 soft
来使用软引用。该变量值不区分大小写,但设置为任何其他 String
值都会使引擎继续使用强引用。
Groovy 教程 - 整合 Groovy 至应用程序