重构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服务的新需求了。
