Chapter 18. Enterprise Java Beans (EJB) 集成

18.1. 简介

做为一个轻量级的容器,Spring常被认为是EJB的替代品。我们也相信,对于很多(甚至是绝大多数)应用和情况,采用Spring作为容器,并借助它对事务、ORM和JDBC访问等的支持,是一种比采用EJB及EJB容器以实现同样的功能更好的选择。

然而,需要特别注意的是,使用了Spring并不是说我们就不能用EJB了。实际上,Spring使得访问和实现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="myJndiComponent"/>
  <property name="businessInterface" value="com.mycom.MyComponent"/>
</bean>

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

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

The myController bean definition sets the myComponent property of the controller class to the EJB proxy.

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

另一种选择(在这样的代理定义很多时,更可取)是使用在Spring的"jee"命名空间下的local-slsb配置元素

<jee:local-slsb id="myComponent" jndi-name="myJndiComponent"
      business-interface="com.mycom.MyComponent"/>

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

这种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或者jee:remote-slsb。当然,无论是否使用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.2.4. Accessing EJB 2.x SLSBs versus EJB 3 SLSBs

通过Spring可以透明的访问EJB2.X和EJB3的 Session bean。Spring的EJB访问器包含jee:local-slsbjee:remote-slsb,可在运行时无缝连接实际组件。 如果是EJB 2.x形式的,这些访问器会调用home接口,如果是EJB3形式的并且没有可用的home接口,就会直接调用组件。

注意:即使是EJB3 session bean,你也完全可以使用JndiObjectFactoryBean / jee:jndi-lookup 因为很多可用的组件引用很多是暴露为JNDI查找的。显式的定义jee:local-slsb / jee:remote-slsb查找提供了一致性和更清楚的EJB访问配置。

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

18.3.1. EJB 2.x base classes

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 MyFacadeEJB extends AbstractStatelessSessionBean
        implements MyFacadeLocal {

    private 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 myFacadeMethod(...) {
        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文档。

18.3.2. EJB 3 注入拦截

对EJB3 Session bean和Message-Driven Bean来说, Spring在EJB组件类 org.springframework.ejb.interceptor.SpringBeanAutowiringInterceptor中提供了实用的拦截器来解析Spring2.5的注解@Autowired。 这个拦截器的使用有两种方式,可以在EJB组件类里使用<code>@Interceptors</code>注解,也可以在EJB部署描述文件中使用XML元素interceptor-binding

@Stateless
@Interceptors(SpringBeanAutowiringInterceptor.class)
public class MyFacadeEJB implements MyFacadeLocal {

    // automatically injected with a matching Spring bean
    @Autowired
    private MyComponent myComp;

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

SpringBeanAutowiringInterceptor 默认情况下是从ContextSingletonBeanFactoryLocator获得目标bean的,后者定义在beanRefContext.xml文件中。通常情况下,最好使用单独的上下文定义,并且根据类型而不是名称来获得。然而,如果你需要在多个上下文定义中切换,那么就需要一个特定的定位键。这个定位键(例如定义在beanRefContext.xml中的上下文名称)可以通过以下两种方式来明确的指定。一种方式是在定制的SpringBeanAutowiringInterceptor子类中重写getBeanFactoryLocatorKey方法。

另一种方式是重写SpringBeanAutowiringInterceptorgetBeanFactory 方法,例如从定制支持类中获得一个共享的ApplicationContext