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 社群所有即將舉行的活動。

檢視全部