Spring 動態語言支援與 Groovy DSL

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

自 Spring 2.0 引入 Spring 動態語言支援以來,它一直是 Groovy 的一個有吸引力的整合點,而 Groovy 提供了一個豐富的環境來定義領域特定語言 (DSL)。但是 Spring 參考手冊中的 Groovy 整合範例在範圍上受到限制,並且沒有展示 Spring 中針對 DSL 整合的功能。在本文中,我將展示如何使用這些功能,並以 Grails 發行版中的 Groovy DSL 將 bean 定義添加到現有的 ApplicationContext 作為一個範例。

Groovy Beans

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 物件

我們需要查看的下一個功能是能夠在 Groovy 物件公開為 Spring 組件之前對其進行自訂。我相信,此功能是在 Rod Johnson 和 Guillaume Laforge 在 Spring 2.0 發佈開始時的某次會議上添加的(不在 2.0 中)。Guillaume 對領域特定語言的興趣使他觀察到 Spring 處於一個良好的位置,能夠在任何人有機會使用 Groovy 物件(或其類別)之前,對其進行操作並新增行為,並且由於 Groovy 是一種動態語言,因此這是一種非常強大的慣用語法。

他們提出的機制是 GroovyObjectCustomizer 介面,可以在 Groovy 物件實例化之後以及(如果是 Script)在執行之前將其應用於 Groovy 物件。該介面如下所示

public interface GroovyObjectCustomizer {

	void customize(GroovyObject goo);

}

它允許我們在物件發佈之前對物件的方法和屬性進行操作。

要應用自訂程式,我們只需要在 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 final 中工作。)

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

現在讓我們改進實作,以便我們將 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 中不起作用,因為在 2.5.1 中修復了一個錯誤(請參閱JIRA)。解決方法(如範例中所示)是使用外部檔案來儲存指令碼。

訂閱 Spring 電子報

透過 Spring 電子報保持聯繫

訂閱

取得領先

VMware 提供培訓和認證,以加速您的進度。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看全部