权衡 Apache Geronimo EJB 事务选项,第 1 部分: 容器管理事务

本系列分为三部分,将探索 Apache Geronimo 中的 Enterprise Java™Beans (EJB) 容器管理事务和 bean 管理事务。在第 1 部分中,将找出两种事务之间的差异,其中包括了解容器管理事务如何帮助您避免事务逻辑和管理的复杂性,从而使您可以专注于企业 bean 的业务逻辑。您还将学会如何在 Geronimo 应用服务器中实现容器管理事务,以及如何使用 Geronimo、OpenEJB 和 XDoclet 将自己从繁重的 EJB 编码工作中解放出来。

简介

OpenEJB 是为 Apache Geronimo 选定的 EJB 容器实例。虽然 EJB 3.0 目前已经面市,但直到发布 Geronimo 2.0 版,在 Geronimo 接受 Java 1.5 认证时,Geronimo 才支持 EJB。

本系列分为三部分,将使您了解 Geronimo 和 OpenEJB 可以为您提供什么帮助,以及在 EJB 2.1 中现在可以实现的 EJB 事务概念(让您顺利进入 EJB 3.0)。

EJB 框架提供的好处是:可以使用事务,但没有事务 API 编程的痛苦。在实现 EJB 事务时,您有两种选择:

  • 告诉 EJB 容器处理所有的硬性事务工作(容器管理的事务)。
  • 让企业 bean 处理一部分事务工作(bean 管理的事务)。

在本系列的第 1 部分中,将从事务的概述开始,然后讨论 EJB 2.1 中描述的 EJB 容器管理的事务。最后用一些代码片断结束介绍,这些代码将显示如何在 Geronimo 应用服务器上实现容器管理的事务。

在第 2 部分中,将获得 EJB 2.1 中 bean 管理的事务的概述,并查看一些示例代码实现。

在第 3 部分中,将综合这两种事务,并了解与容器管理的事务和 bean 管理的事务有关的难题和附加特性。

事务—— 概述

什么是事务?为什么它们如此重要?可以考虑一下银行事务这个非常简单的案例:将 100 美元从您的一个活期存款帐户转移到您的储蓄存款帐户。通过进一步的调查,可将这一操作分解为两个更小的操作:

  • 银行从您的活期存款帐户减去 100 美元。
  • 银行在您的储蓄存款帐户增加 100 美元。

如果银行将活期存款额减少 100 美元,但您的储蓄存款额并没有增加 100 美元,那么您可能会感到有点沮丧。就个人而言,我愿意将两个操作视为一个操作。因此,如果您的储蓄存款帐户从没有增加 100 美元,那么 100 美元也决不应从您的活期存款帐户中减去!

类似地,在应用过程中,很多业务案例都是进行整体确认的 (all-or-nothing approach)。一些大的操作由一个或多个更小的步骤组成。为了完成操作,操作中的所有 步骤都必须完成或不完成,这种行为称为原子 行为。

原子性是事务必须保证的四个特征(或属性)之一。其他三个属性是:

  • 一致性
  • 隔离性
  • 耐久性

这四种属性一起被称为 ACID 属性。

ACID 属性

事务对这些已知 ACID 属性的描述为:

  • 事务是原子的。所有操作都被认为是一个工作单元。像前面讨论的那样,是整体确认的。
  • 事务是一致的。在执行事务之后,必须将系统维持在一致(或合法)状态下。合法状态的定义取决于系统。根据早先的示例,在执行任何撤消操作之后,银行指示您,将保留您的活期存款帐户为顺差。
  • 事务是隔离的。每个事务在同一资源进行操作时与其他事务都是相互隔离的。这可通过数据的锁同步来实现。
  • 事务是持久的。资源更新必须避免系统故障,如硬件或网络故障。在分布式系统中,当出现网络故障或数据库崩溃时,恢复过程是必需的。

事务模型

有两种流行的事务模型:flat 事务和 nested 事务。EJB 支持 flat 事务模型。

flat 事务是作为单个工作单元处理的一系列操作。工作单元只有两种结果:要么成功,要么失败。如果事务中的所有步骤都成功完成,则事务获得提交,并且该操作执行 的所有持久存储数据更改都将永久化。如果事务中某一步骤失败,则事务将回滚 (roll back),并反转事务中步骤受影响的所有数据。

nested 事务允许事务嵌套在其他事务中。嵌套在其他事务中的事务允许在不影响其父事务的情况下进行回滚。失败的 nested 事务可以继续重试。如果再次失败,则可回滚父事务。

EJB 事务

EJB 是用于组件开发的一个框架。您开发的 EJB 将运行在 EJB 容器中。此外,EJB 容器为事务带来了一些好处。OpenEJB 是 Geronimo 用来提供事务管理的 EJB 容器。

EJB 架构支持分布式事务。一些需要分布式事务的场景模式范例包括:

  • 更新多个数据库的单个事务中的应用。
  • 从 Java Message Service (JMS) 目标发送或接收消息并更新一个或多个数据库的单个事务中的应用。
  • 通过多个 EJB 服务器来更新多个数据库的单个事务中的应用。
  • 在更新多个 EJB 服务器上的多个数据库之前,Java 客户端明确区分事务边界。

事务边界

在实现 EJB 事务时,您将划分事务边界:谁启动事务、谁提交或中止事务,以及什么时候使用事务。这取决于 EJB 容器和服务器提供商提供的事务管理和底层事务通信协议。

有两种划分方案:

  • declarative 方案,使用该方案可以将事务实现委托给 EJB 容器。(该方案是本文其余部分的焦点。)
  • programmatic 方案,在该方案中,企业 bean 使用自己的代码自己提供提交或中止信息。(本系列的第 2 部分中将介绍此方案。)

在使用 declarative 事务划分时,EJB 容器根据 EJB 部署描述符中由应用程序开发人员声明的指令,在企业 bean 的方法上应用事务边界。这称为容器管理的事务

在实现 programmatic 划分事务时,应用程序开发人员负责将事务逻辑和界线编入企业 bean 代码中。这称为 bean 管理的事务

我应该使用哪种事务?

容器管理的事务更加简单并且在代码中不需要实现事务逻辑,无论您的企业 bean 方法是否必须运行在事务中。此外,调用 bean 的 Java 客户端不能滥用您的企业 bean,因为事务始终是有始有终的。

如果想完全控制事务边界,请使用 bean 管理的事务。该方法允许在代码中直接控制提交或控制回滚逻辑发生的地方。

会话 bean 和消息驱动 bean (MDB) 可以使用 bean 管理的事务或容器管理的事务,但是实体 bean 必须始终使用容器管理的事务。实体 bean 使用 bean 管理的持久性是不合法的。

容器管理的事务

事务划分边界是通过指令或事务属性提供的。这些属性描述了企业 bean 是如何参与到事务中的。您可以对每个 bean 指定不同的事务属性而不必考虑 bean 的数目。您可以为 bean 的个别或所有方法指定属性。方法的属性是优先于 bean 的。

会话 bean 和实体 bean 的事务属性

会话 bean 和实体 bean 可能的属性值包括:

  • Required —— bean 必须始终运行在事务中。如果客户端已经启动一个事务,则 bean 将加入到事务中。如果客户端还没有启动事务,那么 EJB 容器将启动一个新事务。当需要 bean 始终运行在事务中时,请使用该属性。
  • RequiresNew —— bean 始终启动一个新的事务。如果客户端已经启动一个事务,则挂起现有事务,直到新事务已提交或中止。在新事务完成之后,现有事务将继续。当需要 bean 作为一个单独的工作单元运行并展示所有的 ACID 属性时,请使用该属性。
  • Supports —— 如果客户端启动一个事务,则 bean 将加入到事务中。但是,如果事务不存在,EJB 容器不会启动一个新事务。要在企业 bean 上执行非任务关键型操作时,请使用该属性。
  • Mandatory —— 在调用 bean 时客户端必须启动一个事务。这不会创建一个新的事务。在调用 bean 时,如果没有事务已经启动,则将抛出一个异常。当 bean 是某一较大系统的一部分时,请使用该属性。通常可能由第三方负责启动事务。对用户而言,这是一个安全选项,因为它可以确保 bean 将成为事务的一部分。
  • NotSupported —— 在事务中不能调用 bean。如果客户端已经启动一个事务,则挂起现有事务,直到 bean 的方法完成。在完成上述方法之后,现有事务将继续。如果客户端没有启动事务,则不会创建一个新事务。在不需要 bean 展示任何 ACID 属性(比如类似报表的非系统关键型操作)时,请使用该属性。
  • Never —— 如果客户端启动一个事务,则 bean 将抛出一个异常。在您可能永远都不想让您的 bean 参与到事务中的情况下,请使用该属性。

消息驱动 bean 的事务属性

只有两种消息驱动 bean 消息监听器方法使用的事务属性:

  • NotSupported —— bean 不能参与到事务中。如果客户端启动一个事务,那么现有事务将挂起,直到 bean 的方法完成为止。在完成上述方法之后,现有事务将继续。如果客户端没有启动事务,则不会创建一个新的事务。
  • Required —— bean 必须始终运行在事务中。如果客户端已经启动事务,则 bean 将加入到事务中。如果客户端没有启动事务,则 EJB 容器将启动一个新事务。

在为企业 bean 方法确定正确事务属性之后,就可以配置 EJB 部署描述符了。

配置 EJB 部署描述符

对于每个企业 bean,都要在部署描述符中配置事务的下列两个部分:

  • 在 EJB 部署描述符中使用 <transaction-type> 元素指定 bean 使用的是容器管理的事务还是 bean 管理的事务。可能的值是 containerbean。由于实体 bean 必须使用容器管理的事务,这只对会话 bean 和消息驱动 bean 是必需的。
  • 对于容器管理的事务,您可以为企业 bean 的方法随意指定事务属性。在 EJB 部署描述符中的 <container-transaction> 部分指定它。清单 1 中显示了每种方法的通用格式。
清单 1. 每种方法的通用格式

<method>
     
<ejb-name>EJBNAME</ejb-name>
     
<method-name>METHODNAME</method-name>             
     
<trans-attribute>TRANSATTRIBUTE</trans-attribute>
</method>

TRANSATTRIBUTE 可能的值有:

  • NotSupported
  • Required
  • Supports
  • RequiresNew
  • Mandatory
  • Never

也可以对企业 bean 的所有方法指定事务属性。对 <method-name> 属性使用 *

清单 2 显示了为容器管理的企业 bean 指定事务属性的示例。除了为 updateClaimNumber 方法分配 Mandatory 属性以外,ClaimRecord企业 bean 为所有方法都分配了 Required 属性。Coverage bean 对所有方法指派 RequiresNew 属性。


清单 2. ejb 部署描述符文件中的事务属性
<ejb-jar>

<assembly-descriptor>

<container-transaction>
      
<method>
            
<ejb-name>ClaimRecord</ejb-name>
            
<method-name>*</method-name>
      
</method>
      
<trans-attribute>Required</trans-attribute>
</container-transaction>
<container-transaction>
      
<method>
             
<ejb-name>ClaimRecord</ejb-name>
      
<method-name>updateClaimNumber</methodname>
      
</method>
     
<trans-attribute>Mandatory</trans-attribute>
</container-transaction>
<container-transaction>
    
<method>
        
<ejb-name>Coverage</ejb-name>
        
<method-name>*</method-name>
    
</method>
    
<trans-attribute>RequiresNew</trans-attribute>
</container-transaction>

</assembly-descriptor>

</ejb-jar>

Geronimo 配置

既然您明白了在 EJB 部署描述符中指定事务属性的通用格式,那么可以考虑一下如何在 Geronimo 中使用 OpenEJB 实现这一点。在 Geronimo 中开发 EJB 时,可以通过使用 XDoclet 生成所需的大部分单调的 EJB 编程工件 (artifact) 来节省时间。作为这些工件的一部分,XDoclet 生成了 EJB 部署描述符。

作为正常开发过程的一部分,可以在企业 bean 中指定 JavaDoc-style 标识标签。通过在企业 bean 中声明标识标签,XDoclet 可生成 ejbjar.xml。这包括属性定义的任何事务。您不用自己直接编译部署描述符 (ejb-jar.xml)。

在 XDoclet 中使用 @ejb.transaction 标识指定事务属性。在需要使用它时,可以在企业 bean 的方法之上声明它。

XDoclet 配置示例和 ejbjar.xml 生成

下面的代码片断显示了一个简洁的会话 bean 和实体 bean 示例,然后由 XDoclet 生成最终的 ejbjar.xml 文件。首先,清单 3 显示了一个名为 SampleSession 的无状态会话 bean。只需要注意与事务相关的部分即可(用粗体显示)。


清单 3. 会话 bean
package org.my.package.ejb;
/**
 * Sample session bean. 
 * Declare all my XDoclet tags here
 * 
 *  
 * @ejb.bean name="SampleSession"
 *   type="Stateless"
 *   local-jndi-name="java:comp/env/ejb/SampleSessionLocal"
 *   jndi-name="org.my.package.ejb/SampleSessionLocal/Home"
 *   view-type="both"
 *
 * @ejb.permission unchecked="true"
 *
 * @ejb.interface generate="local,remote"
 *   remote-class="org.my.package.ejb.SampleSession"
 *   local-class=" org.my.package.ejb. SampleSession Local"
 * @ejb.home generate="local, remote"
 *      remote-class="org.my.package.ejb.SampleSession Home"
 *    local-class="org.my.package.SampleSession LocalHome"
 * @ejb.util generate="physical"
 * 
 * 
 
*/

public abstract class SampleSessionBean implements javax.ejb.SessionBean {

   
/**
    * Perform a business operation. Add something
    * 
@param someParam the value
    * @ejb.interface-method view-type="both"
    * @ejb.transaction      type="Required"
    
*/

   
public void doSomething(java.lang.String someParam)) {
      
   }


   
/*
    * Perform another business operation. Add something
    * @param someParam the value
    * @ejb.interface-method view-type="both"
    * @ejb.transaction      type="RequiresNew"
    
*/

   
public void doSomethingElse(java.lang.String someParam)) {
      
   }

   
/**
    * @ejb.create-method
    * @ejb.transaction type="Required"
    
*/

   
public void ejbCreate ()
           
throws javax.ejb.CreateException
   
{
   }


   
public void ejbPostCreate ()
           
throws javax.ejb.CreateException
   
{
   }


   
protected javax.ejb.SessionContext _ctx = null;

   
public void setSessionContext( javax.ejb.SessionContext     ctx )
   
{
        _ctx 
= ctx;
   }


   
protected javax.ejb.SessionContext getSessionContext()
   
{
        
return _ctx;
   }

}

同样的标识 @ejb.transaction 被用来指定该实体 bean 的事务属性。
清单 4 显示如何指定实体 bean 的事务属性。同样,只需要注意粗体的标识即可。

清单 4. 实体 bean
package org.my.package.ejb;

/**
 *
 * @ejb.bean 
 *    type="CMP" 
 *    cmp-version="2.x"
 *    name="ClaimEntry" 
 *    local-jndi-name="org.my.package.ejb/ClaimLocalHome"
 *    view-type="local"
 *    primkey-field="name"
 *    
 *
 * @xx-ejb.data-object
 *    container="true"
 *    setdata="true"
 *    generate="true"
 *    
 * @ejb.value-object
 *
 * @ejb.transaction type="Required"
 * @ejb.permission unchecked="true"
 * @struts.form include-all="true"
 *
 * @web.ejb-local-ref
 *    name="ejb/ClaimLocal"
 *    type="Entity"
 *    home="org.my.package.ejb.ClaimLocalHome"
 *    local="org.my.package.ejb.ClaimLocal"
 *    link="PhoneBookEntry"
 *
 * @ejb.persistence table-name="Claim"
 *
 
*/

public abstract class ClaimBean 
       
implements javax.ejb.EntityBean
{

 
*  EJB entity bean implementation here   
}

在编译过程中执行 XDoclet 时,生成了 ejb-jar.xml。
清单5 显示了文件的事务相关部分。注意粗体显示的 <transaction-type><trans-attribute> 元素。

清单 5. 生成的 ejb-jar.xml 片断

<ejb-jar >

   
<description><![CDATA[No Description.]]></description>
   
<display-name>Generated by XDoclet</display-name>

   
<enterprise-beans>

      
<!-- Session Beans -->
      
<session >
         
<description><![CDATA[Sample session
 bean.]]
></description>

         
<ejb-name>SampleSession</ejb-name>

         
<home>org.my.package.ejb.SampleSessionHome</home>
         
<remote>org.my.package.ejb.SampleSession</remote>
<local-home>org.my.package.ejb.SampleSessionLocalHome
</local-home>
         
<local>org.my.package.ejb.SampleSessionLocal</local>
        
 
<ejb-class>org.my.package.ejb.SampleSessionSessionSession
</ejb-class>
         
<session-type>Stateless</session-type>
         
<transaction-type>Container</transaction-type>
      
</session>


      
<!-- Entity Beans -->
      
<entity >
         
<description><![CDATA[]]></description>
         
<ejb-name>Claim</ejb-name>
         
<local-home>
 org.my.
package.ejb.ClaimLocalHome</local-home>
         
<local>org.my.package.ejb.ClaimLocal</local>
         
<ejb-class>org.my.package.ejb.ClaimCMP</ejb-class>
         
<persistence-type>Container</persistence-type>
         
<prim-key-class>java.lang.String</prim-key-class>
         
<reentrant>False</reentrant>
         
<cmp-version>2.x</cmp-version>
         
<abstract-schema-name>Claim</abstract-schema-name>

      
</entity>

   
<container-transaction >
      
<method >
         
<ejb-name>Claim</ejb-name>
          
<method-name>*</method-name>
       
</method>
       
<trans-attribute>Required</trans-attribute>
    
</container-transaction>
   
<container-transaction >
      
<method >
         
<ejb-name>SampleSession</ejb-name>
         
<method-intf>Local</method-intf>
         
<method-name>doSomething</method-name>
         
<method-params>
            
<method-param>java.lang.String</method-param>
         
</method-params>
      
</method>
      
<trans-attribute>RequiresNew</trans-attribute>
   
</container-transaction>
   
<container-transaction >
      
<method >
         
<ejb-name>SampleSession</ejb-name>
         
<method-intf>Remote</method-intf>
         
<method-name>doSomething</method-name>
         
<method-params>
            
<method-param>java.lang.String</method-param>
         
</method-params>
      
</method>
      
<trans-attribute>RequiresNew</trans-attribute>
   
</container-transaction>
   
<container-transaction >
      
<method >
         
<ejb-name>SampleSession</ejb-name>
         
<method-intf>Local</method-intf>
         
<method-name>doSomethingElse</method-name>
         
<method-params>
            
<method-param>java.lang.String</method-param>
         
</method-params>
      
</method>
      
<trans-attribute>Required</trans-attribute>
   
</container-transaction>
   
<container-transaction >
      
<method >
         
<ejb-name>SampleSession</ejb-name>
         
<method-intf>Remote</method-intf>
         
<method-name>doSomethingElse</method-name>
         
<method-params>
            
<method-param>java.lang.String</method-param>
         
</method-params>
      
</method>
      
<trans-attribute>Required</trans-attribute>
   
</container-transaction>


</ejb-jar>

事务同步

容器管理的事务允许 EJB 容器指定事务边界,这可以简化您的工作。然而,当事务中止时,可能需要执行一些 bean 状态的恢复工作。对于无状态会话 bean,可能抛出一个简单的异常。有状态会话 bean 表示了对话状态(或商业流程),这可能会跨越几个 bean 方法调用。

如果要求有状态会话 bean 获得事务边界状态事件通知,则需要编写企业 bean 代码来实现可选的 javax.ejb.SessionSynchronization 接口。您必须实现定义在接口上的下列方法:

  • afterBegin() —— 在新事务启动之后但在调用业务方法之前通知会话 bean。在事务提交之前,bean 实例可以做任何它所需要的数据库读取操作。在缓冲事务所需要的数据时,这很有用。
  • beforeCompletion() —— 在业务方法完成之后但是在事务提交之前通知会话 bean。如果有任何缓冲数据,可以将其更新到数据库中。bean 还可以在会话上下文中通过调用 setRollBackOnly() 执行事务的手动回滚。
  • afterCompletion(boolean committed) —— 在事务提交之后通知会话 bean。提交的布尔值指出是提交事务还是中止事务。如果该值为 true,则事务成功获得提交。如果该值为 false,则事务中止。因此,bean 的对话状态/实例变量可以被恢复或重新设置。

避免使用的方法

既然 EJB 容器是负责控制事务边界,那么您不应该调用任何可能干涉容器边界划分的方法。如果您正在实现容器管理的事务,请确保企业 bean 方法不会调用下列方法:

  • java.sql.ConnectioncommitsetAutoCommitrollback 方法
  • javax.ejb.EJBContextgetUserTransaction 方法
  • javax.transaction.UserTransaction 的任何方法

回滚

在某些情况下您可能需要明确中止事务。有两种回滚容器管理的事务的方式:

  • 让容器自动回滚事务。如果有任何企业 bean 抛出的运行时异常,就会发生这种回滚。
  • 调用 EJBContext 接口的 setRollBackOnly() 方法。在发生回滚时,允许您进行控制。或许由于一些有效性验证失败或存在数据完整性问题,您可能需要回滚整个事务并抛出一个应用程序异常。应用程序异常不会自动导致容器回滚一个异常。

posted @ 2008-03-20 11:21  谢芳[Kevin]  阅读(336)  评论(0编辑  收藏  举报