29. Enterprise JavaBeans(EJB)integration

29.1 简介

作为一个轻量级容器,Spring 通常被认为是 EJB 的替代品。我们相信,对于许多(尽管不是大多数)应用程序和用例,Spring 作为容器,结合其在 transactions,ORM 和 JDBC 访问领域的丰富支持功能,是比通过 EJB 容器和 EJB 实现等效功能更好的选择。 。

但是,请务必注意,使用 Spring 不会阻止您使用 EJB。实际上,Spring 使得访问 EJB 以及在其中实现 EJB 和功能变得更加容易。此外,使用 Spring 访问 EJB 提供的服务允许稍后在本地 EJB,remote EJB 或 POJO(普通旧 Java object)变体之间切换这些服务的 implementation,而不必更改 client code。

在本章中,我们将了解 Spring 如何帮助您访问和实现 EJB。 Spring 在访问 stateless session beans(SLSB)时提供特定的 value,因此我们将首先讨论这个问题。

29.2 访问 EJB

29.2.1 概念

要在本地或 remote stateless session bean 上调用方法, client code 通常必须执行 JNDI 查找以获取(本地或 remote)EJB Home object,然后在该 object 上使用'create'方法调用来获取实际(本地)或 remote)EJB object。然后在 EJB 上调用一个或多个方法。

为避免重复 low-level code,许多 EJB applications 使用 Service Locator 和 Business Delegate 模式。这些比在整个 client code 中喷涂 JNDI 查找更好,但是它们通常的 implementations 有明显的缺点。例如:

  • 通常,使用 EJB 的 code 取决于 Service Locator 或 Business Delegate 单例,因此很难进行测试。

  • 对于没有业务委托使用的服务定位器 pattern,application code 仍然需要在 EJB 主目录上调用 create()方法,并处理生成的 exceptions。因此,它仍然与 EJB API 和 EJB 编程 model 的复杂性联系在一起。

  • 实现 Business Delegate pattern 通常会导致重要的 code 重复,我们必须编写许多方法,只需在 EJB 上调用相同的方法。

Spring 方法允许创建和使用代理 objects,通常在 Spring 容器内配置,充当无代码业务代表。您不需要在 hand-coded Business Delegate 中编写另一个服务定位器,另一个 JNDI 查找或重复方法,除非您实际在此类 code 中添加了实际 value。

29.2.2 访问本地 SLSB

假设我们有一个需要使用本地 EJB 的 web 控制器。我们将遵循最佳实践并使用 EJB 业务方法接口 pattern,以便 EJB 的本地接口扩展非 EJB-specific 业务方法接口。我们称之为业务方法接口MyComponent

public interface MyComponent {
    ...
}

使用业务方法接口 pattern 的主要原因之一是确保本地接口中的方法签名与 bean implementation class 之间的同步是自动的。另一个原因是它后来使我们更容易切换到服务的 POJO(普通的旧 Java object)implementation,如果有意义的话。当然,我们还需要实现本地 home 接口并提供实现SessionBeanMyComponent业务方法接口的 implementation class。现在我们需要做的唯一 Java 编码将我们的 web 层控制器连接到 EJB implementation 是在控制器上公开类型为MyComponent的 setter 方法。这会将 reference 保存为控制器中的实例变量:

private MyComponent myComponent;

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

我们随后可以在控制器中的任何业务方法中使用此实例变量。现在假设我们从 Spring 容器中获取控制器 object,我们可以(在相同的 context 中)配置一个LocalStatelessSessionProxyFactoryBean实例,它将是 EJB 代理 object。代理的 configuration 和控制器的myComponent property 的设置是通过 configuration 条目完成的,例如:

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

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

虽然你没有被迫使用 AOP 概念来享受结果,但是幕后工作还是有很多工作要做,这可以归功于 Spring AOP framework。 myComponent bean 定义为 EJB 创建代理,该代理实现业务方法接口。 EJB 本地主目录在启动时被缓存,因此只有一个 JNDI 查找。每次调用 EJB 时,代理都会调用本地 EJB 上的classname方法,并在 EJB 上调用相应的业务方法。

myController bean 定义将控制器 class 的myComponent property 设置为 EJB 代理。

或者(并且最好在许多此类代理定义的情况下),考虑在 Spring 的“jee”命名空间中使用<jee:local-slsb> configuration 元素:

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

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

这种 EJB 访问机制大大简化了 application code:web 层 code(或其他 EJB client code)不依赖于 EJB 的使用。如果我们想用 POJO 或 mock object 或其他测试存根替换这个 EJB reference,我们可以简单地改变myComponent bean 定义而不改变 Java code 的 line。另外,我们不必编写单个 line 的 JNDI 查找或其他 EJB 管道 code 作为我们的 application 的一部分。

实际应用程序中的基准和经验表明,这种方法的性能开销(涉及目标 EJB 的反射调用)很少,并且在典型使用中通常无法检测到。请记住,我们不希望将 fine-grained calls 设置为 EJB,因为与 application 服务器中的 EJB 基础结构相关的成本。

关于 JNDI 查找有一点需要注意。在 bean 容器中,这个 class 通常最好用作 singleton(没有理由将它作为原型)。但是,如果 bean 容器 pre-instantiates 单例(与各种 XML ApplicationContext变体一样),如果在 EJB 容器加载目标 EJB 之前加载 bean 容器,则可能会出现问题。这是因为 JNDI 查找将在此 class 的init()方法中执行,然后进行缓存,但 EJB 尚未绑定到目标位置。解决方案是不要 pre-instantiate 这个工厂 object,但允许它在第一次使用时创建。在 XML 容器中,这是通过lazy-init属性控制的。

尽管大多数 Spring 用户都不会对此感兴趣,但那些使用 EJB 编程 AOP 的人可能希望查看LocalSlsbInvokerInterceptor

29.2.3 访问 remote SLSB

除了使用SimpleRemoteStatelessSessionProxyFactoryBean<jee:remote-slsb> configuration 元素之外,访问 remote EJB 与访问本地 EJB 基本相同。当然,无论有没有 Spring,remote 调用语义都适用;对另一台计算机中另一台虚拟机中 object 上的方法的调用有时必须在使用场景和故障处理方面区别对待。

Spring 的 EJB client 支持比 non-Spring 方法增加了一个优势。通常,EJB client code 很容易在本地或远程调用 EJB 之间来回切换。这是因为 remote 接口方法必须声明它们抛出RemoteException,而 client code 必须处理这个,而本地接口方法则不然。为需要移动到 remote EJB 的本地 EJB 编写的 Client code 通常必须修改以添加 remote exceptions 的处理,并且为需要移动到本地 EJB 的 remote EJB 编写的 client code 可以保持不变但是很多不必要的 remote exceptions 处理,或需要修改以删除该 code。使用 Spring remote EJB 代理,您可以不在业务方法接口中声明任何抛出的RemoteException并实现 EJB code,而是具有相同的 remote 接口,除了它抛出RemoteException,并依赖代理动态处理这两个接口好像他们是一样的。也就是说,client code 不必处理已检查的RemoteException class。在 EJB 调用期间抛出的任何实际RemoteException都将 re-thrown 作为 non-checked RemoteAccessException class,它是RuntimeException的子类。然后,可以在本地 EJB 或 remote EJB(甚至是普通的 Java object)implementation 之间随意切换目标服务,而无需知道或关注 client code。当然,这是可选的;没有什么可以阻止你在业务界面中声明RemoteExceptions

29.2.4 访问 EJB 2.x SLSB 与 EJB 3 SLSB

通过 Spring 访问 EJB 2.x Session Beans 和 EJB 3 Session Beans 在很大程度上是透明的。 Spring 的 EJB 访问器,包括<jee:local-slsb><jee:remote-slsb>工具,在运行时透明地适应实际的 component。如果找到(EJB 2.x 样式),它们将处理 home 接口,或者如果没有可用的 home 接口(EJB 3 样式),则执行直接 component 调用。

注意:对于 EJB 3 Session Beans,您也可以有效地使用JndiObjectFactoryBean/<jee:jndi-lookup>,因为那里公开了完全可用的 component references 用于普通 JNDI 查找。定义显式<jee:local-slsb>/<jee:remote-slsb>查找只是提供一致且更明确的 EJB 访问配置。

29.3 使用 Spring 的 EJB implementation 支持 classes

29.3.1 EJB 3 注入拦截器

对于 EJB 3 Session Beans 和 Message-Driven Beans,Spring 提供了一个方便的拦截器,可以解析 EJB component class:org.springframework.ejb.interceptor.SpringBeanAutowiringInterceptor中的 Spring 的@Autowired annotation。此拦截器可以通过 EJB component class 中的@Interceptors annotation 应用,也可以通过 EJB 部署描述符中的interceptor-binding XML 元素应用。

@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获取目标 beans,context 在名为beanRefContext.xml的 bean 定义文件中定义。默认情况下,需要一个 context 定义,它是通过类型而不是 name 获得的。但是,如果需要在多个 context 定义之间进行选择,则需要特定的定位器 key。可以通过覆盖自定义SpringBeanAutowiringInterceptor子类中的getBeanFactoryLocatorKey方法来显式指定定位器 key(i.e.beanRefContext.xml中 context 定义的 name)。

或者,考虑从自定义 holder class 重写SpringBeanAutowiringInterceptor's getBeanFactory method, e.g. obtaining a shared ApplicationContext`。