Chapter 18. Enterprise Java Bean(EJB)集成

18.1. 简介

做为一个轻量级的容器,Spring常被认为EJB是的替代品。我们也相信,对于很多(不一定是绝大多数)应用和情况,相比采用EJB及EJB容器来实现同样的功能,采用Spring作为容器,借助它对事务,ORM和JDBC存取这些领域的良好支持,的确会是一个更好的选择。

不过,需要特别注意的是,使用了Spring并不是说我们就不能用EJB了。实际上,Spring使得访问EJB和实现EJB及其内部功能更加方便。另外,如果通过Spring来访问EJB组件服务,以后就可以在本地EJB组件,远程EJB组件,或者是POJO(简单Java对象)这些变体之间透明地切换实现方式,而不需要改变客户端的代码。

本章,我们来看看Spring是如何帮助我们访问和实现EJB组件的。Spring在访问无状态Session Bean(SLSBs)的时候特别有用,现在我们就由此开始讨论。

18.2. 访问EJB

18.2.1. 概念

为了调用一个本地或者远程无状态session bean上的方法,通常客户端的代码必须进行JNDI查找,获取(本地或远程的)EJB Home对象,然后调用该对象的"create"方法,才能得到实际的(本地或远程的)EJB对象。如此一来,调用的方法比调用EJB组件本身的方法还要多。

为了避免重复的底层代码,很多EJB应用使用了服务定位器(Service Locator)和业务委托(Bussiness Delegate)模式,这样要比在客户端代码中到处都进行JNDI查找要好些,不过它们的常见实现都有严重的缺陷。例如:

  • 通常如果代码依赖于服务定位器或业务代理单件来使用EJB,则很难对其进行测试。

  • 如果只使用了服务定位器模式而不使用业务委托模式,应用程序代码仍然需要调用EJB Home组件的create方法,并且要处理由此产生的异常。这样代码依然存在和EJB API的耦合并感染了EJB编程模型的复杂性。

  • 实现业务委托模式通常会导致大量的冗余代码,因为对于EJB组件的同一个方法,我们不得不编写很多方法去调用它。

Spring的解决方法是允许用户创建并使用只要很少代码量的业务委托代理对象,一般在Spring的容器里配置。不再需要编写额外的服务定位器或JNDI查找的代码,以及手写的业务委托对象里面冗余的方法,除非它们可以带来实质性的好处。

18.2.2. 访问本地的无状态Session Bean(SLSB)

假设有一个web控制器需要使用本地EJB组件。我们将遵循最佳实践经验,使用EJB的业务方法接口(Business Methods Interface)模式,这样,这个EJB组件的本地接口就扩展了非EJB特定的业务方法接口。让我们把这个业务方法接口称为MyComponent。

public interface MyComponent {
    ...
}

使用业务方法接口模式的一个主要原因就是为了保证本地接口和bean的实现类之间的方法签名自动同步。另外一个原因是它使得稍后我们更容易改用基于POJO(简单Java对象)的服务实现方式,只要这样的改变是有意义的。当然,我们也需要实现本地Home接口,并提供一个Bean实现类,用它来实现接口SessionBean和业务方法接口MyComponent。现在为了把我们Web层的控制器和EJB的实现链接起来,我们唯一要写的Java代码就是在控制器上发布一个类型为MyComponent的setter方法。这样就可以把这个引用保存在控制器的一个实例变量中。

private MyComponent myComponent;

public void setMyComponent(MyComponent myComponent) {
    this.myComponent = myComponent;
}

然后我们可以在控制器的任意业务方法里面使用这个实例变量。假设我们现在从Spring容器获得该控制器对象,我们就可以(在同一个上下文中)配置一个LocalStatelessSessionProxyFactoryBean的实例,它将作为EJB组件的代理对象。这个代理对象的配置和控制器属性myComponent的设置是使用一个配置项完成的,如下所示:

<bean id="myComponent"
      class="org.springframework.ejb.access.LocalStatelessSessionProxyFactoryBean">
  <property name="jndiName" value="myComponent"/>
  <property name="businessInterface" value="com.mycom.MyComponent"/>
</bean>

<bean id="myController" class="com.mycom.myController">
  <property name="myComponent" ref="myComponent"/>
</bean>

这些看似简单的代码背后有很多复杂的处理,受益于Spring的AOP框架,你甚至不必知道这些概念,就可以享用它的成果。Bean myComponent的定义创建了一个EJB组件的代理对象,它实现了业务方法接口。这个EJB组件的本地Home对象在启动的时候就被放到了缓存中,所以只需要执行一次JNDI查找。每当EJB组件被调用的时候,这个代理对象就调用本地EJB组件的classname方法,并调用EJB组件上相应的业务方法。

在Bean myController的定义中,控制器类的属性myController的值被设置为上述的EJB代理对象。

这种EJB组件访问机制大大简化了应用程序代码:Web层(或其他EJB客户端)的代码不再依赖于EJB组件的使用。如果我们想把这个EJB的引用替换为一个POJO,或者是模拟对象或其他测试桩架,我们只需要简单地修改Bean myComponent的定义而无需修改一行Java代码,此外,我们也不再需要在应用程序中编写任何JNDI查找或其它EJB相关的代码。

真实应用中的评测和经验表明,这种方法(尽管其中使用了反射来调用目标EJB组件的方法)的性能开销是最小的,通常使用中几乎觉察不出。虽然如此,仍请牢记我们不要过多地调用EJB组件,因为应用服务器中EJB的基础框架毕竟会带来性能损失。

关于JNDI查找有一点需要特别注意。在一个Bean容器中,这个类通常最好用作单件(没理由使之成为原型)。不过,如果这个Bean容器会预先实例化单件(XML ApplicationContext的几种变体就会这样),并在EJB容器加载目标EJB前被加载,我们就可能会遇到问题。因为JNDI查找会在该类的init方法中被执行然后缓存结果,但是此时EJB还没有被绑定到目标位置。解决方案就是不要预先实例化这个工厂对象,而让它在第一次被用到的时候再创建,在XML容器中,这是通过属性lazy-init来控制的。

尽管大部分Spring的用户不会对这些感兴趣,但那些对EJB进行AOP编程工作的用户则想看看LocalSlsbInvokerInterceptor

18.2.3. 访问远程SLSB

基本上访问远程EJB与访问本地EJB差别不大,只是前者使用的是SimpleRemoteStatelessSessionProxyFactoryBean。当然,无论是否使用Spring,远程调用的语义都相同;对于使用场景和错误处理来说,调用另一台计算机上不同虚拟机中对象的方法和本地调用会有所不同。

与不使用Spring方式的EJB客户端相比,Spring的EJB客户端有一个额外的好处。通常如果要想能随意的在本地和远程EJB调用之间切换EJB客户端代码,是会产生问题的。这是因为远程接口的方法需要声明他们抛出的RemoteException方法 ,然后客户端代码必须处理这种异常,但是本地接口的方法却不需要这样。如果要把针对本地EJB的代码改为访问远程EJB,就需要修改客户端代码,增加处理远程异常的代码,反之要么保留这些用不上的远程异常处理代码要么就需要进行修改以去除这些异常处理代码。使用Spring的远程EJB代理,我们就不再需要在业务方法接口和EJB的实现代码中声明要抛出的RemoteException,而是定义一个相似的远程接口,唯一不同就是它抛出的是RemoteException, 然后交给代理对象去动态的协调这两个接口。也就是说,客户端代码不再需要与 RemoteException这个checked exception打交道,实际上在EJB调用中被抛出的RemoteException都将被以unchecked exception RemoteAccessException的方式重新抛出,它是RuntimeException的一个子类。这样目标服务就可以在本地EJB或远程EJB(甚至POJO)之间随意地切换,客户端不需要关心甚至根本不会觉察到这种切换。当然,这些都是可选的,没有什么阻止你在你的业务接口中声明RemoteExceptions异常。

18.3. 使用Spring提供的辅助类实现EJB组件

Spring也提供了一些辅助类来为EJB组件的实现提供便利。它们是为了倡导一些好的实践经验,比如把业务逻辑放在在EJB层之后的POJO中实现,只把事务划分和远程调用这些职责留给EJB。

要实现一个无状态或有状态的Session Bean,或消息驱动Bean,你只需要从AbstractStatelessSessionBeanAbstractStatefulSessionBeanAbstractMessageDrivenBean/AbstractJmsMessageDrivenBean分别继承你的实现类。

考虑这个无状态Session bean的例子:实际上我们把无状态Session Bean的实现委托给一个普通的Java服务对象。业务接口的定义如下:

public interface MyComponent {
    public void myMethod(...);
    ...
}

这是简单Java对象的实现:

public class MyComponentImpl implements MyComponent {
    public String myMethod(...) {
        ...
    }
    ...
}

最后是无状态Session Bean自身:

public class MyComponentEJB extends AbstractStatelessSessionBean
        implements MyComponent {

    MyComponent myComp;

    /**
     * Obtain our POJO service object from the BeanFactory/ApplicationContext
     * @see org.springframework.ejb.support.AbstractStatelessSessionBean#onEjbCreate()
     */
    protected void onEjbCreate() throws CreateException {
        myComp = (MyComponent) getBeanFactory().getBean(
            ServicesConstants.CONTEXT_MYCOMP_ID);
    }

    // for business method, delegate to POJO service impl.
    public String myMethod(...) {
        return myComp.myMethod(...);
    }
    ...
}

缺省情况下,Spring EJB支持类的基类在其生命周期中将创建并加载一个Spring IoC容器供EJB使用(比如像前面获得POJO服务对象的代码)。加载的工作是通过一个策略对象完成的,它是BeanFactoryLocator的子类。 默认情况下,实际使用的BeanFactoryLocator的实现类是ContextJndiBeanFactoryLocator,它根据一个被指定为JNDI环境变量的资源位置来创建一个ApplicationContext对象(对于EJB类,路径是 java:comp/env/ejb/BeanFactoryPath)。如果需要改变BeanFactory或ApplicationContext的载入策略,我们可以在 setSessionContext()方法调用或在具体EJB子类的构造函数中调用setBeanFactoryLocator()方法来覆盖默认使用的 BeanFactoryLocator实现类。具体细节请参考JavaDoc。

如JavaDoc中所述,有状态Session Bean在其生命周期中将会被钝化并重新激活,由于(一般情况下)使用了一个不可串行化的容器实例,不可以被EJB容器保存, 所以还需要手动在ejbPassivateejbActivate 这两个方法中分别调用unloadBeanFactory()loadBeanFactory, 才能在钝化或激活的时候卸载或载入。

有些情况下,要载入ApplicationContext以使用EJB组件,ContextJndiBeanFactoryLocator的默认实现基本上足够了, 不过,当ApplicationContext需要载入多个bean,或这些bean初始化所需的时间或内存 很多的时候(例如Hibernate的SessionFactory的初始化),就有可能出问题,因为 每个EJB组件都有自己的副本。这种情况下,用户会想重载ContextJndiBeanFactoryLocator的默认实现,并使用其它 BeanFactoryLocator的变体,例如ContextSingletonBeanFactoryLocator ,他们可以载入并在多个EJB或者其客户端间共享一个容器。这样做相当简单,只需要给EJB添加类似于如下的代码:

   /**
    * Override default BeanFactoryLocator implementation
    * @see javax.ejb.SessionBean#setSessionContext(javax.ejb.SessionContext)
    */
   public void setSessionContext(SessionContext sessionContext) {
       super.setSessionContext(sessionContext);
       setBeanFactoryLocator(ContextSingletonBeanFactoryLocator.getInstance());
       setBeanFactoryLocatorKey(ServicesConstants.PRIMARY_CONTEXT_ID);
   }

然后需要创建一个名为beanRefContext.xml的bean定义文件。这个文件定义了EJB中所有可能用到的bean工厂(通常以应用上下文的形式)。许多情况下,这个文件只包括一个bean的定义,如下所示(文件businessApplicationContext.xml包括了所有业务服务POJO的bean定义):

<beans>
    <bean id="businessBeanFactory" class="org.springframework.context.support.ClassPathXmlApplicationContext">
        <constructor-arg value="businessApplicationContext.xml" />
    </bean>
</beans>

上例中,常量ServicesConstants.PRIMARY_CONTEXT_ID定义如下:

public static final String ServicesConstants.PRIMARY_CONTEXT_ID = "businessBeanFactory";

BeanFactoryLocator和类ContextSingletonBeanFactoryLocator的更多使用信息请分别查看他们各自的Javadoc文档。