時(shí)間:2005-09-27
作者:
Peter Braswell本文關(guān)鍵字:
spring,
輕量級(jí),
反向控制,
IoC,
J2EE
摘要
J2EE編程正在變得越來越復(fù)雜。J2EE已經(jīng)發(fā)展為一個(gè)API、復(fù)雜化的編程和配置的復(fù)雜網(wǎng)絡(luò)。為了應(yīng)對(duì)這種復(fù)雜性,新的框架和方法不斷涌現(xiàn)。這些框架高度依賴于一個(gè)稱為IoC(Inversion of Control,反向控制)的概念。本文將探討這種方法的一些特性和優(yōu)點(diǎn),因?yàn)檫@種方法與J2EE編程相關(guān),而且可以使J2EE編程變得更輕松。
簡(jiǎn)介
馬克·吐溫的一句話常被引用:“……關(guān)于我死亡的報(bào)道是一種夸張。”現(xiàn)在已經(jīng)出現(xiàn)了很多關(guān)于.Net的流言,以及認(rèn)為J2EE API的復(fù)雜性無法克服和EJB作為一種組件架構(gòu)即將滅亡的流行極客(geek)文化。從學(xué)術(shù)或者只是想像的立場(chǎng)來看,這沒什么大不了的,但事實(shí)是J2EE/EJB API已經(jīng)經(jīng)歷了一場(chǎng)達(dá)爾文式的進(jìn)化。具有DCOM或CORBA項(xiàng)目經(jīng)驗(yàn)的讀者會(huì)明白我的意思。過去,人們都樂于聽聞EJB組件模型的美好前景。實(shí)際情況是,人們?cè)谂cJ2EE相關(guān)的各個(gè)方面都投入巨大。宣布拋棄以前的所有工作并重新組織,這種想法看起來也許有理,但是它并沒有建立在良好的業(yè)務(wù)洞察力之上。EJB繼續(xù)發(fā)展,而術(shù)語、實(shí)踐和框架也隨之涌現(xiàn)(spring up),它們彌補(bǔ)了J2EE API的不足。我說的不是“Spring出現(xiàn)(up)”,對(duì)吧?
我是一名顧問,職責(zé)是幫助構(gòu)建大型的分布式應(yīng)用程序,而且通常是J2EE應(yīng)用程序。因此,我有機(jī)會(huì)親歷許多項(xiàng)目的整個(gè)生命周期。另外我還能夠?qū)⑽覐囊粋€(gè)剛剛完成的項(xiàng)目中剛剛學(xué)到的東西直接帶入一個(gè)全新的項(xiàng)目。從某種意義上說我的“自然選擇”過程加快了。我可以說最近Spring(更具體地說就是IoC,即反向控制)已經(jīng)越來越多地融入到我的項(xiàng)目中了。在本文中,我將從支持或增強(qiáng)J2EE項(xiàng)目的角度來探討Spring。更確切地講,Spring框架能夠標(biāo)準(zhǔn)化許多J2EE最佳實(shí)踐,還能同類化(homogenize)許多無處不在的J2EE模式。接下來我們將瀏覽Spring龐大體系中的一小部分內(nèi)容,重點(diǎn)介紹(依我淺見)能夠幫助改進(jìn)J2EE應(yīng)用程序的功能。
IoC簡(jiǎn)介
一般來說,IoC是一種管理類之間關(guān)聯(lián)的技術(shù)。沒錯(cuò),就這么簡(jiǎn)單!任何人都不是孤立的,對(duì)于各個(gè)對(duì)象來說也是如此。應(yīng)用程序中的對(duì)象是相互依賴的。通過編程方式來表現(xiàn)這種依賴性通常既冗長(zhǎng)又容易出錯(cuò)。好的IoC框架將聲明式地(通過一個(gè)XML配置文件)而不是編程式地(這種方式的可靠性較差)——串連起應(yīng)用程序之間的相互依賴性。
自由使用接口是IoC開發(fā)的一個(gè)主要方針。接口編程大大提高了應(yīng)用程序的靈活性,從而增強(qiáng)了聲明式的關(guān)聯(lián)。接口實(shí)現(xiàn)是通過IoC配置在運(yùn)行時(shí)聲明的,這樣就能夠在不影響或少影響實(shí)際應(yīng)用程序代碼的情況下“重建(rewire)”關(guān)聯(lián)。這在各種IoC框架中是反復(fù)提及的一個(gè)主題,一般而言,也是應(yīng)該遵循的良好實(shí)踐。
一個(gè)小例子
我喜歡通過例子來更快地理解概念。下面就是運(yùn)用了IoC的一組例子;您將看到,這些例子的復(fù)雜性是逐遞增的。大多數(shù)人在一開始使用IoC容器時(shí)都是利用其依賴注入(inject dependency)功能——即,聲明式地將對(duì)象關(guān)聯(lián)起來。利用IoC有助于創(chuàng)建更整潔的代碼,如有必要重建對(duì)象之間的關(guān)聯(lián),一般來說對(duì)于這些代碼也會(huì)更靈活、更容易。IoC的優(yōu)點(diǎn)遠(yuǎn)不止依賴注入,而其擴(kuò)展功能確是以依賴注入程序?yàn)槠瘘c(diǎn)的。
我們將從構(gòu)建簡(jiǎn)單的依賴注入例子開始。第一個(gè)例子用于闡明已經(jīng)提及的兩個(gè)概念。第一個(gè)概念是IoC在運(yùn)行時(shí)構(gòu)建和關(guān)聯(lián)對(duì)象的能力,第二個(gè)是與接口編碼相結(jié)合而產(chǎn)生的靈活性。首先假定架構(gòu)師遞交了圖1所示的UML。
圖1. 接口可插性
這個(gè)小例子表示一個(gè)溫度測(cè)量系統(tǒng)。幾個(gè)傳感器對(duì)象屬于不同的類型,但都實(shí)現(xiàn)了ProtocolAdapterIfc接口,因此在將它們插入TemperatureSensor對(duì)象時(shí),它們是可互換的。在需要TemperatureSensor時(shí),系統(tǒng)中的某個(gè)實(shí)體必須知道要生成并與該傳感器對(duì)象關(guān)聯(lián)的ProtocolAdapterIfc的具體類型。在本例中,該傳感器可基于命令行參數(shù)、數(shù)據(jù)庫中的行或通過屬性文件進(jìn)行配置。本例還不足以造成挑戰(zhàn)或展示一個(gè)復(fù)雜框架,但它足以闡明IoC基礎(chǔ)。
但是,想象一下:在一個(gè)相當(dāng)復(fù)雜的應(yīng)用程序中這種情況屢屢發(fā)生,而您還希望能動(dòng)態(tài)地——至少要在外部——改變對(duì)象關(guān)聯(lián)。假設(shè)有一個(gè)DummyProtocolAdapter,它總是返回42這個(gè)值,使用它來進(jìn)行測(cè)試。為什么不提供一個(gè)單個(gè)的統(tǒng)一框架?——讓開發(fā)人員能夠依靠該框架,以一種一致的、外部配置的方式建立類之間的關(guān)聯(lián),并且不引起工廠單元素類(factory singleton classe)的異常增加。這聽起來可能沒什么大不了,但它要依賴于IoC的簡(jiǎn)單性。
我們使用一個(gè)TemperatureSensor類,它與一個(gè)實(shí)現(xiàn)ProtocolAdapterIfc接口的類有關(guān)聯(lián)。TemperatureSensor將使用該委托類來獲得溫度值。如UML圖所示,在實(shí)現(xiàn)ProtocolAdapterIfc并且隨后可用于該關(guān)聯(lián)的應(yīng)用程序中有若干個(gè)類。我們將使用IoC框架(在本例中是Spring)來聲明要使用的ProtocolAdaperIfc的實(shí)現(xiàn)。Spring將在運(yùn)行時(shí)建立關(guān)聯(lián)。我們先來看XML代碼,它將實(shí)例化TemperatureSensor對(duì)象并將一個(gè)ProtocolAdapterIfc實(shí)現(xiàn)與它關(guān)聯(lián)起來。該代碼如下所示:
<bean id="tempSensor"
class="yourco.project.sensor.TemperatureSensor">
<property name="sensorDelegate">
<ref bean="sensor"/>
</property>
</bean>
<!-- Sensor to associate with tempSensor -->
<bean id="sensor" class="yourco.project.comm.RS232Adapter"/>
看了這些代碼之后,對(duì)于其目的就應(yīng)該非常清楚了。我們配置Spring來實(shí)例化TemperatureSensor對(duì)象,并將其與RS232Adapter相關(guān)聯(lián),作為實(shí)現(xiàn)ProtocolAdapterIfc接口的類。若想改變已經(jīng)與TemperatureSensor關(guān)聯(lián)的實(shí)現(xiàn),惟一需要更改的就是sensor bean標(biāo)記中的class值。只要實(shí)現(xiàn)了ProtocolAdapterIfc接口,TemperatureSensor就不再關(guān)心關(guān)聯(lián)了什么。
將這應(yīng)用于應(yīng)用程序相當(dāng)簡(jiǎn)單。我們必須先接入Spring框架,將它指向正確的配置文件,然后根據(jù)名稱向Spring索取tempSensor對(duì)象的實(shí)例。下面是相應(yīng)的代碼:
ClassPathXmlApplicationContext appContext =
new ClassPathXmlApplicationContext(
new String[]
{ "simpleSensor.xml" });
BeanFactory bf = (BeanFactory) appContext;
TemperatureSensor ts = (TemperatureSensor)
bf.getBean("tempSensor");
System.out.println("The temp is: "+
ts.getTemperature());
可以看出,這些代碼并不是非常難。首先是啟動(dòng)Spring并指定要使用的配置文件。接下來根據(jù)名稱(tempSensor)引用Bean。Spring使用這樣一種機(jī)制:基于simpleSensor.xml文件的描述創(chuàng)建該對(duì)象并與其他對(duì)象關(guān)聯(lián)。它用于注入依賴性——在本例中,通過將它作為一個(gè)參數(shù)傳遞給sensorDelegate()方法而實(shí)例化RS232Adapter對(duì)象并將其與TemperatureSensor對(duì)象關(guān)聯(lián)。
比較起來,使用編程式Java完成這一任務(wù)也不是很難。如下所示:
TemperatureSensor ts2 = new TemperatureSensor();
ts2.setSensorDelegate(new RS232Adapter());
純粹主義者或許會(huì)認(rèn)為實(shí)際上這是更好的方法。代碼行數(shù)少,并且可讀性可能更強(qiáng)。確實(shí)如此,但這種方法的靈活性要小得多。
- 可以隨意換入和換出不同層中不同對(duì)象的不同實(shí)現(xiàn)。例如,若Web層中的組件需要來自新業(yè)務(wù)對(duì)象的額外的功能,您只需將該業(yè)務(wù)對(duì)象與Web層對(duì)象相關(guān)聯(lián),就像上面TemperatureSensor例子中的做法。它將被“注入”到Web對(duì)象中以隨時(shí)使用。
- 能夠重新配置整個(gè)應(yīng)用程序的結(jié)構(gòu),意味著可以輕松更改數(shù)據(jù)源。比如說,或者為不同的部署場(chǎng)景創(chuàng)建不同的配置文件,或者為測(cè)試場(chǎng)景創(chuàng)建更有用的、不同的配置文件。在測(cè)試場(chǎng)景中可能會(huì)注入實(shí)現(xiàn)接口的模擬對(duì)象,而不注入真正的對(duì)象。稍后我們將介紹一個(gè)這樣的例子。
上面所述的例子可能是依賴注入的最簡(jiǎn)單形式。利用相同的策略,我們不僅能夠關(guān)聯(lián)不同的類,還能夠在類中安裝屬性。諸如字符串、整數(shù)或浮點(diǎn)數(shù)之類的屬性,只要具有JavaBean樣式的存取器,就可以通過Spring配置文件將它們注入類中。我們還可以通過構(gòu)造函數(shù)來創(chuàng)建對(duì)象和安裝屬性或bean引用。其語法只比通過屬性進(jìn)行設(shè)置稍稍復(fù)雜一些。
所有這一切都是利用一種靈活的聲明性配置完成的。無需更改代碼,建立依賴關(guān)聯(lián)的所有艱難任務(wù)都由Spring來完成。
Spring--標(biāo)準(zhǔn)化的定位器模式
我一直將服務(wù)定位器模式視作良好的J2EE規(guī)范的主要組成部分。對(duì)于不熟悉這一術(shù)語的人來說,可以這樣理解它:我們一般認(rèn)為典型的J2EE應(yīng)用程序由若干層組成。通常有Web層、服務(wù)層(EJB、JMS、WS、WLS控件)以及數(shù)據(jù)庫。一般來說,完成某一請(qǐng)求所需的“查找”服務(wù)中都包含了一些方法。Service Locator(服務(wù)定位器)模式認(rèn)為,將這些方法包裝在某種隱藏了生成或查找給定服務(wù)的復(fù)雜性的工廠類中是一個(gè)好主意。這減少了JNDI或只會(huì)造成Web層操作類混亂的其他服務(wù)產(chǎn)品代碼的增加。在Spring出現(xiàn)以前,這通常是由經(jīng)過考驗(yàn)證明可靠的(tried-and-true)Singleton類來實(shí)現(xiàn)的。Singleton/Locator/Factory模式可以描繪為:
圖2. 定位器模式的順序圖
這是對(duì)散布在整個(gè)Web控制器代碼中的增加的JNDI查找代碼的一個(gè)巨大改進(jìn)。它被巧妙地隱藏在工廠內(nèi)部的協(xié)作類中。我們可以使用Spring來改進(jìn)這一術(shù)語。此外,該解決方案將適用于EJB、Web services、異步JMS調(diào)用,甚至還有基于WLS控件的服務(wù)。由Spring實(shí)現(xiàn)的這種定位器模式的變體考慮了業(yè)務(wù)服務(wù)之間的一些抽象化和同質(zhì)性。換句話說,Web控制器的開發(fā)人員真的可以不考慮他們所使用的服務(wù)的種類,一個(gè)類似于“WLS控件”但是更通用的概念。
IoC框架大大改進(jìn)了這種模式的效用,而且實(shí)際上廢除了復(fù)雜而特殊的singleton代碼來實(shí)現(xiàn)它。通過借用上例中引入的概念,我們實(shí)際上無需額外代碼便能構(gòu)建一個(gè)非常強(qiáng)大且無處不在的Service Locator模式。為此,在一開始有一個(gè)簡(jiǎn)單的要求,即Web操作的開發(fā)人員應(yīng)專門處理實(shí)現(xiàn)接口的那些事情。這基本上已經(jīng)通過EJB編程實(shí)現(xiàn),但并不是說Web操作的開發(fā)人員處理的服務(wù)必須通過EJB來實(shí)現(xiàn)。它們可能只是普通Java對(duì)象或Web services。要點(diǎn)是應(yīng)當(dāng)通過接口(這樣實(shí)現(xiàn)能夠換入換出)來編寫服務(wù)程序,并且運(yùn)行時(shí)配置能夠由Spring處理。
Spring之所以非常適合于Service Locator模式,是因?yàn)樗蚨嗷蛏倌軌蚪y(tǒng)一地處理不同類型的對(duì)象。通過少許的規(guī)劃和大量使用IoC,我們多少都能夠以一種通用方式來處理大多數(shù)對(duì)象,而不用管它們的特性(EJB、POJO等等)如何,并且不會(huì)引起Singleton工廠類的增加。這使Web層編程變得更加輕松和靈活。
我們先來看一個(gè)關(guān)于這種模式如何應(yīng)用于EJB的例子。我們都知道使用EJB可能是最復(fù)雜的方法,因?yàn)橐獙⒁粋€(gè)活動(dòng)的引用引入EJB要做很多工作。若使用Spring,建議用EJB接口擴(kuò)展非特定于EJB的業(yè)務(wù)接口。這樣做有兩個(gè)目的:保持兩個(gè)接口自動(dòng)同步,以及幫助保證業(yè)務(wù)服務(wù)對(duì)非EJB實(shí)現(xiàn)是可交換的,以便進(jìn)行測(cè)試或清除(stubbing)。我們可以利用Spring固有的實(shí)用工具來定位和創(chuàng)建EJB實(shí)例,同時(shí)為我們處理所有難以處理的工作。相應(yīng)代碼如下所示:
<bean id="myBizServiceRef"
class="org.springframework.ejb.access.
LocalStatelessSessionProxyFactoryBean">
<property name="jndiName">
<value>myBizComponent</value>
</property>
<property name="businessInterface">
<value>
yourco.project.biz.MyBizInterface
</value>
</property>
</bean>
接下來可以檢索bean并開始使用它,方法如下:
MyBizInterface myService = bf.getBean("myBizServiceRef");
這將返回Spring動(dòng)態(tài)創(chuàng)建并包裝了底層目標(biāo)(在本例中是一個(gè)本地EJB實(shí)例)的一個(gè)對(duì)象。這種方法非常好,因?yàn)樗耆[藏了我們?cè)谔幚鞥JB這一事實(shí)。我們將與一個(gè)實(shí)現(xiàn)簡(jiǎn)單業(yè)務(wù)接口的代理對(duì)象交互。Spring已經(jīng)基于“真正的”業(yè)務(wù)對(duì)象考慮周到地動(dòng)態(tài)生成了該對(duì)象。所包裝的對(duì)象當(dāng)然就是Spring定位和檢索引用所要獲得的本地EJB。此外,您還會(huì)注意到,這種代碼形式與前面用于檢索tempSensor對(duì)象的代碼完全相同。
那么如果我們改變主意,想用普通Java對(duì)象來實(shí)現(xiàn)業(yè)務(wù)組件;或者可能在測(cè)試中,我們想用一個(gè)返回“固定(canned)”響應(yīng)的已清除(stubbed)對(duì)象來替換重量級(jí)EJB,該怎么做呢?利用IoC和Spring,通過更改Spring上下文文件就可輕而易舉地實(shí)現(xiàn)這些目標(biāo)。我們只需使用更常規(guī)一點(diǎn)的東西(如我們?cè)诘谝粋€(gè)Spring例子中所看到的)來替換EJB代理的連接即可:
<bean id="myBizServiceRef"
class="yourco.project.biz.MyStubbedBizService">
</bean>
請(qǐng)注意,我只更改了Spring框架所返回的內(nèi)容的細(xì)節(jié),沒有更改bean id。最后的結(jié)果是業(yè)務(wù)對(duì)象的解決方案未變;它看上去和以前完全一樣:
MyBizInterface myService =
bf.getBean("myBizServiceRef");
最大的區(qū)別顯然是實(shí)現(xiàn)該業(yè)務(wù)接口的對(duì)象現(xiàn)在由一個(gè)普通Java對(duì)象(POJO)支持,并且只是該接口的一個(gè)已清除(stubbed)版本。這給單元測(cè)試或改變業(yè)務(wù)服務(wù)的特性帶來了極大方便,而對(duì)客戶端代碼的影響很小。
使用Spring來標(biāo)準(zhǔn)化異常
Spring的一大貢獻(xiàn)是“模板化”代碼塊。這在純JDBC編程中表現(xiàn)得最為明顯。我們都曾寫過具有下述功能的代碼:
- 創(chuàng)建一個(gè)數(shù)據(jù)庫連接,可以的話從某個(gè)池創(chuàng)建。
- 構(gòu)造一個(gè)查詢字符串并提交。
- 迭代結(jié)果并將數(shù)據(jù)封送到域?qū)ο笾小?
- 處理不同階段出現(xiàn)的大量異常。
- 確保記得編寫finally代碼塊以關(guān)閉連接。
但是各處的這種代碼往往都會(huì)或多或少地有點(diǎn)“樣板化”。一般來說這是有害的,不僅因?yàn)椴恍枰拇a會(huì)增加,還因?yàn)橛行〇|西可能會(huì)遺漏,如非常重要的關(guān)閉連接,如果沒有實(shí)現(xiàn)它,可能導(dǎo)致數(shù)據(jù)資源池的泄漏。
雖然我敢肯定我們都曾多次寫過這類“樣板”代碼,但是將Spring方法和直接的JDBC實(shí)現(xiàn)對(duì)照來看,其結(jié)果將會(huì)有趣而又對(duì)比鮮明。“傳統(tǒng)”的JDBC實(shí)現(xiàn)可能如下:
Connection con = null;
try
{
String url = "jdbc://blah.blah.blah;";
con = myDataSource().getConnection();
Statement stmt = con.createStatement();
String query = "SELECT TYPE FROM SENSORS";
ResultSet rs = stmt.executeQuery(query);
while(rs.next()){
String s = rs.getString("TYPE);
logger.debug(s + " " + n);
}
} catch(SQLException ex)
{
logger.error("SQL ERROR!",ex);
}
finally
{
con.close();
}
對(duì)于該方法要做一些說明。首先,它是有效的!該代碼絕對(duì)不會(huì)出現(xiàn)任何錯(cuò)誤。它會(huì)連接到數(shù)據(jù)庫,并從‘SENSOR’表獲取所需的數(shù)據(jù)。該方法的基本問題源于缺乏抽象化。在大型應(yīng)用程序中,必須反復(fù)剪切和粘貼這段代碼,或者至少會(huì)出現(xiàn)類似的其他情況。較大的問題在于它依賴于編程人員去做“該做的事”。我們都知道,不管數(shù)據(jù)庫操作的結(jié)果是什么,都必須用finally語句來關(guān)閉該數(shù)據(jù)庫。有時(shí)我們忘記做該做的事。我和所有人一樣感到內(nèi)疚!
編寫一個(gè)小框架來解決這一問題會(huì)非常容易。我相信大家也都曾這樣做過,但為何不讓Spring幫我們處理這一問題呢?我不敢保證我能想出一個(gè)更整潔、更優(yōu)雅的解決方案。我們來看Spring框架是如何處理這種樣板JDBC場(chǎng)景的。
Spring支持各種各樣的JDBC、Hibernate、JDO和iBatis模板。模板采用樣板概念,并將它轉(zhuǎn)換為合法的編程術(shù)語。例如,下面的代碼片斷封裝了上面列出的各個(gè)步驟:
DataSource ds = (DataSource) bf.getBean("myDataSource");
JdbcTemplate temp = new JdbcTemplate(ds);
List sensorList = temp.query("select sensor.type FROM sensors",
new RowMapper() {
public Object mapRow(ResultSet rs, int rowNum) throws SQLException;
return rs.getString(1);
}
});
這段簡(jiǎn)短的代碼消除了JDBC編程的冗長(zhǎng),代表了前面提及的樣板的思路。請(qǐng)注意我們使用了Spring的IoC來查找該查詢的數(shù)據(jù)源。Spring還支持對(duì)已檢查異常使用未檢查異常;因此許多已檢查的JDBC異常會(huì)重新映射到通常更有用而且更友好的未檢查異常層次結(jié)構(gòu)中。在Spring的上下文文件中配置該數(shù)據(jù)源類似于下面代碼:
<bean id="myDataSource"
class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName">
<value>org.gjt.mm.mysql.Driver</value>
</property>
<property name="url">
<value>jdbc:mysql://romulus/sensors</value>
</property>
<property name="username">
<value>heater</value>
</property>
<property name="password">
<value>hotshot</value>
</property>
</bean>
在本例中,我們利用Apache commons工具箱配置了一個(gè)基本數(shù)據(jù)源。但并不是說我們只能使用它。我們可以改變配置,使用在JNDI中配置并裝載的數(shù)據(jù)源。Spring提供了一些實(shí)用工具和IoC功能以配置和返回存儲(chǔ)在JNDI中的對(duì)象。例如,若想配置并使用與JNDI上下文關(guān)聯(lián)的數(shù)據(jù)源,則可以輸入以下代碼,替換先前的數(shù)據(jù)源配置:
<bean id="myDataSource"
class="org.springframework.jndi.
JndiObjectFactoryBean">
<property name="tempSensorDS">
<value>ConnectionFactory</value>
</property>
</bean>
該代碼突出了Spring所提供的關(guān)于表格的測(cè)試靈活性。該代碼可以在“容器內(nèi)”運(yùn)行(從JNDI查找數(shù)據(jù)源),經(jīng)過細(xì)微的改動(dòng)之后也可在“容器外”運(yùn)行。
雖然模板化機(jī)制僅適用于某些特定場(chǎng)合,但我們可以泛化這一概念,將它應(yīng)用于更廣泛的場(chǎng)合。例如,一種將已檢查異常轉(zhuǎn)變?yōu)槲礄z查異常、此外還可能為異常處理提供一些中間或統(tǒng)一的異常處理策略的機(jī)制將會(huì)很有幫助。我們還可以使用該機(jī)制將底層的基本異常“軟化”得更合乎人意。我們可以將PacketFrameParityFaultException軟化為CommunicationsUnreliableException。這個(gè)重新映射的較軟的異常表示情況可能并不那么嚴(yán)重,重新請(qǐng)求也是可以的。
Spring已經(jīng)具備了一種類似于包裝EJB調(diào)用(在最后一節(jié)介紹)的機(jī)制,但遺憾的是它并不具備任何“通用”的東西,至少在異常軟化意義上是這樣的。但Spring的確有一個(gè)非常健壯的AOP(面向方面編程)框架,我們可以用它來逼近這種行為。下面是一個(gè)關(guān)于這種軟化適用領(lǐng)域的(公認(rèn)的)精心設(shè)計(jì)的例子。
我們?cè)賮砜纯幢疚那懊嬉呀?jīng)開始探討的一些概念。在第一節(jié)中我們介紹了一個(gè)基于遠(yuǎn)程傳感器的小應(yīng)用程序。現(xiàn)在我們繼續(xù)探討這個(gè)例子。我們將從一個(gè)簡(jiǎn)單的傳感器接口開始介紹,該接口代碼如下:
public interface ProtocolAdapterIfc
{
public Integer getRemoteSensorValue()
throws CommChecksumFault,
CommConnectFailure,
CommPacketSequenceFault;
}
這并沒有什么特別之處。顯然實(shí)現(xiàn)該接口的任何人都會(huì)獲得一個(gè)遠(yuǎn)程值并將它返回給調(diào)用者。在此期間調(diào)用者可能要面對(duì)某些可怕的災(zāi)難,該接口可能拋出的已檢查異常就是例證。
接下來我們來看該接口的一個(gè)實(shí)現(xiàn)程序。實(shí)現(xiàn)ProtocolAdapter的類是CarrierPigeon,其代碼類似于:
public class CarrierPigeon
implements ProtocolAdapterIfc
{
private boolean isTired = true;
private boolean canFlapWings = false;
public Integer getRemoteSensorValue()
throws CommChecksumFault,
CommConnectFailure,
CommPacketSequenceFault
{
if(isTired && !canFlapWings )
{
throw new
CommConnectFailure("I'm Tired!");
}
return new Integer(42);
}
}
為簡(jiǎn)潔起見,這里省略了屬性的getter和setter方法。當(dāng)調(diào)用getRemoteSensorValue()時(shí),CarrierPigeon方法將檢查它是否使用過度以及它還能否執(zhí)行。如果它確是使用過度并且不能執(zhí)行,我們將無法獲得任何值,必須拋出CommConnectionFailure,它是一個(gè)已檢查異常。到現(xiàn)在為止,一直都還不錯(cuò)。但別高興得太早了!我已經(jīng)決定不讓應(yīng)用程序編程人員應(yīng)對(duì)“疲勞的信鴿”。可以將它包裝在某種對(duì)象中,并在達(dá)到目的之前捕獲異常,但必須處理20種不同的傳感器。此外,我更喜歡對(duì)這些東西進(jìn)行適度透明地處理,隨著對(duì)該系統(tǒng)了解的增多,或許還會(huì)更改異常處理程序中的邏輯。我想要的是一個(gè)兼容API,應(yīng)用程序編程人員能夠使用它,并且隨著時(shí)間的推移能夠改變或增強(qiáng)。我想模板化異常處理,并讓應(yīng)用程序編程人員能夠處理軟的未檢查異常,而不是硬異常。Spring非常符合這種情況。下面是為此而使用的策略的要點(diǎn):
- 定義一個(gè)較軟的消除已檢查異常的最小接口。這就是應(yīng)用編程人員將使用的接口。
- 使用Spring AOP結(jié)構(gòu),開發(fā)一個(gè)客戶機(jī)調(diào)用和目標(biāo)對(duì)象調(diào)用之間的攔截器,在本例中是“信鴿”。
- 使用Spring安裝該攔截器并運(yùn)行。
首先來看這個(gè)較軟的接口:
public interface SensorIfc
{
public Integer getSensorValue();
}
請(qǐng)注意,在限定范圍內(nèi)可以重命名方法,使其更有意義。還可以消除已檢查異常,就像這里所做的一樣。接下來,也可能會(huì)更有趣的是我們想要讓Spring將其注入調(diào)用棧的攔截器:
import org.aopalliance.intercept.MethodInterceptor;
public class SensorInvocationInterceptor
implements MethodInterceptor
{
public Object invoke(MethodInvocation
invocationTarget) throws Throwable
{
// Return object reference
Object o = null;
// Convert it to the protocol interface type
ProtocolAdapterIfc pai =
(ProtocolAdapterIfc) invocationTarget.getThis();
try
{
o = pai.getRemoteSensorValue();
}
catch (CommChecksumFault csf)
{
throw new SoftenedProtocolException(
"protocol error [checksum error]: "
+ csf.getMessage());
}
catch (CommConnectFailure cf)
{
throw new SoftenedProtocolException(
"protocol error [comm failure]: "
+ cf.getMessage());
}
catch (CommPacketSequenceFault psf)
{
throw new SoftenedProtocolException(
"protocol error [message sequence error]"
+ psf.getMessage());
}
return o;
}
}
通過實(shí)現(xiàn)Spring MethodInterceptor接口并將該類插入Spring系統(tǒng)(稍后我們將進(jìn)行討論),我們將通過MethodInvocation參數(shù)獲得實(shí)際的目標(biāo)方法。這樣就能提取我們想要的真正對(duì)象。請(qǐng)記住,我們更改了被調(diào)用者看作SensorIfc的接口,該接口使目標(biāo)對(duì)象得以實(shí)現(xiàn)ProtocolAdapterIfc。我們這樣做是為了簡(jiǎn)化調(diào)用,更是為了消除所有的已檢查異常。該攔截器只調(diào)用用來捕獲可能拋出的任何已檢查異常、并用SoftenedProtocolException將它們重新包裝的目標(biāo)方法。實(shí)際上,我們只重新包裝消息,但要做到心中有數(shù)。SoftenedProtocolException擴(kuò)展了RuntimeException,后者無疑是一個(gè)未檢查異常。該操作的最終結(jié)果是應(yīng)用程序開發(fā)人員不必處理任何已檢查異常。如果他們真想處理異常,他們只需(非強(qiáng)制地)處理一個(gè):SoftenedProtocolException。不錯(cuò)吧!
那么,如何才能讓Spring完成這一切呢?答案就是要有正確的配置文件。我們現(xiàn)在就來看一看該配置文件,并了解其工作原理:
<!-- TARGET OBJECT -->
<bean id="protocolAdapter"
class="yourco.project.comm.CarrierPigeon">
<property name="isTired">
<value>true</value>
</property≷
<property name="canFlapWings">
<value>true</value>
</property≷
</bean≷
<!-- INTERCEPTOR -->
<bean id="sensorInterceptor"
class="yourco.project.springsupport.
SensorInvocationInterceptor"/>
<!--WIRE EVERYTHING UP, HAND BACK TO THE USER-->
<bean id="temperatureSensorOne"
class="org.springframework.aop.framework.
ProxyFactoryBean">
<property name="proxyInterfaces">
<value>
yourco.project.interfaces.SensorIfc
</value>
</property>
<property name="target">
<ref local="protocolAdapter"/>
</property>
<property name="interceptorNames">
<list>
<value>sensorInterceptor</value>
</list>
</property>
</bean>
我們逐節(jié)看這些代碼時(shí),可以看到它比我們前面看到的Spring配置文件要稍微復(fù)雜一些。“TARGET OBJECT”注釋下面的第一節(jié)指定了要作為調(diào)用的目標(biāo)對(duì)象的類。還記得攔截器曾將一個(gè)參數(shù)傳入其中來表示目標(biāo)對(duì)象嗎?這就是其工作原理。基于該參數(shù),Spring現(xiàn)在知道要將哪個(gè)對(duì)象傳遞進(jìn)來。“INTERCEPTOR”下面的代碼節(jié)是在目標(biāo)方法之前調(diào)用的類。此時(shí),如果目標(biāo)類拋出已檢查異常,要開始處理異常,并進(jìn)行軟化。Spring將它與目標(biāo)類相關(guān)聯(lián)。最后一節(jié)是將各種元素聯(lián)系起來。用戶要請(qǐng)求的bean位于bean鍵temperatureSensorOne下。ProxyFactoryBean將生成并返回一個(gè)代理類,它用來實(shí)現(xiàn)yourco.project.interfaces.SensorIfc接口。目標(biāo)對(duì)象當(dāng)然就是protocolAdapter bean,它由yourco.project.comm.CarrierPigeon的實(shí)例支持。只要用戶調(diào)用代理的方法就插入并調(diào)用的攔截器位于bean鍵sensorInterceptor下,并由yourco.project.springsupport.SensorInvocationInterceptor支持。請(qǐng)注意,可使用多個(gè)攔截器,因?yàn)樵搶傩允且粋€(gè)列表,因此可以將許多攔截器對(duì)象關(guān)聯(lián)到方法調(diào)用中,這是一個(gè)非常有用的理念。
運(yùn)行該應(yīng)用程序時(shí),根據(jù)插入的CarrierPigeon值,我們可以看到一些有趣的行為。如果我們的CarrierPigeon沒有使用過度并能執(zhí)行,我們將看到這樣的輸出:
The sensor says the temp is:42
顯然“信鴿”沒問題而且狀況很好,并計(jì)算出溫度為42。如果由于改變CarrierPigeon Spring節(jié)中的值,而造成“信鴿”使用過度或不能執(zhí)行,我們將得到如下所示的結(jié)果:
yourco.project.exceptions.comm.SoftenedProtocolException: protocol
error [comm failure]: I'm Tired!
at yourco.project.springsupport.SensorInvocationInterceptor.invoke
(SensorInvocationInterceptor.java:57)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed
(ReflectiveMethodInvocation.java:144)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke
(JdkDynamicAopProxy.java:174)
at .getSensorValue(Unknown Source)
at yourco.project.main.Main.main(Main.java:32)
在這種情況下,“信鴿”或者使用過度,或者不能執(zhí)行,因此得到的結(jié)果是SoftProtocolException,并附帶一條消息說明發(fā)生的情況:“I'm tired!”夠酷吧!
我希望人們能夠開始了解Spring的強(qiáng)大及其多種功能。Spring框架悄然興起,并成為開發(fā)人員的編程工具箱中的真正的“瑞士軍刀”。Spring在實(shí)現(xiàn)讓開發(fā)人員能夠集中于應(yīng)用程序的基本組成部分這一傳說般的承諾方面做得不錯(cuò),這也正是它得以大行其道的業(yè)務(wù)邏輯。Spring將您從J2EE中的一些錯(cuò)綜復(fù)雜的方面中解放出來。Spring的強(qiáng)大在于它能夠使大多數(shù)東西看起來就如同非常普通的Java對(duì)象一樣,而不管它們的性質(zhì)或來源如何。既然Spring自身肩負(fù)起創(chuàng)建、關(guān)聯(lián)和配置的重?fù)?dān),那么開發(fā)人員要做的只是掌握使用對(duì)象而不是構(gòu)造對(duì)象的方法。Spring就如同在雜貨店購買的預(yù)煮的飯菜。您要做的只是決定想吃什么、把它帶回家、加熱,然后吃!
綜合說明
Spring是一個(gè)非常健壯的輕量級(jí)框架,它極好地彌補(bǔ)了J2EE/EJB環(huán)境的不足。Spring真正偉大的一點(diǎn)在于它不走極端。您可以以一種非常簡(jiǎn)單的方式開始使用Spring(正如我所做的那樣),只是建立常見的關(guān)聯(lián),作為定位器模式的一種實(shí)現(xiàn)。稍后,您將發(fā)現(xiàn)其驚人的功能,并且很快對(duì)它的要求會(huì)越來越多。
我所發(fā)現(xiàn)的一個(gè)驚人之處是Spring所提供的測(cè)試靈活性。現(xiàn)在人們對(duì)通過Junit進(jìn)行單元測(cè)試日益重視,這增加了測(cè)試,而不是減少測(cè)試。J2EE容器的使用使測(cè)試極端復(fù)雜,以至于難以進(jìn)行。這種困難局面源于業(yè)務(wù)邏輯和容器框架服務(wù)之間產(chǎn)生的耦合。借助于Spring的配置機(jī)制,使實(shí)現(xiàn)可以動(dòng)態(tài)切換,這樣就有助于將業(yè)務(wù)對(duì)象從容器中釋放出來。我們已經(jīng)看到,如果只想測(cè)試Web組件,將活動(dòng)的EJB換成其代理或一個(gè)已清除的業(yè)務(wù)服務(wù)并不會(huì)造成太大影響。借助于JDBC或Hibernate數(shù)據(jù)訪問對(duì)象,我們可以使用常見的簡(jiǎn)單JDBC、非XA的數(shù)據(jù)源連接來測(cè)試這些組件,并將它們無縫地?fù)Q出,代之以健壯的基于JTA、JNDI的對(duì)應(yīng)連接。結(jié)論是:如果代碼易于測(cè)試,并因此測(cè)試得更多,那么質(zhì)量必然會(huì)提高。
結(jié)束語
本文粗略地概括介紹了IoC,并詳細(xì)介紹了Spring。Spring框架具有許多功能,其中許多功能在本文中只是點(diǎn)到為止。從基本的依賴注入到復(fù)雜的AOP操作,這些組成了Spring的強(qiáng)大功能,這是它的主要優(yōu)點(diǎn)之一。能夠根據(jù)問題需要使用或多或少的IoC功能是一個(gè)極具吸引力的理念,我認(rèn)為,這在通常錯(cuò)綜復(fù)雜的J2EE編程領(lǐng)域也是頗受歡迎的。現(xiàn)在看完了這篇文章,我衷心地希望它能對(duì)您有所幫助,并可以應(yīng)用到您的工作中。歡迎隨時(shí)提供反饋!
參考資料
原文出處
http://dev2dev.bea.com/pub/a/2005/07/better_j2eeing.html
作者簡(jiǎn)介 |
|
Peter Braswell是ALTERThought的首席科學(xué)家。Peter具備超過15年的軟件工程方面的專業(yè)經(jīng)驗(yàn)。他曾領(lǐng)導(dǎo)多個(gè)行業(yè)的前沿性任務(wù)關(guān)鍵型系統(tǒng)和應(yīng)用程序的實(shí)現(xiàn)工作,這些行業(yè)包括高科技、金融、零售和電信等。 |