领先一步
VMware 提供培训和认证,助您加速进步。
了解更多自 Spring 2.0 引入 Spring 动态语言支持以来,它一直是 Groovy 的一个有吸引力的集成点,而 Groovy 为定义领域特定语言 (DSL) 提供了丰富的环境。但是,Spring 参考手册中 Groovy 集成的示例范围有限,并未展示 Spring 中针对 DSL 集成的功能。在本文中,我将展示如何使用这些功能,并以 Grails 发行版中的 Groovy DSL 向现有 ApplicationContext 添加 Bean 定义为例。
Spring 动态语言集成的基本功能在 XML 的 "lang" 命名空间中公开。您可以做的最直接的事情是将 Spring 组件定义为 Groovy Bean,可以放在单独的文件中,也可以直接放在 XML 中。此功能已包含在 Spring 参考指南 (http://static.springframework.org/spring/docs/2.5.x/reference/index.html) 中,因此我们不需要过多细节,但为了完整起见,我们还是看一个简短的示例。
假设我们有一个 Java 接口
public interface Messenger {
String getMessage();
}
这是 Groovy 中一个内联 Bean 定义,它实现了该接口
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:lang="http://www.springframework.org/schema/lang"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang-2.5.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<lang:groovy id="messenger">
<![CDATA[
class GroovyMessenger implements spring.Messenger {
def String message;
}
]]>
</lang:groovy>
</beans>
请注意,由于 Groovy 为所有属性定义了公共的 getter 和 setter,因此我们不必显式编写 getMessage() 方法。另外请记住,Spring 动态语言支持的一个功能是,内联 Groovy 代码也可以提取到单独的源文件中(使用 lang:groovy 元素的 script-source 属性)。
Spring 动态语言支持的另一个功能是,脚本可以超越简单的类定义。您还可以编写一个 Groovy 脚本来执行一些处理,并在最后返回一个对象的实例。例如,如果我们已经有一个名为 JavaMessenger 的 Messenger 实现
<lang:groovy id="messenger">
<![CDATA[
def messenger = new JavaMessenger("Hello World!")
messenger
]]>
</lang:groovy>
这会将一个具有特定消息的 JavaMessenger 实例公开出来——这是一个微不足道的例子,但却是展示此功能的好方法。使用此技术使我们能够超越 Spring 中正常的 Bean 创建模式,并在返回对象之前在脚本中执行任意数量的处理。
在底层,Spring 会创建一个 groovy.util.Script 实例,其 run() 方法在脚本结束时返回对象。当我们开始考虑如何集成 DSL 时,这一点将变得很重要。
为了进入 DSL 领域,我们需要了解的下一个功能是能够在 Groovy 对象作为 Spring 组件公开之前对其进行自定义。我认为此功能是在 Rod Johnson 和 Guillaume Laforge 在接近 Spring 2.0 发布初期的一次会议后添加的(2.0 版本中没有)。Guillaume 对领域特定语言的兴趣促使他观察到,Spring 能够操纵 Groovy 对象(或其类)并在任何人有机会使用它之前为其添加行为,而且由于 Groovy 是一种动态语言,这是一种非常强大的惯用法。
他们想出的机制是 GroovyObjectCustomizer 接口,可以在 Groovy 对象暴露给 Spring 容器客户端之前对其进行应用。该接口看起来像这样
public interface GroovyObjectCustomizer {
void customize(GroovyObject goo);
}
它在 Groovy 对象实例化后(如果它是一个 Script,则在运行之前)应用。这允许我们在对象被释放之前对其方法和属性进行一些操作。
要应用自定义器,我们只需在 Groovy Bean 定义中添加一个对其的引用
<lang:groovy id="messenger" script-source="classpath:..." customizer-ref="customizer"/>
<bean id="customizer" class="..."/>
Grails 为 Spring 组件提供了一个名为 BeanBuilder 的漂亮 DSL(有关更多详细信息,请参阅 此处)。它允许我们以一种自然且简洁的方式用 Groovy 构建一个 Spring ApplicationContext。根据 Graeme Rocher 的说法,在最近版本的 Grails 中,BeanBuilder 也可以在不依赖 Web 框架的情况下工作——您只需要 Grails Core 和 Groovy 在您的类路径中。因此,现在是时候看看我们是否可以将 BeanBuilder 与 Spring 集成了(正如 Spring 论坛 此处 也指出的那样)。(我实际上无法在没有 servlet API 和 Spring webflow jar 的情况下使 Grails 1.0-rc1 示例工作,但它很可能在 rc2 或 1.0 最终版本中工作。)
Groovy 中领域特定语言的表达式通常采用闭包的形式,因此使用 Spring 集成的 Script 模式来定义闭包是很自然的。就 BeanBuilder 而言,它看起来像这样
<lang:groovy id="beans">
<![CDATA[
beans = {
messenger(JavaMessenger) {
message = "Hello World!"
}
// ... more bean definitions here ...
}
]]>
</lang:groovy>
这会生成一个 Script 对象,该对象本身返回一个闭包(称为 "beans"),其中包含 Bean 定义。其中一个 Bean 定义就是我们熟悉的 messenger。我们理想情况下希望能够获取这些 Bean 定义并将它们与当前的 ApplicationContext 合并。为此,我们需要使用 GroovyObjectCustomizer。
public class BeanBuilderClosureCustomizer implements GroovyObjectCustomizer {
public void customize(GroovyObject goo) {
createApplicationContext(goo.run())
}
private ApplicationContext createApplicationContext(Closure value) {
BeanBuilder builder = new BeanBuilder()
builder.beans(value)
builder.createApplicationContext()
}
}
它还没有对它创建的应用程序上下文做任何事情——只是创建它并让它消失。它也没有做任何错误检查,但我们以后可以添加。自定义器是用 Groovy 编写的,因此我们可以直接调用 goo.run() 而无需将其强制转换为 Script。
public class BeanBuilderClosureCustomizer implements GroovyObjectCustomizer {
public void customize(GroovyObject goo) {
addbeanDefinitions(createApplicationContext(goo.run()))
}
private void addBeanDefinitions(ApplicationContext context) {
DefaultListableBeanFactory scriptBeanFactory = context.autowireCapableBeanFactory
for (name in scriptBeanFactory.getBeanDefinitionNames()) {
BeanDefinition definition = scriptBeanFactory.getBeanDefinition(name)
applicationContext.autowireCapableBeanFactory.registerBeanDefinition(name, definition)
}
}
// createAppicationContext defined here....
}
还有什么比这更简单的呢?
将到目前为止的所有内容放在一起,我们可以加载此 Spring 配置
<beans>
<lang:groovy id="beans" customizer-ref="customizer">
<![CDATA[
beans = {
messenger(JavaMessenger) {
message = "Hello World!"
}
// ... more bean definitions here ...
}
]]>
</lang:groovy>
<bean id="customizer" class="BeanBuilderClosureCustomizer"/>
</beans>
然后取出 messenger 并使用它。在示例(请参阅附件)中,我们让 Spring 2.5 TestContextFramework 负责创建 ApplicationContext 并将依赖注入到测试用例中(因此无需进行任何依赖查找)。
为了使我们的 BeanBuilderClosureCustomizer 更有用,作为最后的调整,我们将修改它,使其使用封闭的 ApplicationContext 作为 BeanBuilder 中 Bean 定义的父级。为此,我们只需要在自定义器中有一个父级的引用,因此我们需要实现 ApplicationContextAware 并使用该引用来构造 BeanBuilder
public class BeanBuilderClosureCustomizer implements GroovyObjectCustomizer,
ApplicationContextAware {
def ApplicationContext applicationContext;
public void customize(GroovyObject goo) {
addbeanDefinitions(createApplicationContext(goo.run()))
}
private ApplicationContext createApplicationContext(Closure value) {
BeanBuilder builder = new BeanBuilder(applicationContext)
builder.beans(value)
builder.createApplicationContext()
}
// addBeanDefinitions defined here....
}
由于 BeanBuilderClosureCustomizer 是用 Groovy 编写的,因此我们无需为 applicationContext 属性定义显式的 getter 和 setter——它们由 Groovy 自动生成。
BeanBuilderClosureCustomizer 现在可以使用了(也许还需要一些额外的错误检查)。Groovy 最棒的地方在于它可以编译并作为 JVM 字节码打包在 jar 文件中。所以,我所要做的就是确保生成的类文件在我打包项目时被包含进去。该示例通过将 Groovy Bean 编译到与 Java 编译器使用的相同目标目录中来做到这一点。
在我们的 Groovy DSL 中引用父上下文中的 Bean 也会非常方便。Grails 已经通过在 BeanBuilder DSL 中使用 "ref" 关键字允许我们做到这一点,例如
<lang:groovy id="beans" customizer-ref="customizer">
<![CDATA[
beans = {
messenger(JavaMessenger) {
message = ref("helloMessage")
}
// ... more bean definitions here ...
}
</lang:groovy>
在这里,我们已经从父上下文中的 Bean 定义加载了消息。
要运行示例,只需解压 zip 文件,或者使用 Eclipse 将其导入到现有工作区中(文件 -> 导入... -> 现有项目...)。如果您安装了 Eclipse 的 m2 插件,它应该可以开箱即用。如果没有,您可以使用 m2 Eclipse 插件生成 Eclipse 元数据("mvn eclipse:eclipse")。如果您不使用 Maven 或 Eclipse,那您就自力更生了,但您可以在 pom.xml 中找到顶层项目依赖项。
由于该项目在单元测试中使用 JSR-250 注释进行依赖注入,因此您需要 API 可用。最简单的方法是使用 Java 6 进行运行和编译。例如,在 *NIX 命令行上
$ JAVA_HOME=<path-to-JDK-1.6> mvn clean test
脚注:实际上,我上面说的可以加载包含内联脚本的配置是撒谎——在 Spring 2.5 中由于一个错误而无法工作,该错误已在 2.5.1 中修复(请参阅 JIRA)。解决方法(如示例所示)是使用外部文件来存储脚本。