摘要
本节包含如何在EJB开发中使用事务,包括容器管理事务和Bean管理事务的描述,EJB的事务处理模型的介绍,以及在Apusic应用服务器中的EJB开发中,如何正确地使用事务模型。
本节假设读者已经熟悉JDBC中的事务模型和JTA API,有关以上两个部分,可以参考Apusic应用服务器中的Sun的JTA文档和Sun的JDBC教程。
金蝶Apusic应用服务器的事务服务,提供了JTA API的完整实现,支持EJB1.1和EJB2.1的两阶段提交(two-phase commit)协议。两阶段提交协议是一种在两个或以上的资源管理器之间协调一个事务的方法。两阶段提交协议通过对所有参与事务的资源(如数据库、消息系统等)进行事务性的数据更新来保证数据的完整性。
金蝶Apusic应用服务器中,EJB组件可以在代码中进行程序型的事务边界划分,也可通过部署描述对事务进行声明型的划分。使用这两种方式,开发者可以简单地划分事务边界和确定事务逻辑,运行时的具体底层细节将由Apusic事务服务中的事务管理器进行管理。
在J2EE™平台中,可管理的事务资源包括三种,数据库连接池、消息系统连接和符合J2EE™连接器架构(J2EE Connector Architecture,JCA)的资源。Apusic应用服务器中的事务管理器提供对以上三种资源的事务管理。
在了解EJB的事务划分之前,需要了解EJB客户端的事务与EJB之间的关系,同时也需要了解EJB方法的事务上下文(transaction context)。
在第 44.1 节 “简介”一节中,概述了EJB的客户端类型,为简化问题的描述,本节中不对每种客户端类型进行单独讨论。统称EJB客户端。
EJB规范关于事务处理模型的内容,定义了现阶段的EJB事务处理模型并不包含嵌套事务(nested transaction),只提供平直事务(flat transaction)的支持。
所谓嵌套事务,指一个事务边界内部可包含多个子事务边界,子事务的提交或回滚并不影响父事务的提交或回滚,而父事务的提交或回滚可决定所有包含在此事务边界中的子事务的提交或回滚。
所谓平直事务,指事务不能相互嵌套,事务内部不能包含子事务。事务与事务之间是独立的,某个事务的提交或回滚不会直接影响其他事务的提交或回滚。
因此,按照以上的平直事务模型的定义,根据EJB客户端是否包含在事务边界中,应用服务器中的事务管理器在运行时按照开发者指定的事务管理类型(容器管理或Bean管理),对EJB实例方法的事务边界与客户端事务边界进行划分。通常有如下策略:
客户端无事务
容器根据开发者指定的事务管理类型,在新的事务上下文中完成EJB实例的方法调用,或在未指明的事务上下文(unspecified transaction context)中完成方法调用;如开发者指定调用此EJB方法时,客户端必须伴随事务上下文,则容器将抛出异常。
客户端有事务
根据开发者指定的事务管理类型,容器有以下策略:暂停客户端事务,在新的事务上下文中运行EJB实例的方法,调用完成后,恢复客户端事务;暂停客户端事务,在未指明的事务上下文(unspecified transaction context)中完成EJB实例的方法调用,调用完成后,恢复客户端事务;在客户端的事务上下文中,完成EJB实例的方法调用;另外,如开发者指定调用此EJB方法时,的客户端不可伴随事务上下文,则容器将抛出异常。
按照以上策略,可以了解到金蝶Apusic应用服务器中的事务管理器对EJB实例的方法调用进行事务控制的大致情形。必须注意,这些策略都保证了整个调用过程中,事务之间不会相互嵌套。
一般来讲,对于JSP、Servlet、Java Application和Applet等客户端,不提倡在其中进行事务划分,因为这将使易出错的代码增加,并且失去了不修改代码即可改变系统行为的能力,而且,这种方式将系统的关键功能(事务的管理)移到了整个系统的前端或GUI部分,违反了企业应用的分层原理,因此,在涉及时,应尽量将划分事务边界的操作放到EJB包含的业务逻辑中,并尽量使用容器管理事务的方式。
在前面,提到的未指明的事务上下文(unspecified transaction context),是指在EJB体系结构中,对执行EJB方法的事务语义,没有进行完整定义的情况。在这种情况下的EJB方法,其中对事务性资源的操作(如数据库连接、消息服务连接)不具有可恢复的能力,例如,某个未指明的事务上下文(unspecified transaction context)的EJB方法中,对数据库进行插入、或更新数据的操作,当客户端调用此EJB方法时,伴随了事务,容器将在方法执行前暂停客户端事务,在方法执行完成之后恢复客户端事务,而之后的客户端事务的结果无论是提交或回滚,都不能影响到前面的EJB方法对数据库的操作,这些操作实际上不包含在事务边界中,属于非事务性的不可恢复的操作。
因此,在未指明的事务上下文(unspecified transaction context)的EJB方法中,开发者必须慎重考虑对事务性资源的操作。一般,不提倡开发者在这些方法中对事务性资源进行操作。EJB体系结构中,具体的未指明的事务上下文(unspecified transaction context)的EJB方法,可参考本节中的第 44.11.3.2 节 “容器管理事务(声明型的事务划分)”。
从以上对客户端事务的描述,可以看到,EJB的事务处理与客户端请求的事务上下文存在很紧密的关系,与此同时,EJB的事务处理与容器管理EJB实例的方式也有存在着紧密的关系。我们将在具体的事务划分方式中对以上因素加以分析。
提高企业应用中有关事务的开发和维护效率,降低EJB的开发难度,使开发者不必过多考虑事务处理的底层细节,Apusic应用服务器根据EJB规范和其他J2EE平台™规范,提供了简单的易于开发者使用的事务划分方式。
EJB的规范中,定义了开发者可使用的两种事务划分方式:一种是Bean管理事务,即程序型的事务划分;另一种是容器管理事务,即声明型的事务划分。当使用程序型的事务划分的方式时,开发者必须使用通过JNDI访问容器提供的javax.transaction.UserTransaction接口的引用,使用UserTransaction接口对事务边界进行划分,每个在UserTransaction.beging()和UserTransaction.commit()之间的对资源(如数据库)的操作,作为事务的一个组成部分。当使用声明型的事务划分时,开发者通过配置EJB的部署描述,开发者在部署描述中使用事务相关的元素对方法的事务属性进行设置,完成方法级别的事务划分。
两种方式都最大限度地简化了在事务方面的开发难度,而且具有不同的特点。
对于EJB组件模型中的Session Bean,Entity Bean和Message-driven Bean,Session Bean和Entity Bean可指定使用程序型的事务划分或者声明型的事务划分,但不能在一个Bean内部同时使用两种事务划分方式。Entity Bean必须采用声明型的事务划分方式。
当使用Bean管理事务时,开发者必须通过JNDI API查找并获取由服务器提供的javax.transaction.UserTransaction接口,或者通过伴随EJB实例的javax.ejb.EJBContext接口的getUserTransaction方法,获取javax.transaction.UserTransaction接口,并使用此接口对事务进行划分。另外,还必须在EJB的部署描述文件中,使用transaction-type元素指定其事务管理类型为Bean管理事务类型。
使用JNDI查找UserTransaction接口的范例代码如下:
... try { Context initial = new InitialContext(); Object objref = initial.lookup("java:comp/UserTransaction"); javax.transaction.UserTransaction tx = (javax.transaction.UserTransaction)objref; tx.begin(); ... } catch(...) ...
Session Bean中使用javax.ejb.EJBContext取得UserTransaction接口的范例代码如下:
try { ... javax.ejb.EJBContext ctx = getSessionContext(); javax.transaction.UserTransaction tx = ctx.getUserTransaction(); ... tx.begin(); ... } catch(...) ...
tansaction-type元素的设置范例如下:
... <ejb-jar> <display-name>CartEJB</display-name> <enterprise-beans> <session> <display-name>CartEJB</display-name> ... <transaction-type>Bean</transaction-type> ... </session> ... </enterprise-beans> ... </ejb-jar>
有关javax.transaction.UserTransaction的详细使用说明,请参考Sun的JTA文档。
从上面对客户端事务的描述,可以看到,EJB的事务处理模型与客户端请求存在很紧密的关系,从第 44.1 节 “简介”一节中,我们了解到EJB容器对客户端请求的处理方式,即有状态的Session Bean实例与客户端是一一对应的,无状态的Session Bean实例可由容器分配给任意客户端使用,而Entity Bean实例则有多个客户端共享,Message-driven Bean则与客户端无关。按照EJB组件实例与客户端的关系,对各种组件模型使用Bean管理事务有如下限制:
对于有状态的Session Bean实例,客户端是唯一的,因此,在方法调用之间的事务状态可以得到保持,对于实例的客户端而言,可以使用javax.transaction.UserTransaction接口在方法中清晰地划分事务边界:如在某方法内部使用UserTransaction.begin()方法开始事务边界和使用UserTransaction.commit()方法提交事务;也可在某方法中使用UserTransaction.begin()方法开始事务边界,在进行一系列的业务方法调用之后,使用UserTransaction.commit()方法提交事务;
对于无状态的Session Bean,实例可被容器分配给不同的客户端使用,而且,实例不会保持会话状态,因此,当使用Bean管理事务时,如在方法中使用UserTransaction接口划分事务,必须在同一个方法内部开始事务边界并在方法返回之前提交或回滚事务,否则容器将回滚事务并抛出异常;
对于Message-Driven Bean而言,如果开发者在onMessage方法中使用了UserTransaction方法划分事务边界,则必须在方法返回前提交或回滚事务,否则容器将回滚事务;
对于Entity Bean而言,由于实例在不同的客户端之间共享,容器使用事务对不同的客户端调用进行同步,因此,Entity Bean的事务类型不能是Bean管理事务,只能是容器管理事务,当在Entity Bean中调用javax.ejb.EntityContext接口的getUserTransaction方法时,容器将抛出java.lang.IllegalStateException异常。
当客户端对Bean管理事务的Session Bean进行调用时,根据客户端调用是否伴随事务上下文,容器将使用如下策略对实例的事务进行管理:
客户端请求不伴随事务
当客户端请求不伴随事务,并且实例无关联事务时,容器将在未指明的事务上下文(unspecified transaction context)中调用实例方法,这些方法中的对资源的操作不具有事务性,参见本节中的第 44.11.2 节 “客户端事务与未指明的事务上下文”。
当客户端请求不伴随事务,并且实例被关联到事务T2时(当客户端对有状态Session Bean实例进行的前一个方法调用开始了一个事务边界,而且在调用完成后未提交或回滚事务时,可能发生这种情况),容器将在此关联事务T2中进行方法调用,对于无状态的Session Bean,这种情况永远不会发生。
客户端请求伴随事务
当客户端请求伴随事务T1,并且实例无关联事务时,容器将暂停客户端事务,在未指明的事务上下文(unspecifiedtransaction context)中进行方法调用,在方法调用完成后,容器恢复原客户端伴随的事务T1。
当客户端请求伴随事务T1,并且实例被关联到事务时T2时(当客户端对有状态Session Bean实例进行的前一个方法调用开始了一个事务边界,而且在调用完成后未提交或回滚事务时,可能发生这种情况),容器将暂停T1,并在T2中进行方法调用,在方法完成后恢复T1。对于无状态的Session Bean,这种情况永远不会发生。
由于Message-driven Bean与客户端事务上下文无关,并且,使用容器管理事务的Message-driven Bean必须在onMessage方法中开始事务和在方法返回之前提交或回滚事务,因此,对于Bean管理事务的Message-driven Bean的方法调用运行在未指明的事务上下文中。
当使用容器管理事务时,开发者需要在部署描述文件中使用transaction-type元素指定事务管理类型为容器管理事务,并在Entity Bean的assembly-descriptor元素中,使用container-transaction元素对EJB方法的事务属性进行声明。
使用容器管理事务的EJB的业务方法和Message-driven Bean中的onMessage方法中,不可对相关的资源调用任何特定的事务管理方法,以免干扰容器对事务边界的划分。这些方法包括如java.sql.Connection接口的commit、setAutoCommit和rollback方法,javax.jms.Session接口的commit和rollback等方法。
使用容器管理事务的EJB的业务方法和Message-driven Bean中的onMessage方法中,不能试图访问和使用javax.transaction.UserTransaction接口。
下面是一个容器管理事务的EJB的业务方法的例子,此方法使用JDBC连接更新两个数据库中的相关数据,容器按照部署描述中的事务属性进行事务划分:
... public class MySessionEJB implements javax.ejb.SessionBean { EJBContext ejbContext; public void someMethod(...) { java.sql.Connection con1; java.sql.Connection con2; java.sql.Statement stmt1; java.sql.Statement stmt2; // 获取con1和con2连接对象 con1 = ...; con2 = ...; stmt1 = con1.createStatement(); stmt2 = con2.createStatement(); // // 在con1和con2上进行更新操作。容器使用自动征用con1和 // con2对象,提供对con1和con2的操作的事务管理。 // stmt1.executeQuery(...); stmt1.executeUpdate(...); stmt2.executeQuery(...); stmt2.executeUpdate(...); stmt1.executeUpdate(...); stmt2.executeUpdate(...); // 释放连接 con1.close(); con2.close(); } ... }
此范例Session Bean的事务属性相关的部署描述文件如下:
<ejb-jar> <display-name>MySessionEJB</display-name> <enterprise-beans> <session> <display-name>MySessionEJB</display-name> ... </session> ... </enterprise-beans> <assembly-descriptor> ... <container-transaction> ... <method> ... <ejb-name>MySessionEJB</ejb-name> <method-name>someMethod</method-name> ... </method> <trans-attribute>Required</trans-attribute> ... </container-transaction> ... </assembly-descriptor> </ejb-jar>
使用容器管理持事务划分的EJB实例可调用伴随此实例javax.ejb.EJBContext接口中的setRollbackOnly和getRollbackOnly方法,对实例方法的事务的回滚标记进行设置和对当前事务是否已设置回滚标记进行检查。
通常情况下,开发者需要在抛出应用级异常之前,设置调用实例伴随的EJBContext接口的setRollbackOnly方法,把当前事务标记为回滚以保持数据完整性,因为应用级异常不会使容器自动回滚事务。例如,在一个进行汇款操作的业务方法中,可能涉及两个账户的操作,当减少一个账户的余额成功而增加另一账户余额的操作失败时,则需要调用setRollbackOnly方法,将当前事务标记为回滚。
使用容器管理事务的EJB可调用javax.ejb.EJBContext的getRollbackOnly方法检查当前事务是否被标记为回滚。此标记可由实例本身进行设置,也可由其他EJB或其他组件进行设置。
如采用了容器管理事务类型的EJB,开发者不能在单个事务中使用JMS的请求和响应模式(如发送一条JMS消息之后,同步地接收消息的响应)。这是因为如果直到事务提交时,消息仍未到达终点,则此事务边界内将永远不可能接收到消息的响应。
由于容器将代表EJB,对JMS Session进行事务性的征用(enlistment),createQueueSession(boolean transacted, int acknowledgeMode)和createTopicSession(boolean transacted, int acknowledgeMode)方法中的参数将被忽略。并且根据EJB规范,推荐开发者指定此transacted参数为真,并指定acknowledgeMode为零。
另外,开发者不能在一个事务内或一个未指明事务上下文的事务内使用JMS API中的acknowledge方法。未指明事务上下文的事务内的消息通知由容器进行。
对容器管理事务的Session Bean和Entity Bean的Home接口和组件接口中定义的方法,以及Message-driven Bean中的onMessage方法,开发者可以设置其事务属性。当客户端通过Home接口或组件接口调用这些方法时,或者当消息到达时onMessage方法被调用时,容器将按照开发者指定的事务属性对以上方法的事务进行管理。
当使用容器管理事务的EJB时,开发者必须指定以下方法的事务属性:
Session Bean中,除了在javax.ejb.EJBObject接口和javax.ejb.EJBLocalObject接口中定义的方法,其他定义在组件接口中的方法,以及定义在组件接口直接或非直接地继承的接口中的方法,开发者必须指定方法的事务属性。Session Bean的Home接口中的方法不可指定其事务属性;
Entity Bean中,除了getEJBHome、getEJBLocalHome、getHandler、getPrimaryKey和isIdentical方法之外,其他定义在组件接口中的方法,以及定义在组件接口直接或非直接地继承的接口中的方法,开发者必须指定方法的事务属性。除了远程Home接口中的getEJBMetaData和getHomeHandler方法,其他定义在Home接口中的方法,以及定义在Home接口直接或非直接地继承的接口中的方法,开发者必须指定方法的事务属性;
Message-driven Bean中,开发者必须指定onMessage方法的事务属性。
开发者可指定的事务属性包括以下几种:NotSupported、Required、Supports、RequiresNew、Mandatory和Never。
下面对这六种事务属性具有的事务语义分别进行描述。
当某方法的事务属性被指定为NotSupported时,容器将在未指明的事务上下文中调用此方法。具体情形如下:
当客户端请求伴随事务上下文,容器将在调用EJB的业务方法之间,暂停当前线程伴随的事务上下文。当方法调用完成之后,容器恢复前面暂停的事务上下文。伴随客户端调用事务上下文不会传递到资源管理器或业务方法中调用的其他组件。 如此业务方法调用了其他组件,则此调用不伴随任何事务。
当客户端请求不伴随事务上下文,容器也将不在事务上下文中进行此方法调用。
另外,如果方法中调用了其他的组件,则调用将不会伴随事务上下文。
当某方法的事务属性被指定为Required时,容器将在有效的事务上下文中调用此方法。具体情形如下:
当客户端请求伴随事务上下文,则容器在此事务上下文中调用此业务方法。
如客户端请求不伴随事务上下文,容器在调用此业务方法之前,将自动开始一个新的事务,并在此事务中自动征用(enlist)此业务方法访问的所有事务性资源,如方法中调用了其他的EJB组件,则调用将伴随此容器开始的事务。当容器对业务方法的调用完成,容器将试图提交事务。在方法调用结果返回客户端之前,容器将执行事务的提交协议(如两阶段提交协议)。
当某方法的事务属性被指定为Supports时,容器将按如下规则调用此方法:
如客户端请求伴随事务上下文,容器按照事务属性为Required的情况,使用相同步骤进行处理;
如客户端请求不伴随事务上下文,容器按照事务属性为NotSupported的情况,使用相同步骤进行处理。
![]() | 注意 |
---|---|
使用Supports事务属性时必须注意,因为根据客户端请求是否伴随事务上下文,决定了不同的执行模式,进而决定了不同的方法事务语义,只有在这两种不同的执行模式中都能正确运行的方法,才能考虑使用Supports事务属性。 |
当某方法的事务属性被指定为RequiresNew时,容器将在一个新的事务上下文中调用此方法。具体情形如下:
当客户端请求不伴随事务上下文,容器在调用此业务方法之前,将自动开始一个新的事务,并在此事务中自动征用(enlist)此业务方法访问的所有事务性资源,如方法中调用了其他的EJB组件,则调用将伴随此容器开始的事务。当容器对业务方法的调用完成,容器将试图提交事务。在方法调用结果返回客户端之前,容器将执行事务的提交协议(如两阶段提交协议)。
当客户端请求伴随事务上下文,容器在开始一个新的事务和调用业务方法之前,将自动暂停当前客户端线程伴随的事务上下文。在方法调用和容器开始的事务完成之后,容器将恢复前面暂停的客户端事务。
当某方法的事务属性被指定为Mandatory时,容器将在客户端的事务上下文中调用此方法。客户端调用此方法时,必须伴随事务:
如客户端请求伴随事务上下文,容器按照事务属性为Required的情况,使用相同步骤进行处理;
如客户端请求不伴随事务上下文,如果客户端为远程客户端,则容器将抛出javax.transaction.TransactionRequiredException异常;如果客户端为本地客户端,则容器将抛出javax.transaction.TransactionRequiredLocalException异常;
从以上部分中,可以了解到Apusic应用服务器中,不同的事务划分方式的特点与用法,以及其对应的事务语义。除了本章中列出的有关EJB事务方面的开发规则之外,开发者在编写EJB还应该注意以下准则:
应尽可能地使用声明型的、容器管理的事务类型,避免代码中充斥着对事务管理API的调用,这不仅能减少开发者需要做的工作,同时也能减少应用最终发布时容器产生错误的代码,而且,可以改变应用的行为而不需要修改代码;
处于分布式环境中的事务要在尽量小的时间内提交或回滚,事务应该在接收到客户端请求之后开始并在对客户端进行响应时结束。当用户对响应返回的的数据进行操作时,事务不应该还处于活动的状态。这样,因为数据库的锁被保持在更短的时间之内,可以减少应用对资源的争用;
Session Bean通常作为一组相互关联的Entity Bean的前端,以将Entity Bean中相互关联的方法组合到一个事务中,这样,Session Bean的方法即可作为一个单个的工作单元。只有在方法内部需要控制更高级、更复杂的业务逻辑时,才采用Bean管理事务的方式。但是,使用容器管理事务的方式必须作为优先的考虑;
应避免在JSP、Servlet、Java应用客户端等客户端中划分事务的方式,这种方式具有使用Bean管理事务带来的缺点,同时还违反了多层企业应用中划分层次的原则,错误地把系统中的关键部分(事务管理)放到了整个系统的前端或图形界面的逻辑中。
综上所述,EJB中的事务模型划分需要开发者进行全面的考虑,同时也需要注意采用不同的事务划分方式对应用开发带来的影响。