領先一步
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)。解決方法(如示例所示)是使用外部檔案來儲存指令碼。