透明的事务控制
两种类型的 J2EE 事务 ― 容器管理的和 bean 管理的 ― 在如何启动和结束事务上是不同的。事务启动和结束的地方被称为 事务划分(transaction demarcation)。清单 1 中的示例代码演示了 bean 管理的事务(有时也称为 编程(programmatic)事务)。Bean 管理的事务是由组件使用 UserTransaction 类显式启动和结束的。通过 ejbContext 使 UserTransaction 对 EJB 组件可用,通过 JNDI 使其对其它 J2EE 组件可用。
容器根据组件的部署描述符中的事务属性代表应用程序透明地启动和结束容器管理的事务(或称为 宣告式事务(declarative transaction))。通过将 transaction-type 属性设置为 Container 或 Bean 您可以指出 EJB 组件是使用 bean 管理的事务性支持还是容器管理的事务性支持。
使用容器管理的事务,您可以在 EJB 类或方法级别上指定事务性属性;您可以为 EJB 类指定缺省的事务性属性,如果不同的方法会有不同的事务性语义,您还可以为每个方法指定属性。这些事务性属性在装配描述符(assembly descriptor)的 container-transaction 部分被指定。清单 2 显示了一个装配描述符示例。 trans-attribute 的受支持的值有:
Supports
Required
RequiresNew
Mandatory
NotSupported
Never
trans-attribute 决定方法是否支持在事务内部执行、当在事务内部调用方法时容器会执行什么操作以及在事务外部调用方法时容器会执行什么操作。最常用的容器管理的事务属性是 Required 。如果设置了 Required ,过程中的事务将在该事务中征用您的 bean,但如果没有正在运行的事务,容器将为您启动一个。在这个系列的第 3 部分,当您可能想使用每个事务属性时,我们将研究各个事务属性之间的区别。
清单 2. EJB 装配描述符样本
2 ...
3 <container-transaction>
4 <method>
5 <ejb-name>MyBean</ejb-name>
6 <method-name>*</method-name>
7 </method>
8 <trans-attribute>Required</trans-attribute>
9 </container-transaction>
10 <container-transaction>
11 <method>
12 <ejb-name>MyBean</ejb-name>
13 <method-name>updateName</method-name>
14 </method>
15 <trans-attribute>RequiresNew</trans-attribute>
16 </container-transaction>
17 ...
18 </assembly-descriptor>
19
功能强大,但很危险
与清单 1 中的示例不同,由于有宣告式事务划分,这段组件代码中没有事务管理代码。这不仅使结果组件代码更加易读(因为它不与事务管理代码混在一起),而且它还有另一个更重要的优点 ― 不必修改,甚至不必访问组件的源代码,就可以在应用程序装配时改变组件的事务性语义。
尽管能够指定与代码分开的事务划分是一种非常强大的功能,但在装配时做出不好的决定会使应用程序变得不稳定,或者严重影响它的性能。对容器管理的事务进行正确分界的责任由组件开发者和应用程序装配人员共同担当。组件开发者需要提供足够的文档说明组件是做什么的,这样应用程序部署者就能够明智地决定如何构建应用程序的事务。应用程序装配人员需要理解应用程序中的组件是怎样相互作用的,这样就可以用一种既强制应用程序保持一致又不削弱性能的方法对事务进行分界。在这个系列的第 3 部分中我们将讨论这些问题。
透明的事务传播
在任何类型的事务中,资源征用都是透明的;容器自动将事务过程中使用的任意事务性资源征调到当前事务中。这个过程不仅扩展到事务性方法使用的资源(比如在清单 1 中获得的数据库连接),还扩展到它调用的方法(甚至远程方法)使用的资源。我们来看一下这是如何发生的。
容器用线程与事务相关联
我们假设对象 A 的 methodA() 启动一个事务,然后调用对象 B 的 methodB() (对象 B 将得到一个 JDBC 连接并更新数据库)。 B 获得的连接将被自动征调到 A 创建的事务中。容器怎么知道要做这件事?
当事务启动时,事务上下文与执行线程关联在一起。当 A 创建事务时, A 在其中执行的线程与该事务关联在一起。由于本地方法调用与主调程序(caller)在同一个线程内执行,所以 A 调用的每个方法也都在该事务的上下文中。
橱中骸骨
如果对象 B 其实是在另一个线程,甚至另一个 JVM 中执行的 EJB 组件的存根,情况会怎样?令人吃惊的是,远程对象 B 访问的资源仍将在当前事务中被征用。EJB 对象存根(在主调程序的上下文中执行的那部分)、EJB 协议(IIOP 上的 RMI)和远端的骨架对象协力要使其透明地发生。存根确定调用者是不是正在执行一个事务。如果是,事务标识,或者说 Xid,被作为 IIOP 调用的一部分与方法参数一起传播到远程对象。(IIOP 是 CORBA 远程-调用协议,它为传播执行上下文(比如事务上下文和安全性上下文)的各种元素而备;关于 RMI over IIOP 的更多信息,请参阅 参考资料。)如果调用是事务的一部分,那么远程系统上的骨架对象自动设置远程线程的事务上下文,这样,当调用实际的远程方法时,它已经是事务的一部分了。(存根和骨架对象还负责开始和提交容器管理的事务。)
事务可以由任何 J2EE 组件来启动 ― 一个 EJB 组件、一个 servlet 或者一个 JSP 页面(如果容器支持的话,还可以是一个应用程序客户机)。这意味着,应用程序可以在请求到达时在 servlet 或者 JSP 页面中启动事务、在 servlet 或者 JSP 页面中执行一些处理、作为页面逻辑的一部分访问多个服务器上的实体 bean 和会话 bean 并使所有这些工作透明地成为一个事务的一部分。图 1 演示了事务上下文怎样遵守从 servlet 到 EJB,再到 EJB 的执行路径。
图 1.单个事务中的多个组件

最优化
让容器来管理事务允许容器为我们做出某些最优化决定。在图 1 中,我们看到一个 servlet 和多个 EJB 组件在单个事务的上下文中访问一个数据库。每个组件都获得到数据库的 Connection ;很可能每个组件都在访问同一个数据库。即使多个连接是从不同的组件到同一个资源,JTS 也可以检测出多个资源是否和事务有关,并最优化该事务的执行。您可以从第 1 部分回忆起来,单个事务要包含多个资源管理器需要使用两阶段提交协议,这比单个资源管理器使用的单阶段提交代价要高。JTS 能够确定事务中是不是只征用了一个资源管理器。如果它检测出所有与事务相关的资源都一样,它可以跳过两阶段提交并让资源管理器自己来处理事务。
结束语
这个虑及透明事务控制、资源征用和透明传播的魔术不是 JTS 的一部分,而是 J2EE 容器如何在幕后代表 J2EE 应用程序使用 JTA 和 JTS 服务的一部分。在幕后有许多实体合力使这个魔术透明地发生;EJB 存根和骨架、容器厂商提供的 JDBC 驱动器包装器、数据库厂商提供的 JDBC 驱动器、JMS 提供器和 JCA 连接器。所有这些实体都与事务管理器进行交互,于是应用程序代码就不必与之交互了。
在第 3 部分,我们将看一下关于管理 J2EE 上下文中事务的一些实际问题 ― 事务划分和孤立 ― 以及它们对应用程序一致性、稳定性和性能的影响。