Spring 动态语言支持与 Groovy DSL

工程 | Dave Syer | 2007 年 11 月 29 日 | ...

自从 Spring 2.0 引入 Spring 动态语言支持以来,它一直是 Groovy 的一个极具吸引力的集成点,并且 Groovy 为定义领域特定语言(DSL)提供了一个丰富的环境。但是,Spring 参考手册中关于 Groovy 集成的示例范围有限,并未展示 Spring 中针对 DSL 集成的特性。在本文中,我将展示如何使用这些特性,并以一个示例为例,说明如何使用来自 Grails 发行版的 Groovy DSL 向现有的 ApplicationContext 添加 bean 定义。

Groovy 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 时,这一点将非常重要。

定制 Groovy 对象

为了进入 DSL 领域,我们需要关注的下一个特性是在 Groovy 对象被暴露为 Spring 组件之前对其进行定制的能力。我相信这个特性是在 Spring 2.0 发布初期(2.0 版本中没有)Rod Johnson 和 Guillaume Laforge 在一次会议上会面后添加的。Guillaume 对领域特定语言的兴趣使他观察到,Spring 处于一个有利位置,可以在 Groovy 对象(或其类)被任何人使用之前对其进行操作和添加行为,而且由于 Groovy 是一种动态语言,这是一个相当强大的范例。

他们提出的机制是 GroovyObjectCustomizer 接口,该接口可以在 Groovy 对象实例化之后、运行之前(如果它是一个 Script)应用于该对象。该接口如下所示

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="..."/>

领域特定语言 - BeanBuilder

Grails 有一个很好的针对 Spring 组件的 DSL,称为 BeanBuilder(详见此处)。它允许我们以一种非常自然简洁的方式在 Groovy 中构建 Spring ApplicationContext。据 Graeme Rocher 说,在最近版本的 Grails 中,BeanBuilder 也可以独立于 Web 框架工作——你只需要 classpath 中有 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 对象,该对象本身返回一个包含 bean 定义的闭包(称为 "beans")。其中一个 bean 定义就是我们的朋友 messenger。我们理想中希望能够获取这些 bean 定义并将其与当前的 ApplicationContext 合并。为此,我们将需要使用 GroovyObjectCustomizer。

一个基本的 GroovyObjectCustomizer

这是一个定制器的基本框架,它将从脚本化的 Groovy 对象中获取闭包并从中创建一个应用程序上下文
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 类型。

改进的 GroovyObjectCustomizer

现在让我们改进实现,以便将 BeanBuilder 中的 bean 定义转移到外层的 ApplicationContext 中。
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 编译器相同的目标目录中来实现这一点。

引用父上下文中的 Bean

在我们的 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 将其导入到现有工作区(File->Import...->Existing Projects...)。如果你安装了 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.1 中修复的 bug(参见 JIRA),这在 Spring 2.5 中不起作用。变通方法(如示例所示)是使用外部文件来存储脚本。

获取 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速发展。

了解更多

获取支持

Tanzu Spring 通过一项简单的订阅,提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区所有即将举行的活动。

查看全部