程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> J2EE中使用Spring AOP框架和EJB組件

J2EE中使用Spring AOP框架和EJB組件

編輯:關於JAVA

快速發展的開發人員社區、對各種後端技術(包括JMS、JTA、JDO、Hibernate、iBATIS等等)的支持,以及(更為重要的)非侵入性的輕量級IoC容器和內置的AOP運行時,這些因素使得Spring Framework對於J2EE應用程序開發十分具有吸引力。Spring托管的組件(POJO)可以與EJB共存,並允許使用AOP方法來處理企業應用程序中的橫切方面——從監控和審計、緩存及應用程序級的安全性開始,直到處理特定於應用程序的業務需求。

本文將向您介紹Spring的AOP框架在J2EE應用程序中的實際應用。

簡介

J2EE技術為實現服務器端和中間件應用程序提供了堅實的基礎。J2EE容器(比如BEA WebLogic Server)可以管理系統級的元素,包括應用程序生命周期、安全性、事務、遠程控制和並發性,而且它可以保證為JDBC、JMS和JTA之類的常見服務提供支持。然而,J2EE的龐大和復雜性使開發和測試變得異常困難。傳統的J2EE應用程序通常嚴重依賴於通過容器的JNDI才可用的服務。這意味著需要大量直接的JNDI查找,或者要使用Service Locator模式,後者稍微有所改進。這種架構提高了組件之間的耦合度,並使得單獨測試某個組件成為幾乎不可能實現的事情。您可以閱讀Spring Framework創建者所撰寫的J2EE Development without EJB一書,其中深入分析了這種架構的缺陷。

借助於Spring Framework,可以將使用無格式Java對象實現的業務邏輯與傳統的J2EE基礎架構連接起來,同時極大地減少了訪問J2EE組件和服務所需的代碼量。基於這一點,可以把傳統的OO設計與正交的AOP組件化結合在一起。本文稍後將會演示如何重構J2EE組件以利用Spring托管的Java對象,然後應用一種AOP方法來實現新特性,從而維護良好的組件獨立性和可測試性。

與其他AOP工具相比,Spring提供了AOP功能中的一個有限子集。它的目標是緊密地集成AOP實現與Spring IoC容器,從而幫助解決常見的應用問題。該集成是以非侵入性的方式完成的,它允許在同一個應用程序中混合使用Spring AOP和表現力更強的框架,包括AspectJ。Spring AOP使用無格式Java類,不要求特殊的編譯過程、控制類裝載器層次結構或更改部署配置,而是使用Proxy模式向應該由Spring IoC容器托管的目標對象應用通知。

可以根據具體情況在兩種類型的代理之間進行選擇:

◆第一類代理基於Java動態代理,只適用於接口。它是一種標准的Java特性,可提供卓越的性能。

◆第二類代理可用於目標對象沒有實現任何接口的場景,而且這類接口不能被引入(例如,對於遺留代碼的情況)。它基於使用CGLIB庫的運行時字節碼生成。

對於所代理的對象,Spring允許使用靜態的(方法匹配基於確切名稱或正則表達式,或者是注釋驅動的)或動態的(匹配是在運行時進行的,包括cflow切入點類型)切入點定義指派特定的通知,而每個切入點可以與一條或多條通知關聯在一起。所支持的通知類型有幾種:環繞通知(around advice),前通知(before advice),返回後通知(after returning advice),拋出異常後通知(after throwing advice),以及引入通知(introduction advice)。本文稍後將給出環繞通知的一個例子。想要了解更詳細的信息,可以參考Spring AOP框架文檔。

正如先前提到的那樣,只可以通知由Spring IoC容器托管的目標對象。然而,在J2EE應用程序中,組件的生命周期是由應用服務器托管的,而且根據集成類型,可以使用一種常見的端點類型把J2EE應用程序組件公開給遠程或本地的客戶端:

◆無狀態的、有狀態的或實體bean,本地的或遠程的(基於RMI-IIOP)

◆監聽本地或外部JMS隊列和主題或入站JCA端點的消息驅動bean(MDB)

◆Servlet(包括Struts或其他終端用戶UI框架、XML-RPC和基於SOAP的接口)

圖1:常見的端點類型

要在這些端點上使用Spring的AOP框架,必須把所有的業務邏輯轉移到Spring托管的bean中,然後使用服務器托管的組件來委托調用,或者定義事務劃分和安全上下文。雖然本文不討論事務方面的問題,但是可以在“參考資料”部分中找到相關文章。

我將詳細介紹如何重構J2EE應用程序以使用Spring功能。我們將使用XDoclet的基於JavaDoc的元數據來生成home和bean接口,以及EJB部署描述符。可以在下面的“下載”部分中找到本文中所有示例類的源代碼。

重構EJB組件以使用Spring的EJB類

想像一個簡單的股票報價EJB組件,它返回當前的股票交易價格,並允許設置新的交易價格。這個例子用於說明同時使用Spring Framework與J2EE服務的各個集成方面和最佳實踐,而不是要展示如何編寫股票管理應用程序。按照我們的要求,TradeManager業務接口應該就是下面這個樣子:  

public interface TradeManager {
public static String ID = "tradeManager";
public BigDecimal getPrice(String name);
public void setPrice(String name, BigDecimal price);
}

在設計J2EE應用程序的過程中,通常使用遠程無狀態會話bean作為持久層中的外觀和實體bean。下面的TradeManager1Impl說明了無狀態會話bean中TradeManager接口的可能實現。注意,它使用了ServiceLocator來為本地的實體bean查找home接口。XDoclet注釋用於為EJB描述符聲明參數以及定義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;
}
}

如果要在進行代碼更改之後測試這樣一個組件,那麼在運行任何測試(通常是基於專用的容器內測試框架,比如Cactus或MockEJB)之前,必須要經過構建、啟動容器和部署應用程序這整個周期。雖然在簡單的用例中類的熱部署可以節省重新部署的時間,但是當類模式變動(例如,添加域或方法,或者修改方法名)之後它就不行了。這個問題本身就是把所有邏輯轉移到無格式Java對象中的最好理由。正如您在TradeManager1Impl代碼中所看到的那樣,大量的粘和代碼把EJB中的所有內容組合在一起,而且您無法從圍繞JNDI訪問和異常處理的復制工作中抽身。然而,Spring提供抽象的便利類,可以使用定制的EJB bean對它進行擴展,而無需直接實現J2EE接口。這些抽象的超類允許移除定制bean中的大多數粘和代碼,而且提供用於獲取Spring應用程序上下文的實例的方法。

首先,需要把TradeManager1Impl中的所有邏輯都轉移到新的無格式Java類中,這個新的類還實現了一個TradeManager接口。我們將把實體bean作為一種持久性機制,這不僅因為它超出了本文的討論范圍,還因為WebLogic Server提供了大量用於調優CMP bean性能的選項。在特定的用例中,這些bean可以提供非常好的性能。我們還將使用Spring IoC容器把TradeImpl實體bean的home接口注入到TradeDao的構造函數中,您將從下面的代碼中看到這一點:

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);
}
}
}

現在,可以使用Spring的AbstractStatelessSessionBean抽象類重寫TradeManager1Impl,該抽象類還可以幫助您獲得上面所創建的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);
}
}

現在,EJB把所有調用都委托給在onEjbCreate()方法中從Spring獲得的TradeManager實例,這個方法是在AbstractEnterpriseBean中實現的,它處理所有查找和創建Spring應用程序上下文所需的工作。但是,必須在EJB部署描述符中為EJB聲明BeanFactoryPath env-entry,以便將配置文件和bean聲明的位置告訴Spring。上面的例子使用了XDoclet注釋來生成這些信息。

此外還要注意,我們重寫了setSessionContext()方法,以便告訴AbstractStatelessSessionBean跨所有EJB bean使用Sping應用程序上下文的單個實例。

現在,可以在applicationContext.xml中聲明一個tradeManager bean。基本上需要創建一個上面TradeDao的新實例,把從JNDI獲得的TradeLocalHome實例傳遞給它的構造函數。下面給出了可能的定義:

<?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獲得的,然後把它作為一個構造函數參數注入到tradeManager中。我們還使用了一個FieldRetrievingFactoryBean來避免硬編碼TradeLocalHome的實際JNDI名稱,而是從靜態的域(在這個例子中為TradeLocalHome.JNDI_NAME)獲取它。通常,使用JndiObjectFactoryBean時聲明proxyInterface屬性是一個不錯的主意,如上面的例子所示。

還有另一種簡單的方法可以訪問會話bean。Spring提供一個LocalStatelessSessionProxyFactoryBean,它允許立刻獲得一個會話bean而無需經過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>

這種方法的優點在於,可以很容易地從本地接口切換到遠程接口,只要使用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被設置為false,以支持延遲初始化。

下面,我總結一下到此為止所學習的內容:

◆上面的重構已經為使用高級的Spring功能(也就是依賴性注入和AOP)奠定了基礎。

◆在沒有修改客戶端API的情況下,我把所有業務邏輯都移出外觀會話bean,這就使得這個EJB不懼修改,而且易於測試。

◆業務邏輯現在位於一個無格式Java對象中,只要該Java對象的依賴性不需要JNDI中的資源,就可以在容器外部對其進行測試,或者可以使用存根或模仿(mock)來代替這些依賴性。

◆現在,可以代入不同的tradeManager實現,或者修改初始化參數和相關組件,而無需修改Java代碼。

至此,我們已經完成了所有准備步驟,可以開始解決對TradeManager服務的新需求了。

通知由Spring托管的組件

在前面的內容中,我們重構了服務入口點,以便使用Spring托管的bean。現在,我將向您說明這樣做將如何幫助改進組件和實現新功能。

首先,假定用戶想看到某些符號的價格,而這些價格並非由您的TradeManager組件所托管。換句話說,您需要連接到一個外部服務,以便獲得當前您不處理的所請求符號的當前市場價格。您可以使用雅虎門戶中的一個基於HTTP的免費服務,但是實際的應用程序將連接到提供實時數據的供應商(比如Reuters、Thomson、Bloomberg、NAQ等等)的實時數據更新服務(data feed)。

首先,需要創建一個新的YahooFeed組件,該組件實現了相同的TradeManager接口,然後從雅虎金融門戶獲得價格信息。自然的實現可以使用HttpURLConnection發送一個HTTP請求,然後使用正則表達式解析響應。例如:

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;
}
}

完成這種實現並測試(在容器外部!)之後,就可以把它與其他組件進行集成。傳統的做法是向TradeManager2Impl添加一些代碼,以便檢查getPrice()方法返回的值。這會使測試的次數至少增加一倍,而且要求為每個測試用例設定附加的先決條件。然而,如果使用Spring AOP框架,就可以更漂亮地完成這項工作。您可以實現一條通知,如果初始的TradeManager沒有返回所請求符號的值,該通知將使用YahooFeed組件來獲取價格(在這種情況下,它的值是null,但是也可能會得到一個UnknownSymbol異常)。

要把通知應用到具體的方法,需要在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,這意味著環繞通知的ForeignTradeAdvice應該實現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()調用了一個原始的組件,而且如果它返回null,它將調用另一個在通知創建時作為構造函數參數注入的tradeManager。參見上面foreignTradeAdvisor bean的聲明。

現在可以把在Spring的bean配置中定義的tradeManager重新命名為baseTradeManager,然後使用ProxyFactoryBean把tradeManager聲明為一個代理。新的baseTradeManager將成為一個目標,我們將使用上面定義的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>

基本上,就是這樣了。我們實現了附加的功能而沒有修改原始的組件,而且僅使用Spring應用程序上下文來重新配置依賴性。要想不借助於Spring AOP框架在典型的EJB組件中實現類似的修改,要麼必須為EJB添加附加的邏輯(這會使其難以測試),要麼必須使用decorator模式(實際上增加了EJB的數量,同時也提高了測試的復雜性,延長了部署時間)。在上面的例子中,您可以看到,借助於Spring,可以輕松地不修改現有組件而向這些組件添加附加的邏輯。現在,您擁有的是幾個輕量級組件,而不是緊密耦合的bean,您可以獨立測試它們,使用Spring Framework組裝它們。注意,使用這種方法,ForeignTradeAdvice就是一個自包含的組件,它實現了自己的功能片斷,可以當作一個獨立單元在應用服務器外部進行測試,下面我將對此進行說明。

測試通知代碼

您可能注意到了,代碼不依賴於TradeDao或YahooFeed。這樣就可以使用模仿對象完全獨立地測試這個組件。模仿對象測試方法允許在組件執行之前聲明期望,然後驗證這些期望在組件調用期間是否得到滿足。要了解有關模仿測試的更多信息,請參見“參考資料”部分。下面我們將會使用jMock框架,該框架提供了一個靈活且功能強大的API來聲明期望。

測試和實際的應用程序使用相同的Spring bean配置是個不錯的主意,但是對於特定組件的測試來說,不能使用實際的依賴性,因為這會破壞組件的孤立性。然而,Spring允許在創建Spring的應用程序上下文時指定一個BeanPostProcessor,從而置換選中的bean和依賴性。在這個例子中,可以使用模仿對象的一個Map,這些模仿對象是在測試代碼中創建的,用於置換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創建的。然後,我們就可以創建ClassPathXmlApplicationContext(配置其使用BeanPostProcessor)來實例化一個tradeManager組件。產生的tradeManager組件將使用模仿後的依賴性。

這種方法不僅允許孤立要測試的組件,還可以確保在Spring bean配置中正確定義通知。實際上,要在不模擬大量容器基礎架構的情況下使用這樣的方法來測試在EJB組件中實現的業務邏輯是不可能的:

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()方法也將被調用:

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的參數調用getPrice()方法一次,而且這次調用將返回null。類似地,yahooFeedMock也期望對同一方法只調用一次,但是返回expectedPrice。這允許在setUp()方法中運行所創建的tradeManager組件,並斷言返回的結果。

這個測試用例很容易參數化,從而涵蓋所有可能的用例。注意,當組件拋出異常時,可以很容易地聲明期望。

測試 baseTradeManager yahooFeed 期望 調用 返回 拋出 調用 返回 拋出 結果t 異常 1 true 0.22 - false - - 0.22 - 2 true - e1 false - - - e1 3 true null - true 0.33 - 0.33 - 4 true null - true null - null - 5 true null - true - e2 - e2

可以使用這個表更新測試類,使其使用一個涵蓋了所有可能場景的參數化序列:

...  
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;
}
...

在更復雜的情況下,上面的測試方法可以很容易地擴展為大得多的輸入參數集合,而且它仍然會立刻運行且易於管理。此外,把所有參數移入一個外部配置文件或者甚至Excel電子表格是合理的做法,這些配置文件或電子表格可以由QA團隊管理,或者直接根據需求生成。

組合和鏈接通知

我們已經使用了一個簡單的攔截器通知來實現附加的邏輯,並且將其當作一個獨立的組件進行了測試。當應該在不進行修改並且與其他組件沒有附加耦合的情況下擴展公共執行流時,這種設計十分有效。例如,當價格已經發生變化時,如果需要使用JMS或JavaMail發送通知,我們可以在tradeManager bean的setPrice方法上注冊另一個攔截器,並使用它來向相關組件通知有關這些變化的情況。在很多情況下,這些方面都適用於非功能性需求,比如許多AOP相關的文章和教程中經常用作“hello world”例子的跟蹤、登錄或監控。

另一個傳統的AOP應用程序是緩存。例如,一個基於CMP實體bean的TradeDao組件將從WebLogic Server提供的緩存功能中受益。然而對於YahooFeed組件來說卻並非如此,因為它必須通過Internet連接到雅虎門戶。這明顯是一個應該應用緩存的位置,而且它還允許減少外部連接的次數,並最終降低整個系統的負載。注意,基於截至時間的緩存也會在刷新信息時帶來一些延遲,但是在很多情況下,它仍然是可以接受的。要應用緩存功能,可以定義一個yahooFeedCachingAdvisor,它將把CachingAdvice附加到yahooFeed bean上的getPrice()方法。在“下載”部分中,您可以找到一個CachingAdvice實現的例子。

<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()方法已經成為幾種通知的公共聯結點,所以聲明一個抽象的getPriceAdvisor bean,然後在yahooFeedCachingAdvisor中對其進行擴展,指定具體的通知CachingAdvice。注意,也可以修改前面的foreignTradeAdvisor,使其使用同一個getPriceAdvisor父bean。

現在可以更新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>

當請求命中已經保存在緩存中的數據時,上面的修改將極大地提高性能,但是如果傳入多個針對同一個符號的請求,而該符號尚未進入緩存或者已經到期,我們將看到多個並發的請求到達服務提供者,請求同一個符號。對此,存在一種顯而易見的優化,就是中斷對同一個符號的所有請求,直到第一個請求完成為止,然後使用第一個請求獲得的結果。EJB規范(參見“Programming Restrictions”,2.1版本的25.1.2部分)一般不推薦使用這種方法,因為它對運行在多個JVM上的集群環境不奏效。然而,至少在單個的節點中這種優化可以改進性能。圖2所示的圖表對比說明了優化之前和優化之後的情況:

圖2:優化之前和優化之後

該優化也可以實現為通知,並添加在yahooFeed bean中的攔截器鏈的末端:

...
<property name="interceptorNames">
<list>
<idref local="yahooFeedCachingAdvisor"/>
<idref local="syncPointAdvisor"/>
</list>
</property>

實際的攔截器實現應該像下面這樣:

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];
}
}

可以看出,通知代碼相當簡單,而且不依賴於其他的組件,這使得JUnit測試變得十分簡單。在“參考資料”部分,您可以找到SyncPointAdvice的JUnit測試的完整源代碼。對於復雜的並發場景來說,使用Java 5中java.util.concurrent包的同步機制或者針對老的JVM使用其backport是一種不錯的做法。

結束語

本文介紹了一種把J2EE應用程序中的EJB轉換為Spring托管組件的方法,以及轉換之後可以采用的強大技術。它還給出了幾個實際的例子,說明如何借助於Spring的AOP框架、應用面向方面的方法來擴展J2EE應用程序,並在不修改現有代碼的情況下實現新的業務需求。

在EJB中使用Spring Framework將減少代碼間的耦合,並使許多強大的功能即時生效,從而提高可擴展性和靈活性。這還使得應用程序的單個組件變得更加易於測試,包括新引入的AOP通知和攔截器,它們用於實現業務功能或者處理非功能性的需求,比如跟蹤、緩存、安全性和事務。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved