18.2. 访问EJB

Spring Framework

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访问配置。