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组件之前自定义Groovy对象。我相信,这个功能是在Spring 2.0发布之初的一次会议上,Rod Johnson和Guillaume Laforge在会议上提出的(它不在2.0中)。Guillaume对领域特定语言的兴趣促使他注意到,Spring处于一个有利的地位,能够在任何人有机会使用Groovy对象(或其类)之前操作和添加行为,并且由于Groovy是一种动态语言,这是一种非常强大的习惯用法。

他们提出的机制是GroovyObjectCustomizer接口,它可以在将Groovy对象公开给Spring容器客户端之前应用于该对象。该接口如下所示

public interface GroovyObjectCustomizer {

	void customize(GroovyObject goo);

}

它在实例化后以及(如果它是脚本)在运行之前应用于Groovy对象。这允许我们在对象发布之前使用它的方法和属性进行操作。

要应用自定义器,我们只需在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框架的情况下工作——您只需要Grails Core和Groovy在您的类路径中。所以现在是看看我们能否将BeanBuilder与Spring集成的好时机(正如Spring论坛此处所指出的那样)。(实际上,我无法在没有servlet API和Spring webflow jar的情况下使用Grails 1.0-rc1使示例工作,但它可能在rc2或1.0最终版中工作。)

Groovy中领域特定语言中的表达式通常采用闭包的形式,因此使用Spring集成的脚本模式来定义闭包是很自然的。对于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()
	}

}

它还没有对它创建的应用程序上下文执行任何操作——只是创建它并让它消失。它也没有进行任何错误检查,但我们稍后可以添加。

改进的GroovyObjectCustomizer

现在让我们改进实现,以便我们将bean定义从BeanBuilder转移到封闭的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将其导入现有工作区(文件->导入...->现有项目...)。如果您有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中已修复的错误(请参见JIRA),它在Spring 2.5中不起作用。解决方法(如示例所示)是使用外部文件来存储脚本。

获取Spring通讯

通过Spring通讯保持联系

订阅

领先一步

VMware提供培训和认证,以加快您的进步。

了解更多

获得支持

Tanzu Spring在一个简单的订阅中提供对OpenJDK™、Spring和Apache Tomcat®的支持和二进制文件。

了解更多

即将举行的活动

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

查看全部