摘要 本文介紹如何在Eclipse中利用Spring框架作為一個平台來創建輕量級的能夠與你的現有J2EE應用程序無縫集成的插件。
一.引言
一般地,企業軟件產品都要求在客戶端具有定制能力,而且當客戶必須修改核心產品的配置來引入他們自己的定制時一般都要求進行更新操作。借助於易於擴展和可升級的高度模塊化的軟件,插件技術能夠提供針對這種典型場所下的完美解決方案。
注釋1-什麼是插件呢?一個插件是使用什麼樣的代碼構成的?
在眾多的定義當中,我認為最好的定義當屬Eclipse工程中所定義的:插件是一種代碼貢獻,它能夠把代碼添加到一個系統中的眾所周知的擴展點處。也就是說,一個插件是一個良好定義的代碼包(例如一個jar文件或目錄),它提供足夠的配置能力來實現在系統中的一個特定的眾所周知的位置插入和激活自身。
插件本身還可以定義另外的其它插件能夠擴展的擴展點。一個擴展點定義了一個語言接口(該插件將提供它的一個實現)和使用該被發現的插件的組件。一個擴展點能夠接受被動態地發現和在運行時刻配置的插件。
借助於一種擁有清晰定義的擴展點的插件環境,核心產品可以自由升級,而且插件本身可以根據獨立的計劃發行和升級。例如,借助於我的開源Classpath助理工程(基於Eclipse的插件框架),我可以按常規來升級我的Eclipse,而且還可以輕松地發行我自己的插件的更新版本。
特別對於Java開發者來說,與現有J2EE組件(參考"注釋2-J2EE組件不是插件嗎?")相比,插件提供了一種更好的升級技術。可以設想你的許多EJB是由不同的開發小組構建的;然後,在了解它們能夠良好工作的情況下,就可以把它們整合到一個應用程序中。一個插件架構應該是允許進行這種級別的組件化的。
注釋2-J2EE組件不是插件嗎?
是的,J2EE組件,例如EJB和Servlet,都不是插件。盡管它們都具有一定程度的"可插入性"(這是指,你能夠交換一個EJB或Servlet實現),但是配置它們並不那麼清晰明快,而且它們缺乏一個插件所具有的容易的升級能力。例如,Servlet無法把代碼與配置結合到一起。因此,盡管你能夠在其自己的jar文件中打包一個servlet實現;但是,此時你往往需要修改web.xml以便servlet容器能夠識別它。
乍看上去,EJB似乎更象插件-它們包含提供有關自己信息的發布描述符。然而,EJB也不是插件,因為,典型情況下,它們都要求外部配置(一種在EAR的application.xml中的引用);並且,典型地,它們在其各自的發布描述符中進行彼此參考。這兩種特征都使一個EJB無法成為"插件式可發布的"。
借助於流行的Spring框架的BeanFactoryPostProcessor接口,開發者可以輕松地創建一個輕量級插件框架。本文正是想討論如何實現這一點;同時,還要向你展示一個使用輕量級插件的工作示例。
二.准備你的插件平台
在你的平台能夠支持可插入的組件前,它需要滿足下列兩個標准:
組件必須是自發現的。你已經了解到J2EE組件不能成為真正插件的准確理由。典型情況下,你應該找到一個需要升級的外部配置文件以便該平台能夠感知新的代碼。
組件必須包含足夠信息以便在應用程序內部集成或配置其本身。
如果你僅是添加一些不需要與系統進行協作的代碼(也就是說,松耦合的),那麼自動發現就是很簡單的。真正的挑戰是結合有緊密集成的自發現。
三.Spring中的自發現功能
事實證明,Spring實際上為支持插件開發作了比較好的准備。Spring已經能夠在若干種bean上下文文件中存儲配置,並且它使得自發現配置文件非常簡單。例如,下面的Spring語句自動發現以ctx.xml結尾的存在於classpath的META-INF/services目錄下的任何文件:
<import resource="classpath*:META-INF/services/*.ctx.xml" />
這種現成的功能正是當構建輕量級插件框架時你要利用的一個特色。
注意,Spring並不關心它自己的代碼自動發現功能。這通常不是一個問題,因為大多數J2EE容器都提供一個lib目錄,存放於這個目錄下的任何jar文件將被自動地添加到classpath中。這意味著,如果你想以jar文件形式捐獻你的代碼的話,那麼在任何一種J2EE容器中實現自發現都會是相當容易的事情。
在一個應用程序服務器外,使用例如ant這樣的工具來實現jar文件的自發現也是非常容易的。下列的Apache Ant XML以一種與一個應用程序服務器類似的方式檢測所有的存在於lib目錄下的jar文件:
<path id="classpath">
<fileset dir="${basedir}/lib">
<include name="**/*.jar"/>
</fileset>
</path>
<target name="start.server" description="launches the server process">
<java classname="platform.bootstrap.Server">
<classpath refid="classpath" />
</java>
</target>
因此,盡管Spring並不直接支持自發現功能,但是通過使用標准技術,你仍然可以使你的代碼容易地實現自發現。這一點,與Spring的能夠自動檢測配置的能力相結合,就可以使你既能夠實現代碼捐獻的目的也能夠使你的代碼在系統中被發現和激活。
四.在Spring中實現自配置
你需要進一步實現的是,使插件具有自配置能力。盡管Spring並不直接支持這種功能,但是,借助於它提供的一些工具,實現這一目標也是相當直接的。實現自配置的關鍵部分是BeanFactoryPostProcessor,這是一個Spring調用的接口(該調用應該是在所有配置被發現和加載到一個內存描述之後,但在創建實際的對象之前發生)。
通過使用BeanFactoryPostProcessor,你可以動態地把所有的bean組合到一起而不必修改原始的文件系統配置。下列代碼是我的BeanFactoryPostProcessor實現的核心部分:PluginBeanFactoryPostProcessor(下載源碼中提供了完整的類):
private String extensionBeanName;//經由spring設置(在此沒有顯示setter)
private String propertyName;//經由spring設置(在此沒有顯示setter)
private String pluginBeanName;//經由spring設置(在此沒有顯示setter)
/*
*(非Javadoc)
*@請參考BeanFactoryPostProcessor#postProcessBeanFactory(ConfigurableListableBeanFactory)
*/
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory)
throws BeansException {
//找到我們希望修改的bean定義
BeanDefinition beanDef =
beanFactory.getBeanDefinition(extensionBeanName);
//在該bean定義中,查找它的屬性並且發現我們將修改的具體屬性。
MutablePropertyValues propValues = beanDef.getPropertyValues();
if ( !propValues.contains(propertyName))
throw new IllegalArgumentException("Cannot find property " +
propertyName + " in bean " + extensionBeanName);
PropertyValue pv = propValues.getPropertyValue(propertyName);
//取出值定義(在我們的情況下,我們僅支持列表風格屬性的更新)
Object prop = pv.getValue();
if ( !(prop instanceof List))
throw new IllegalArgumentException("Property " + propertyName +
" in extension bean " +
extensionBeanName +
" is not an instanceof List.");
//把我們的bean參考添加到列表中。當Spring創建對象
// 並且把它們綁定到一起時,我們的bean現在准備好了.
List l = (List) pv.getValue();
l.add(new RuntimeBeanReference(pluginBeanName));
}
下面展示了配置在Spring中看上去的樣子。首先,在你的核心工程中定義擴展點-它是example.craps.Table的一個實例,其中它的兩個屬性(dice,players)配置以空列表。這是標准的Spring用法:
<beans>
<bean id="extension-point.craps.table"
class="example.craps.Table"
init-method="init">
<property name="dice">
<list>
</list>
</property>
<property name="players">
<list>
</list>
</property>
</bean>
</beans>
現在,你可以使用插件類連同它的Spring上下文(這將是自發現的)打包一個jar文件,並且你可以擁有一個類似如下的配置:
<beans>
<bean id="real-dice" class="example.craps.RealDice" />
<bean class="platform.spring.PluginBeanFactoryPostProcessor">
<property name="extensionBeanName"
value="extension-point.craps.table" />
<property name="propertyName" value="dice" />
<property name="pluginBeanName" value="real-dice" />
</bean>
</beans>
在這個Spring配置中定義了一個example.craps.RealDice的實例,然後,它定義你的PluginBeanFactoryPostProcessor(它被配置以找到extension-point.craps.table bean)。這一實例還會把你的real-dice bean添加到craps表的dice屬性中。
注意,這是本文中真正的焦點所在。這個到Spring的小擴展就是編寫基於插件的組件的所有要求。注意,如果你刪除這個包含該Spring上下文的jar文件,那麼,你還要從extension-point.craps.table bean中"分離"你的bean。然後,把該jar添加回去,並且把它自己綁定到系統中的適當位置。
五.使用輕量級插件進行開發
我常常吃驚於大多數的架構師團隊極少地考慮開發者能否容易地使用他們的框架。其實,EJB就是一種具有學術式優點的極好的例子,但是,其實踐中的開發缺點使其變得極為昂貴。所以,我認為,當選用一種框架實現典型的編碼/構建/調試工作時,先了解一下該框架具有什麼樣的負荷能力和影響是非常重要的。
從這種角度來看,輕量級插件則是相當"無痛苦"的。你可以把每一個插件作為它自己的簡單地依賴於核心產品的jar的可構建工程。這在一種類似於Eclipse這樣的工具(在其中,核心產品具有其自己的Java工程並且每一種插件也都有其自己的)中是很容易建模的。你僅需要一個最終的裝配工程-它依賴於核心產品和包括的各種插件工程。通過使裝配工程依附於核心和插件工程,你的classpath會被自動地正確構建。本文的下載源碼中提供了一個類似這樣的工程。記住,你可以為每一種客戶創建一個裝配工程,從而允許你把不同的插件與不同的客戶相匹配。這種方式與Eclipse恰好吻合-允許在調試期間的增長式編譯和代碼熱交換;這使你的開發進程相當靈活-不必要加入完全妨礙Eclipse的本機Java支持的構建步驟。
六.一切都是插件嗎?
Eclipse的一個根本特征是,一切都是插件(請參考注釋3:Eclipse插件比較)。從系統的初始啟動到Java開發環境,再到在線幫助系統,每一種捐獻代碼(即使不是Java代碼的代碼)都以一種插件形式存在。這種方式具有其優點,但是它規定了一種工業插件開發環境-具有完整的工具,例如管理組件、調試器支持,等等。幸好,Eclipse提供了這些功能,但是,具有這種級別支持的服務器端框架並不存在(據我所知)。
注釋3-Eclipse插件比較
比較於Eclipse插件,我一直把該插件稱作是輕量級的,但是你可能疑惑:憑什麼說它們是輕量級的?其實,我使用術語"輕量級"術語主要是強調,實現一種基於插件的架構的主要優點是相當輕快和簡單的。
Eclipse工程基於一種具有工業強度的插件架構。因此,我認為,把稍微擴展Spring框架功能的插件架構與一種具有豐富特征的插件實現進行比較是很有價值的。
多個類加載器支持
Eclipse工程具有一種復雜的類加載模型,這區別於(但非完全不同於)一種應用程序服務器的使用類加載器層次的方式。既然Eclipse鼓勵第三方進行插件開發,那麼很可能存在具體類的命名和版本沖突問題。
通過不支持同一個類的多個版本,輕量級方法則可以完全避免這個問題。例如,對於我所工作的應用程序來說,這就是一種合理的約束,因為我們主要使用插件來提供一種可信的升級功能。我們只是或多或少控制我們想使用哪些版本和jar文件;因此,我們不需要多個類加載器支持。
Manifest和其它Meta信息
Eclipse插件提供了一種詳細的manifest-它負責不僅提供有關一個插件擴展了哪些擴展點的信息而且還提供有關它如何依賴於其它插件的信息。在運行時刻,你可以浏覽該插件倉庫以發現插件並且遍歷它們的依賴性。Eclipse鼓勵使用一種"懶惰式"插件加載模型。當實現一個擴展點時,你必須顯式地查找擴展它的那個插件;並且,典型地,你僅加載你需要的那些插件。這種方案減少了啟動時間並且能夠防止因加載不用的對象而造成資源浪費。
Meta信息也是很重要的,Eclipse可以使用它來強制實現你的聲明。Eclipse能夠通知你有關丟失的相關性信息,告訴你使用相同的擴展點時何時你有太多或太少的插件,等等。
借助於輕量級插件,你不必擁有一個正常的manifest,它允許你以編程方式存取你依賴的內容,而且所有的你的插件在運行時刻加載,只是你必須自己來進行任何類型的檢查。
如果不使用一種顯式的依賴性列表,那麼,你必須或者把所有你對於第三方的依賴性打包到你的插件jar中或者假定一種第三方類的基本集合總是位於客戶環境中。如果你忘記一些東西,那麼你就會遇到典型的挑戰-確定丟失了哪些jar文件。
在實踐中,我經常需要決定是否要構建第三方庫(如果它是顧客特定的jar,那麼,典型情況下,我都把它們嵌入到這個插件jar中;如果我使用一種標准的開源包,例如Jakarta commons,那麼我會經常把它添加到核心應用程序中)。當輕量級過於"輕量級"並且你不能以一種ad-hoc方式來管理這些類型的依賴性時,作好調用判斷確實更為重要。
至於其它的manifest相關數據,你可以通過提供一種輕量級的manifest來擴展PluginBeanFactoryPostProcessor.java以便跟蹤具有插件的bean。你還可以使用該信息來強制實施一些約束規則。
在服務器端開發中,構建的EJB、JSP/Servlet等組件並不是以真正插件的形式出現的,它們都要求真正的工作以便定義和歸檔一個擴展點。因此,把一切都當作一個插件可能會增加大量工作,因為大多數J2EE工程師可能對此不太熟悉。
我總是試圖把插件作為一種工具來實現特定領域的定制目的。同樣地,利用Spring創建輕量級的與你的現有應用程序和技術無縫接合的插件就成為極其緊迫的任務。注意,在大部分情況下,你的應用程序通常是一種比較獨立的Spring/J2EE應用程序。
另外,你還應該熟悉一些可選擇的插件框架。特別是那些經常在http://jpf.sourceforge.net/上發布的Java插件框架工程。我從來沒有使用這些框架來確定是否它能夠與Spring良好協作以及你的應用程序需要花多大代價來采納它。但是,如果基於Spring的其它插件不太適合你的口味的話,那麼這些框架可能是你的一個不錯的選擇。