快速發(fā)展的開發(fā)人員社區(qū)、對各種后端技術(shù)(包括JMS、JTA、JDO、Hibernate、iBATIS等等)的支持,以及(更為重要的)非侵入性的輕量級IoC容器和內(nèi)置的AOP運(yùn)行時,這些因素使得SPRing Framework對于J2EE應(yīng)用程序開發(fā)十分具有吸引力。Spring托管的組件(POJO)可以與EJB共存,并允許使用AOP方法來處理企業(yè)應(yīng)用程序中的橫切方面——從監(jiān)控和審計、緩存及應(yīng)用程序級的安全性開始,直到處理特定于應(yīng)用程序的業(yè)務(wù)需求。
本文將向您介紹Spring的AOP框架在J2EE應(yīng)用程序中的實際應(yīng)用。
J2EE技術(shù)為實現(xiàn)服務(wù)器端和中間件應(yīng)用程序提供了堅實的基礎(chǔ)。J2EE容器(比如BEA WebLogic Server)可以管理系統(tǒng)級的元素,包括應(yīng)用程序生命周期、安全性、事務(wù)、遠(yuǎn)程控制和并發(fā)性,而且它可以保證為JDBC、JMS和JTA之類的常見服務(wù)提供支持。然而,J2EE的龐大和復(fù)雜性使開發(fā)和測試變得異常困難。傳統(tǒng)的J2EE應(yīng)用程序通常嚴(yán)重依賴于通過容器的JNDI才可用的服務(wù)。這意味著需要大量直接的JNDI查找,或者要使用Service Locator模式,后者稍微有所改進(jìn)。這種架構(gòu)提高了組件之間的耦合度,并使得單獨(dú)測試某個組件成為幾乎不可能實現(xiàn)的事情。您可以閱讀Spring Framework創(chuàng)建者所撰寫的J2EE Development without EJB一書,其中深入分析了這種架構(gòu)的缺陷。
借助于Spring Framework,可以將使用無格式java對象實現(xiàn)的業(yè)務(wù)邏輯與傳統(tǒng)的J2EE基礎(chǔ)架構(gòu)連接起來,同時極大地減少了訪問J2EE組件和服務(wù)所需的代碼量。基于這一點,可以把傳統(tǒng)的OO設(shè)計與正交的AOP組件化結(jié)合在一起。本文稍后將會演示如何重構(gòu)J2EE組件以利用Spring托管的Java對象,然后應(yīng)用一種AOP方法來實現(xiàn)新特性,從而維護(hù)良好的組件獨(dú)立性和可測試性。
與其他AOP工具相比,Spring提供了AOP功能中的一個有限子集。它的目標(biāo)是緊密地集成AOP實現(xiàn)與Spring IoC容器,從而幫助解決常見的應(yīng)用問題。該集成是以非侵入性的方式完成的,它允許在同一個應(yīng)用程序中混合使用Spring AOP和表現(xiàn)力更強(qiáng)的框架,包括aspectJ。Spring AOP使用無格式Java類,不要求特殊的編譯過程、控制類裝載器層次結(jié)構(gòu)或更改部署配置,而是使用Proxy模式向應(yīng)該由Spring IoC容器托管的目標(biāo)對象應(yīng)用通知。
可以根據(jù)具體情況在兩種類型的代理之間進(jìn)行選擇:
對于所代理的對象,Spring允許使用靜態(tài)的(方法匹配基于確切名稱或正則表達(dá)式,或者是注釋驅(qū)動的)或動態(tài)的(匹配是在運(yùn)行時進(jìn)行的,包括cflow切入點類型)切入點定義指派特定的通知,而每個切入點可以與一條或多條通知關(guān)聯(lián)在一起。所支持的通知類型有幾種:環(huán)繞通知(around advice),前通知(before advice),返回后通知(after returning advice),拋出異常后通知(after throwing advice),以及引入通知(introdUCtion advice)。本文稍后將給出環(huán)繞通知的一個例子。想要了解更詳細(xì)的信息,可以參考Spring AOP框架文檔。
正如先前提到的那樣,只可以通知由Spring IoC容器托管的目標(biāo)對象。然而,在J2EE應(yīng)用程序中,組件的生命周期是由應(yīng)用服務(wù)器托管的,而且根據(jù)集成類型,可以使用一種常見的端點類型把J2EE應(yīng)用程序組件公開給遠(yuǎn)程或本地的客戶端:
圖 1.常見的端點類型
要在這些端點上使用Spring的AOP框架,必須把所有的業(yè)務(wù)邏輯轉(zhuǎn)移到Spring托管的bean中,然后使用服務(wù)器托管的組件來委托調(diào)用,或者定義事務(wù)劃分和安全上下文。雖然本文不討論事務(wù)方面的問題,但是可以在“參考資料”部分中找到相關(guān)文章。
我將詳細(xì)介紹如何重構(gòu)J2EE應(yīng)用程序以使用Spring功能。我們將使用XDoclet的基于JavaDoc的元數(shù)據(jù)來生成home和bean接口,以及EJB部署描述符。可以在下面的“下載”部分中找到本文中所有示例類的源代碼。
想像一個簡單的股票報價EJB組件,它返回當(dāng)前的股票交易價格,并允許設(shè)置新的交易價格。這個例子用于說明同時使用Spring Framework與J2EE服務(wù)的各個集成方面和最佳實踐,而不是要展示如何編寫股票管理應(yīng)用程序。按照我們的要求,TradeManager業(yè)務(wù)接口應(yīng)該就是下面這個樣子:
public interface TradeManager { public static String ID = "tradeManager"; public BigDecimal getPrice(String name); public void setPrice(String name, BigDecimal price); }
在設(shè)計J2EE應(yīng)用程序的過程中,通常使用遠(yuǎn)程無狀態(tài)會話bean作為持久層中的外觀和實體bean。下面的TradeManager1Impl說明了無狀態(tài)會話bean中TradeManager接口的可能實現(xiàn)。注意,它使用了ServiceLocator來為本地的實體bean查找home接口。XDoclet注釋用于為EJB描述符聲明參數(shù)以及定義EJB組件的已公開方法。
/** * @ejb.bean * name="org.javatx.spring.aop.TradeManager1" * type="Stateless" * view-type="both" * transaction-type="Container" * * @ejb.transaction type="NotSupported" * * @ejb.home * remote-pattern="{0}Home" * local-pattern="{0}LocalHome" * * @ejb.interface * remote-pattern="{0}" * local-pattern="{0}Local" */public class TradeManager1Impl implements sessionBean, TradeManager { private SessionContext ctx; private TradeLocalHome tradeHome; /** * @ejb.interface-method view-type="both" */ public BigDecimal getPrice(String symbol) { try { return tradeHome.findByPrimaryKey(symbol).getPrice(); } catch(ObjectNotFoundException ex) { return null; } catch(FinderException ex) { throw new EJBException("Unable to find symbol", ex); } } /** * @ejb.interface-method view-type="both" */ public void setPrice(String symbol, BigDecimal price) { try { try { tradeHome.findByPrimaryKey(symbol).setPrice(price); } catch(ObjectNotFoundException ex) { tradeHome.create(symbol, price); } } catch(CreateException ex) { throw new EJBException("Unable to create symbol", ex); } catch(FinderException ex) { throw new EJBException("Unable to find symbol", ex); } } public void ejbCreate() throws EJBException { tradeHome = ServiceLocator.getTradeLocalHome(); } public void ejbActivate() throws EJBException, RemoteException { } public void ejbPassivate() throws EJBException, RemoteException { } public void ejbRemove() throws EJBException, RemoteException { } public void setSessionContext(SessionContext ctx) throws EJBException, RemoteException { this.ctx = ctx; } }
如果要在進(jìn)行代碼更改之后測試這樣一個組件,那么在運(yùn)行任何測試(通常是基于專用的容器內(nèi)測試框架,比如Cactus或MockEJB)之前,必須要經(jīng)過構(gòu)建、啟動容器和部署應(yīng)用程序這整個周期。雖然在簡單的用例中類的熱部署可以節(jié)省重新部署的時間,但是當(dāng)類模式變動(例如,添加域或方法,或者修改方法名)之后它就不行了。這個問題本身就是把所有邏輯轉(zhuǎn)移到無格式Java對象中的最好理由。正如您在TradeManager1Impl代碼中所看到的那樣,大量的粘和代碼把EJB中的所有內(nèi)容組合在一起,而且您無法從圍繞JNDI訪問和異常處理的復(fù)制工作中抽身。然而,Spring提供抽象的便利類,可以使用定制的EJB bean對它進(jìn)行擴(kuò)展,而無需直接實現(xiàn)J2EE接口。這些抽象的超類允許移除定制bean中的大多數(shù)粘和代碼,而且提供用于獲取Spring應(yīng)用程序上下文的實例的方法。
首先,需要把TradeManager1Impl中的所有邏輯都轉(zhuǎn)移到新的無格式Java類中,這個新的類還實現(xiàn)了一個TradeManager接口。我們將把實體bean作為一種持久性機(jī)制,這不僅因為它超出了本文的討論范圍,還因為WebLogic Server提供了大量用于調(diào)優(yōu)CMP bean性能的選項。在特定的用例中,這些bean可以提供非常好的性能(請參見“參考資料”部分中有關(guān)CMP性能調(diào)優(yōu)的文章)。我們還將使用Spring IoC容器把TradeImpl實體bean的home接口注入到TradeDao的構(gòu)造函數(shù)中,您將從下面的代碼中看到這一點:
public class TradeDao implements TradeManager { private TradeLocalHome tradeHome; public TradeDao(TradeLocalHome tradeHome) { this.tradeHome = tradeHome; } public BigDecimal getPrice(String symbol) { try { return tradeHome.findByPrimaryKey(symbol).getPrice(); } catch(ObjectNotFoundException ex) { return null; } catch(FinderException ex) { throw new EJBException("Unable to find symbol", ex); } } public void setPrice(String symbol, BigDecimal price) { try { try { tradeHome.findByPrimaryKey(symbol).setPrice(price); } catch(ObjectNotFoundException ex) { tradeHome.create(symbol, price); } } catch(CreateException ex) { throw new EJBException("Unable to create symbol", ex); } catch(FinderException ex) { throw new EJBException("Unable to find symbol", ex); } }}
現(xiàn)在,可以使用Spring的AbstractStatelessSessionBean抽象類重寫TradeManager1Impl,該抽象類還可以幫助您獲得上面所創(chuàng)建的TradeDao bean的一個Spring托管的實例:
/** * @ejb.home * remote-pattern="TradeManager2Home" * local-pattern="TradeManager2LocalHome" * extends="javax.ejb.EJBHome" * local-extends="javax.ejb.EJBLocalHome" * * @ejb.transaction type="NotSupported" * * @ejb.interface * remote-pattern="TradeManager2" * local-pattern="TradeManager2Local" * extends="javax.ejb.SessionBean" * local-extends="javax.ejb.SessionBean, org.javatx.spring.aop.TradeManager" * * @ejb.env-entry * name="BeanFactoryPath" * value="applicationContext.xml" */ public class TradeManager2Impl extends AbstractStatelessSessionBean implements TradeManager { private TradeManager tradeManager; public void setSessionContext(SessionContext sessionContext) { super.setSessionContext(sessionContext); // make sure there will be the only one Spring bean config setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance()); } public void onEjbCreate() throws CreateException { tradeManager = (TradeManager) getBeanFactory().getBean(TradeManager.ID); } /** * @ejb.interface-method view-type="both" */ public BigDecimal getPrice(String symbol) { return tradeManager.getPrice(symbol); } /** * @ejb.interface-method view-type="both" */ public void setPrice(String symbol, BigDecimal price) { tradeManager.setPrice(symbol, price); }}
現(xiàn)在,EJB把所有調(diào)用都委托給在onEjbCreate()方法中從Spring獲得的TradeManager實例,這個方法是在AbstractEnterpriseBean中實現(xiàn)的,它處理所有查找和創(chuàng)建Spring應(yīng)用程序上下文所需的工作。但是,必須在EJB部署描述符中為EJB聲明BeanFactoryPath env-entry,以便將配置文件和bean聲明的位置告訴Spring。上面的例子使用了XDoclet注釋來生成這些信息。
此外還要注意,我們重寫了setSessionContext()方法,以便告訴AbstractStatelessSessionBean跨所有EJB bean使用Sping應(yīng)用程序上下文的單個實例。
現(xiàn)在,可以在applicationContext.xml中聲明一個tradeManager bean。基本上需要創(chuàng)建一個上面TradeDao的新實例,把從JNDI獲得的TradeLocalHome實例傳遞給它的構(gòu)造函數(shù)。下面給出了可能的定義:
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "spring-beans.dtd"><beans> <bean id="tradeManager" class="org.javatx.spring.aop.TradeDao"> <constructor-arg index="0"> <bean class="org.springframework.jndi.JndiObjectFactoryBean"> <property name="jndiName"> <bean id="org.javatx.spring.aop.TradeLocalHome.JNDI_NAME" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/> </property> <property name="proxyInterface" value="org.javatx.spring.aop.TradeLocalHome"/> </bean> </constructor-arg> </bean></beans>
在這里,我們使用了一個匿名定義的TradeLocalHome實例,這個實例是使用Spring的JndiObjectFactoryBean從JNDI獲得的,然后把它作為一個構(gòu)造函數(shù)參數(shù)注入到tradeManager中。我們還使用了一個FieldRetrievingFactoryBean來避免硬編碼TradeLocalHome的實際JNDI名稱,而是從靜態(tài)的域(在這個例子中為TradeLocalHome.JNDI_NAME)獲取它。通常,使用JndiObjectFactoryBean時聲明proxyInterface屬性是一個不錯的主意,如上面的例子所示。
還有另一種簡單的方法可以訪問會話bean。Spring提供一個LocalStatelessSessionProxyFactoryBean,它允許立刻獲得一個會話bean而無需經(jīng)過home接口。例如,下面的代碼說明了如何使用通過Spring托管的另一個bean中的本地接口訪問的MyComponentImpl會話bean:
<bean id="tradeManagerEjb" class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean"> <property name="jndiName"> <bean id="org.javatx.spring.aop.TradeManager2LocalHome.JNDI_NAME" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/> </property> <property name="businessInterface" value="org.javatx.spring.aop.TradeManager"/></bean>
這種方法的優(yōu)點在于,可以很容易地從本地接口切換到遠(yuǎn)程接口,只要使用SimpleRemoteStatelessSessionProxyFactoryBean修改Spring上下文中的一處bean聲明即可。例如:
<bean id="tradeManagerEjb" class="org.springframework.ejb.access.SimpleRemoteStatelessSessionProxyFactoryBean"> <property name="jndiName"> <bean id="org.javatx.spring.aop.TradeManager2Home.JNDI_NAME" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean"/> </property> <property name="businessInterface" value="org.javatx.spring.aop.TradeManager"/> <property name="lookupHomeOnStartup" value="false"/> </bean>
注意,lookupHomeOnStartup property被設(shè)置為false,以支持延遲初始化。
下面,我總結(jié)一下到此為止所學(xué)習(xí)的內(nèi)容:
至此,我們已經(jīng)完成了所有準(zhǔn)備步驟,可以開始解決對TradeManager服務(wù)的新需求了。
在前面的內(nèi)容中,我們重構(gòu)了服務(wù)入口點,以便使用Spring托管的bean。現(xiàn)在,我將向您說明這樣做將如何幫助改進(jìn)組件和實現(xiàn)新功能。
首先,假定用戶想看到某些符號的價格,而這些價格并非由您的TradeManager組件所托管。換句話說,您需要連接到一個外部服務(wù),以便獲得當(dāng)前您不處理的所請求符號的當(dāng)前市場價格。您可以使用雅虎門戶中的一個基于HTTP的免費(fèi)服務(wù),但是實際的應(yīng)用程序?qū)⑦B接到提供實時數(shù)據(jù)的供應(yīng)商(比如Reuters、Thomson、Bloomberg、NAQ等等)的實時數(shù)據(jù)更新服務(wù)(data feed)。
首先,需要創(chuàng)建一個新的YahooFeed組件,該組件實現(xiàn)了相同的TradeManager接口,然后從雅虎金融門戶獲得價格信息。自然的實現(xiàn)可以使用HttpURLConnection發(fā)送一個HTTP請求,然后使用正則表達(dá)式解析響應(yīng)。例如:
public class YahooFeed implements TradeManager { private static final String SERVICE_URL = "http://finance.yahoo.com/d/quotes.csv?f=k1&s="; private Pattern pattern = Pattern.compile("/"(.*) - (.*)/""); public BigDecimal getPrice(String symbol) { HttpURLConnection conn; String responseMessage; int responseCode; try { URL serviceUrl = new URL(SERVICE_URL+symbol); conn = (HttpURLConnection) serviceUrl.openConnection(); responseCode = conn.getResponseCode(); responseMessage = conn.getResponseMessage(); } catch(Exception ex) { throw new RuntimeException("Connection error", ex); } if(responseCode!=HttpURLConnection.HTTP_OK) { throw new RuntimeException("Connection error "+responseCode+" "+responseMessage); } String response = readResponse(conn); Matcher matcher = pattern.matcher(response); if(!matcher.find()) { throw new RuntimeException("Unable to parse response ["+response+"] for symbol "+symbol); } String time = matcher.group(1); if("N/A".equals(time)) { return null; // unknown symbol } String price = matcher.group(2); return new BigDecimal(price); } public void setPrice(String symbol, BigDecimal price) { throw new UnsupportedOperationException("Can't set price of 3rd party trade"); } private String readResponse(HttpURLConnection conn) { // ... return response; }}
完成這種實現(xiàn)并測試(在容器外部!)之后,就可以把它與其他組件進(jìn)行集成。傳統(tǒng)的做法是向TradeManager2Impl添加一些代碼,以便檢查getPrice()方法返回的值。這會使測試的次數(shù)至少增加一倍,而且要求為每個測試用例設(shè)定附加的先決條件。然而,如果使用Spring AOP框架,就可以更漂亮地完成這項工作。您可以實現(xiàn)一條通知,如果初始的TradeManager沒有返回所請求符號的值,該通知將使用YahooFeed組件來獲取價格(在這種情況下,它的值是null,但是也可能會得到一個UnknownSymbol異常)。
要把通知應(yīng)用到具體的方法,需要在Spring的bean配置中聲明一個Advisor。有一個方便的類叫做NameMatchMethodPointcutAdvisor,它允許通過名稱選擇方法,在本例中還需要一個getPrice方法:
<bean id="yahooFeed" class="org.javatx.spring.aop.YahooFeed"/> <bean id="foreignTradeAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor"> <property name="mappedName" value="getPrice"/> <property name="advice"> <bean class="org.javatx.spring.aop.ForeignTradeAdvice"> <constructor-arg index="0" ref="yahooFeed"/> </bean> </property> </bean>
正如您所看到的,上面的advisor指派了一個ForeignTradeAdvice給getPrice()方法。針對通知類,Spring AOP框架使用了AOP Alliance API,這意味著環(huán)繞通知的ForeignTradeAdvice應(yīng)該實現(xiàn)MethodInterceptor接口。例如:
public class ForeignTradeAdvice implements MethodInterceptor { private TradeManager tradeManager; public ForeignTradeAdvice(TradeManager manager) { this.tradeManager = manager; } public Object invoke(MethodInvocation invocation) throws Throwable { Object res = invocation.proceed(); if(res!=null) return res; Object[] args = invocation.getArguments(); String symbol = (String) args[0]; return tradeManager.getPrice(symbol); }}
上面的代碼使用invocation.proceed()調(diào)用了一個原始的組件,而且如果它返回null,它將調(diào)用另一個在通知創(chuàng)建時作為構(gòu)造函數(shù)參數(shù)注入的tradeManager。參見上面foreignTradeAdvisor bean的聲明。
現(xiàn)在可以把在Spring的bean配置中定義的tradeManager重新命名為baseTradeManager,然后使用ProxyFactoryBean把tradeManager聲明為一個代理。新的baseTradeManager將成為一個目標(biāo),我們將使用上面定義的foreignTradeAdvisor通知它:
<bean id="baseTradeManager" class="org.javatx.spring.aop.TradeDao"> ... same as tradeManager definition in the above example </bean> <bean id="tradeManager" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="org.javatx.spring.aop.TradeManager"/> <property name="target" ref="baseTradeManager"/> <property name="interceptorNames"> <list> <idref local="foreignTradeAdvisor"/> </list> </property> </bean>
基本上,就是這樣了。我們實現(xiàn)了附加的功能而沒有修改原始的組件,而且僅使用Spring應(yīng)用程序上下文來重新配置依賴性。要想不借助于Spring AOP框架在典型的EJB組件中實現(xiàn)類似的修改,要么必須為EJB添加附加的邏輯(這會使其難以測試),要么必須使用decorator模式(實際上增加了EJB的數(shù)量,同時也提高了測試的復(fù)雜性,延長了部署時間)。在上面的例子中,您可以看到,借助于Spring,可以輕松地不修改現(xiàn)有組件而向這些組件添加附加的邏輯。現(xiàn)在,您擁有的是幾個輕量級組件,而不是緊密耦合的bean,您可以獨(dú)立測試它們,使用Spring Framework組裝它們。注意,使用這種方法,F(xiàn)oreignTradeAdvice就是一個自包含的組件,它實現(xiàn)了自己的功能片斷,可以當(dāng)作一個獨(dú)立單元在應(yīng)用服務(wù)器外部進(jìn)行測試,下面我將對此進(jìn)行說明。
您可能注意到了,代碼不依賴于TradeDao或YahooFeed。這樣就可以使用模仿對象完全獨(dú)立地測試這個組件。模仿對象測試方法允許在組件執(zhí)行之前聲明期望,然后驗證這些期望在組件調(diào)用期間是否得到滿足。要了解有關(guān)模仿測試的更多信息,請參見“參考資料”部分。下面我們將會使用jMock框架,該框架提供了一個靈活且功能強(qiáng)大的API來聲明期望。
測試和實際的應(yīng)用程序使用相同的Spring bean配置是個不錯的主意,但是對于特定組件的測試來說,不能使用實際的依賴性,因為這會破壞組件的孤立性。然而,Spring允許在創(chuàng)建Spring的應(yīng)用程序上下文時指定一個BeanPostProcessor,從而置換選中的bean和依賴性。在這個例子中,可以使用模仿對象的一個Map,這些模仿對象是在測試代碼中創(chuàng)建的,用于置換Spring配置中的bean:
public class StubPostProcessor implements BeanPostProcessor { private final Map stubs; public StubPostProcessor( Map stubs) { this.stubs = stubs; } public Object postProcessBeforeInitialization(Object bean, String beanName) { if(stubs.containsKey(beanName)) return stubs.get(beanName); return bean; } public Object postProcessAfterInitialization(Object bean, String beanName) { return bean; }}
在測試用例類的setUp()方法中,我們將使用baseTradeManager和yahooFeed組件的模仿對象來初始化StubPostProcessor,而這兩個組件是使用jMock API創(chuàng)建的。然后,我們就可以創(chuàng)建ClassPathXmlApplicationContext(配置其使用BeanPostProcessor)來實例化一個tradeManager組件。產(chǎn)生的tradeManager組件將使用模仿后的依賴性。
這種方法不僅允許孤立要測試的組件,還可以確保在Spring bean配置中正確定義通知。實際上,要在不模擬大量容器基礎(chǔ)架構(gòu)的情況下使用這樣的方法來測試在EJB組件中實現(xiàn)的業(yè)務(wù)邏輯是不可能的:
public class ForeignTradeAdviceTest extends TestCase { TradeManager tradeManager; private Mock baseTradeManagerMock; private Mock yahooFeedMock; protected void setUp() throws Exception { super.setUp(); baseTradeManagerMock = new Mock(TradeManager.class, "baseTradeManager"); TradeManager baseTradeManager = (TradeManager) baseTradeManagerMock.proxy(); yahooFeedMock = new Mock(TradeManager.class, "yahooFeed"); TradeManager yahooFeed = (TradeManager) yahooFeedMock.proxy(); Map stubs = new HashMap(); stubs.put("yahooFeed", yahooFeed); stubs.put("baseTradeManager", baseTradeManager); ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext(CTX_NAME); ctx.getBeanFactory().addBeanPostProcessor(new StubPostProcessor(stubs)); tradeManager = (TradeManager) proxyFactory.getProxy(); } ...
在實際的testAdvice()方法中,可以為模仿對象指定期望并驗證(例如)baseTradeManager上的getPrice()方法是否返回null,然后yahooFeed上的getPrice()方法也將被調(diào)用:
public void testAdvice() throws Throwable { String symbol = "testSymbol"; BigDecimal eXPectedPrice = new BigDecimal("0.222"); baseTradeManagerMock.expects(new InvokeOnceMatcher()).method("getPrice") .with(new IsEqual(symbol)).will(new ReturnStub(null)); yahooFeedMock.expects(new InvokeOnceMatcher()).method("getPrice") .with(new IsEqual(symbol)).will(new ReturnStub(expectedPrice)); BigDecimal price = tradeManager.getPrice(symbol); assertEquals("Invalid price", expectedPrice, price); baseTradeManagerMock.verify(); yahooFeedMock.verify(); }
這段代碼使用jMock約束來指定,baseTradeManagerMock期望只使用一個等于symbol的參數(shù)調(diào)用getPrice()方法一次,而且這次調(diào)用將返回null。類似地,yahooFeedMock也期望對同一方法只調(diào)用一次,但是返回expectedPrice。這允許在setUp()方法中運(yùn)行所創(chuàng)建的tradeManager組件,并斷言返回的結(jié)果。
這個測試用例很容易參數(shù)化,從而涵蓋所有可能的用例。注意,當(dāng)組件拋出異常時,可以很容易地聲明期望。
可以使用這個表更新測試類,使其使用一個涵蓋了所有可能場景的參數(shù)化序列,:
... public static TestSuite suite() { BigDecimal v1 = new BigDecimal("0.22"); BigDecimal v2 = new BigDecimal("0.33"); RuntimeException e1 = new RuntimeException("e1"); RuntimeException e2 = new RuntimeException("e2"); TestSuite suite = new TestSuite(ForeignTradeAdviceTest.class.getName()); suite.addTest(new ForeignTradeAdviceTest(true, v1, null, false, null, null, v1, null)); suite.addTest(new ForeignTradeAdviceTest(true, null, e1, false, null, null, null, e1)); suite.addTest(new ForeignTradeAdviceTest(true, null, null, true, v2, null, v2, null)); suite.addTest(new ForeignTradeAdviceTest(true, null, null, true, null, null, null, null)); suite.addTest(new ForeignTradeAdviceTest(true, null, null, true, null, e2, null, e2)); return suite; } public ForeignTradeAdviceTest( boolean baseCall, BigDecimal baseValue, Throwable baseException, boolean yahooCall, BigDecimal yahooValue, Throwable yahooException, BigDecimal expectedValue, Throwable expectedException) { super("test"); this.baseCall = baseCall; this.baseWill = baseException==null ? (Stub) new ReturnStub(baseValue) : new ThrowStub(baseException); this.yahooCall = yahooCall; this.yahooWill = yahooException==null ? (Stub) new ReturnStub(yahooValue) : new ThrowStub(yahooException); this.expectedValue = expectedValue; this.expectedException = expectedException; } public void test() throws Throwable { String symbol = "testSymbol"; if(baseCall) { baseTradeManagerMock.expects(new InvokeOnceMatcher()) .method("getPrice").with(new IsEqual(symbol)).will(baseWill); } if(yahooCall) { yahooFeedMock.expects(new InvokeOnceMatcher()) .method("getPrice").with(new IsEqual(symbol)).will(yahooWill); } try { BigDecimal price = tradeManager.getPrice(symbol); assertEquals("Invalid price", expectedValue, price); } catch(Exception e) { if(expectedException==null) { throw e; } } baseTradeManagerMock.verify(); yahooFeedMock.verify(); } public String getName() { return super.getName()+" "+ baseCalled+" "+baseValue+" "+baseException+" "+ yahooCalled+" "+yahooValue+" "+yahooException+" "+ expectedValue+" "+expectedException; } ...
在更復(fù)雜的情況下,上面的測試方法可以很容易地擴(kuò)展為大得多的輸入?yún)?shù)集合,而且它仍然會立刻運(yùn)行且易于管理。此外,把所有參數(shù)移入一個外部配置文件或者甚至Excel電子表格是合理的做法,這些配置文件或電子表格可以由QA團(tuán)隊管理,或者直接根據(jù)需求生成。
我們已經(jīng)使用了一個簡單的攔截器通知來實現(xiàn)附加的邏輯,并且將其當(dāng)作一個獨(dú)立的組件進(jìn)行了測試。當(dāng)應(yīng)該在不進(jìn)行修改并且與其他組件沒有附加耦合的情況下擴(kuò)展公共執(zhí)行流時,這種設(shè)計十分有效。例如,當(dāng)價格已經(jīng)發(fā)生變化時,如果需要使用JMS或JavaMail發(fā)送通知,我們可以在tradeManager bean的setPrice方法上注冊另一個攔截器,并使用它來向相關(guān)組件通知有關(guān)這些變化的情況。在很多情況下,這些方面都適用于非功能性需求,比如許多AOP相關(guān)的文章和教程中經(jīng)常用作“hello world”例子的跟蹤、登錄或監(jiān)控。
另一個傳統(tǒng)的AOP應(yīng)用程序是緩存。例如,一個基于CMP實體bean的TradeDao組件將從WebLogic Server提供的緩存功能中受益。然而對于YahooFeed組件來說卻并非如此,因為它必須通過Internet連接到雅虎門戶。這明顯是一個應(yīng)該應(yīng)用緩存的位置,而且它還允許減少外部連接的次數(shù),并最終降低整個系統(tǒng)的負(fù)載。注意,基于截至?xí)r間的緩存也會在刷新信息時帶來一些延遲,但是在很多情況下,它仍然是可以接受的。要應(yīng)用緩存功能,可以定義一個yahooFeedCachingAdvisor,它將把CachingAdvice附加到y(tǒng)ahooFeed bean上的getPrice()方法。在“下載”部分中,您可以找到一個CachingAdvice實現(xiàn)的例子。
<bean id="getPriceAdvisor" abstract="true" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor"> <property name="mappedName" value="getPrice"/> </bean> <bean id="yahooFeedCachingAdvisor" parent="getPriceAdvisor"> <property name="advice"> <bean class="org.javatx.spring.aop.CachingAdvice"> <constructor-arg index="0" ref="cache"/> </bean> </property> </bean>
因為getPrice()方法已經(jīng)成為幾種通知的公共聯(lián)結(jié)點,所以聲明一個抽象的getPriceAdvisor bean,然后在yahooFeedCachingAdvisor中對其進(jìn)行擴(kuò)展,指定具體的通知CachingAdvice。注意,也可以修改前面的foreignTradeAdvisor,使其使用同一個getPriceAdvisor父bean。
現(xiàn)在可以更新yahooFeed bean的定義,并將它包裝在一個ProxyFactoryBean中,然后使用yahooFeedCachingAdvisor通知它。例如:
<bean id="yahooFeed" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces" value="org.javatx.spring.aop.TradeManager"/> <property name="target"> <bean class="org.javatx.spring.aop.YahooFeed"> </property> <property name="interceptorNames"> <list> <value>yahooFeedCachingAdvisor</value> </list> </property> </bean>
當(dāng)請求命中已經(jīng)保存在緩存中的數(shù)據(jù)時,上面的修改將極大地提高性能,但是如果傳入多個針對同一個符號的請求,而該符號尚未進(jìn)入緩存或者已經(jīng)到期,我們將看到多個并發(fā)的請求到達(dá)服務(wù)提供者,請求同一個符號。對此,存在一種顯而易見的優(yōu)化,就是中斷對同一個符號的所有請求,直到第一個請求完成為止,然后使用第一個請求獲得的結(jié)果。EJB規(guī)范(參見“Programming Restrictions”,2.1版本的25.1.2部分)一般不推薦使用這種方法,因為它對運(yùn)行在多個JVM上的集群環(huán)境不奏效。然而,至少在單個的節(jié)點中這種優(yōu)化可以改進(jìn)性能。圖2所示的圖表對比說明了優(yōu)化之前和優(yōu)化之后的情況:
圖2. 優(yōu)化之前和優(yōu)化之后
該優(yōu)化也可以實現(xiàn)為通知,并添加在yahooFeed bean中的攔截器鏈的末端:
... <property name="interceptorNames"> <list> <idref local="yahooFeedCachingAdvisor"/> <idref local="syncPointAdvisor"/> </list> </property>
實際的攔截器實現(xiàn)應(yīng)該像下面這樣:
public class SyncPointAdvice implements MethodInterceptor { private long DEFAULT_TIMEOUT = 10000L; private Map requests = Collections.synchronizedMap(new HashMap()); public Object invoke(MethodInvocation invocation) throws Throwable { String symbol = (String) invocation.getArguments()[0]; Object[] lock = (Object[]) requests.get(symbol); if(lock==null) { lock = new Object[1]; requests.put(symbol, lock); try { lock[0] = invocation.proceed(); return lock[0]; } finally { requests.remove(symbol); synchronized(lock) { lock.notifyAll(); } } } synchronized(lock) { lock.wait(DEFAULT_TIMEOUT); } return lock[0]; }}
可以看出,通知代碼相當(dāng)簡單,而且不依賴于其他的組件,這使得JUnit測試變得十分簡單。在“參考資料”部分,您可以找到SyncPointAdvice的JUnit測試的完整源代碼。對于復(fù)雜的并發(fā)場景來說,使用Java 5中java.util.concurrent包的同步機(jī)制或者針對老的JVM使用其backport是一種不錯的做法。
sources.zip 包含了本文中使用的所有源代碼。如果您希望構(gòu)建代碼,可以遵照README.txt中的指導(dǎo)。
本文介紹了一種把J2EE應(yīng)用程序中的EJB轉(zhuǎn)換為Spring托管組件的方法,以及轉(zhuǎn)換之后可以采用的強(qiáng)大技術(shù)。它還給出了幾個實際的例子,說明如何借助于Spring的AOP框架、應(yīng)用面向方面的方法來擴(kuò)展J2EE應(yīng)用程序,并在不修改現(xiàn)有代碼的情況下實現(xiàn)新的業(yè)務(wù)需求。
在EJB中使用Spring Framework將減少代碼間的耦合,并使許多強(qiáng)大的功能即時生效,從而提高可擴(kuò)展性和靈活性。這還使得應(yīng)用程序的單個組件變得更加易于測試,包括新引入的AOP通知和攔截器,它們用于實現(xiàn)業(yè)務(wù)功能或者處理非功能性的需求,比如跟蹤、緩存、安全性和事務(wù)。
(出處:http://www.survivalescaperooms.com)
新聞熱點
疑難解答