ITPub博客

首页 > Linux操作系统 > Linux操作系统 > [转载]使用SPRING AOP框架和EJB组件

[转载]使用SPRING AOP框架和EJB组件

原创 Linux操作系统 作者:dinner1007 时间:2019-03-19 17:06:08 0 删除 编辑
使用SPRING AOP框架和EJB组件

摘要

  快速发展的开发人员社区、对各种后端技术(包括JMS、JTA、JDO、 Hibernate、iBATIS等等)的支持,以及(更为重要的)非侵入性的轻量级IoC容器和内置的AOP运行时,这些因素使得Spring Framework对于J2EE应用程序开发十分具有吸引力。Spring托管的组件(POJO)可以与EJB共存,并允许使用AOP方法来处理企业应用 程序中的横切方面?D?D从监控和审计、缓存及应用程序级的安全性开始,直到处理特定于应用程序的业务需求。

  本文将向您介绍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.常见的端点类型
图 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可以提供非常好的性能(请参见“参考资料”部分中有关CMP性能调优的文章)。我们还将使用 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实例传递给它的构造函数。下面给出了可能的定义:

  在这里,我们使用了一个 匿名定义的TradeLocalHome实例,这个实例是使用Spring的JndiObjectFactoryBean从JNDI获得的,然后把它作为 一个构造函数参数注入到tradeManager中。我们还使用了一个FieldRetrievingFactoryBean来避免硬编码 TradeLocalHome的实际JNDI名称,而是从静态的域(在这个例子中为TradeLocalHome.JNDI_NAME)获取它。通常,使 用JndiObjectFactoryBean时声明proxyInterface属性是一个不错的主意,如上面的例子所示。

  还有另一 种简单的方法可以访问会话bean。Spring提供一个LocalStatelessSessionProxyFactoryBean,它允许立刻获得 一个会话bean而无需经过home接口。例如,下面的代码说明了如何使用通过Spring托管的另一个bean中的本地接口访问的 MyComponentImpl会话bean:

  这种方法的优点在于,可以很容易地从本地接口切换到远程接口,只要使用SimpleRemoteStatelessSessionProxyFactoryBean修改Spring上下文中的一处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方法:

  正如您所看到的,上面的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通知它:

... same as tradeManager definition in the above example

  基本上,就是这样了。我们实现了附加的功能而没有修改原始的组件,而且仅使用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实现的例子。

   因为getPrice()方法已经成为几种通知的公共联结点,所以声明一个抽象的getPriceAdvisor bean,然后在yahooFeedCachingAdvisor中对其进行扩展,指定具体的通知CachingAdvice。注意,也可以修改前面的 foreignTradeAdvisor,使其使用同一个getPriceAdvisor父bean。

  现在可以更新yahooFeed bean的定义,并将它包装在一个ProxyFactoryBean中,然后使用yahooFeedCachingAdvisor通知它。例如:

yahooFeedCachingAdvisor

  当请求命中已经保存在缓存中的数据时,上面的修改将极大地提高性能, 但是如果传入多个针对同一个符号的请求,而该符号尚未进入缓存或者已经到期,我们将看到多个并发的请求到达服务提供者,请求同一个符号。对此,存在一种显 而易见的优化,就是中断对同一个符号的所有请求,直到第一个请求完成为止,然后使用第一个请求获得的结果。EJB规范(参见“Programming Restrictions”,2.1版本的25.1.2部分)一般不推荐使用这种方法,因为它对运行在多个JVM上的集群环境不奏效。然而,至少在单个的 节点中这种优化可以改进性能。图2所示的图表对比说明了优化之前和优化之后的情况:

图2. 优化之前和优化之后
图2. 优化之前和优化之后

  该优化也可以实现为通知,并添加在yahooFeed bean中的拦截器链的末端:

...

  实际的拦截器实现应该像下面这样:

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是一种不错的做法。

下载

  sources.zip 包含了本文中使用的所有源代码。如果您希望构建代码,可以遵照README.txt中的指导。

结束语

   本文介绍了一种把J2EE应用程序中的EJB转换为Spring托管组件的方法,以及转换之后可以采用的强大技术。它还给出了几个实际的例子,说明如何 借助于Spring的AOP框架、应用面向方面的方法来扩展J2EE应用程序,并在不修改现有代码的情况下实现新的业务需求。

  在EJB 中使用Spring Framework将减少代码间的耦合,并使许多强大的功能即时生效,从而提高可扩展性和灵活性。这还使得应用程序的单个组件变得更加易于测试,包括新引 入的AOP通知和拦截器,它们用于实现业务功能或者处理非功能性的需求,比如跟踪、缓存、安全性和事务。

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/374079/viewspace-131526/,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2018-08-23

  • 博文量
    734
  • 访问量
    511570