国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學(xué)院 > 開發(fā)設(shè)計 > 正文

使用SPRING AOP框架和EJB組件

2019-11-18 16:12:41
字體:
供稿:網(wǎng)友

摘要

  快速發(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)行選擇:

  • 第一類代理基于Java動態(tài)代理,只適用于接口。它是一種標(biāo)準(zhǔn)的Java特性,可提供卓越的性能。
  • 第二類代理可用于目標(biāo)對象沒有實現(xiàn)任何接口的場景,而且這類接口不能被引入(例如,對于遺留代碼的情況)。它基于使用CGLIB庫的運(yùn)行時字節(jié)碼生成。

  對于所代理的對象,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)程或本地的客戶端:

  • 無狀態(tài)的、有狀態(tài)的或?qū)嶓wbean,本地的或遠(yuǎn)程的(基于RMI-IIOP)
  • 監(jiān)聽本地或外部JMS隊列和主題或入站JCA端點的消息驅(qū)動bean(MDB)
  • Servlet(包括Struts或其他終端用戶UI框架、xml-RPC和基于SOAP的接口)

使用SPRING AOP框架和EJB組件(圖一)
圖 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部署描述符。可以在下面的“下載”部分中找到本文中所有示例類的源代碼。

重構(gòu)EJB組件以使用Spring的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)容:

  • 上面的重構(gòu)已經(jīng)為使用高級的Spring功能(也就是依賴性注入和AOP)奠定了基礎(chǔ)。
  • 在沒有修改客戶端API的情況下,我把所有業(yè)務(wù)邏輯都移出外觀會話bean,這就使得這個EJB不懼修改,而且易于測試。
  • 業(yè)務(wù)邏輯現(xiàn)在位于一個無格式Java對象中,只要該Java對象的依賴性不需要JNDI中的資源,就可以在容器外部對其進(jìn)行測試,或者可以使用存根或模仿(mock)來代替這些依賴性。
  • 現(xiàn)在,可以代入不同的tradeManager實現(xiàn),或者修改初始化參數(shù)和相關(guān)組件,而無需修改Java代碼。

  至此,我們已經(jīng)完成了所有準(zhǔn)備步驟,可以開始解決對TradeManager服務(wù)的新需求了。

通知由Spring托管的組件

  在前面的內(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)組件拋出異常時,可以很容易地聲明期望。

測試baseTradeManageryahooFeed期望調(diào)用返回拋出調(diào)用返回拋出結(jié)果t異常1true0.22-false--0.22-2true-e1false---e13truenull-true0.33-0.33-4truenull-truenull-null-5truenull-true-e2-e2


  可以使用這個表更新測試類,使其使用一個涵蓋了所有可能場景的參數(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)化之后的情況:

使用SPRING AOP框架和EJB組件(圖二)
圖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)。

結(jié)束語

  本文介紹了一種把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ù)。

參考資料

  • Spring Framework 項目頁面
  • Inversion of Control Containers and the Dependency Injection pattern,作者M(jìn)artin Fowler
  • Better J2EEing with Spring,作者Peter Braswell (dev2dev,2005年7月)
  • Spring Integration with WebLogic Server,作者Andy Piper、Rod Johnson、Chris Wall和Nick Tran (dev2dev,2005年9月)
  • Peak Performance Tuning of CMP 2.0 Entity Beans in WebLogic Server 8.1 and 9.0,作者 Dmitri Maximovich (dev2dev,2005年11月)
  • Spring-based AOP Caching,作者Pieter Coucke
  • Acegi security項目——為The Spring Framework 提供全面的安全性服務(wù)
  • Wire Hibernate Transactions in Spring,作者Binildas Christudas (ONJava,2005年7月)
  • Implementing Transaction Suspension in Spring,作者Juergen Hoeller (dev2dev,2005年11月)
  • XDoclet
  • Unit testing with mock objects,作者Alexander Chaffee和William Pietri (developerWorks,2005年11月)
  • Concurrent Programming with J2SE 5.0,作者Qusay Mahmoud (Sun Developer Network,2005年3月)
  • Getting to Know Synchronizers——簡要介紹了java.util.concurrent包所支持的同步機(jī)制
  • Java 1.4 backport of JSR 166 (java.util.concurrent)
  • dev2dev EJB 技術(shù)中心
原文出處:Using the Spring AOP Framework with EJB Components http://dev2dev.bea.com/pub/a/2005/12/spring-aop-with-ejb.Html
使用SPRING AOP框架和EJB組件(圖三)
 作者簡介Eugene Kuleshov是一位獨(dú)立顧問。他在軟件設(shè)計和開發(fā)方面具有超過12年的經(jīng)驗,擅長于應(yīng)用程序安全、企業(yè)集成(EAI)和面向消息的中間件。他的客戶包括大型的金融和保險公司。

(出處:http://www.survivalescaperooms.com)



發(fā)表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發(fā)表
主站蜘蛛池模板: 英吉沙县| 长丰县| 红原县| 白山市| 伽师县| 滦南县| 长宁区| 威信县| 外汇| 梧州市| 林周县| 山西省| 陆丰市| 柯坪县| 尤溪县| 新巴尔虎左旗| 玉山县| 乌拉特中旗| 上思县| 庄浪县| 岳西县| 拉孜县| 台江县| 天门市| 兴国县| 庐江县| 雷山县| 定远县| 东乡县| 朔州市| 盐城市| 昌邑市| 晋江市| 黄陵县| 安福县| 浠水县| 本溪市| 普定县| 衡水市| 旬邑县| 嵊州市|