快速发展的开发人员社区、对各种后端技术(包括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通知和拦截器,它们用于实现业务功能或者处理非功能性的需求,比如跟踪、缓存、安全性和事务。