本系列分為三部分,將探索 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 管理的事務。可能的值是 container 或 bean。由於實體 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.Connection 的 commit、setAutoCommit 和 rollback 方法
javax.ejb.EJBContext 的 getUserTransaction 方法
javax.transaction.UserTransaction 的任何方法
回滾
在某些情況下您可能需要明確中止事務。有兩種回滾容器管理的事務的方式:
讓容器自動回滾事務。如果有任何企業 bean 拋出的運行時異常,就會發生這種回滾。
調用 EJBContext 接口的 setRollBackOnly() 方法。在發生回滾時,允許您進行控制。或許由於一些有效性驗證失敗或存在數據完整性問題,您可能需要回滾整個事務並拋出一個應用程序異常。應用程序異常不會自動導致容器回滾一個異常。
結束語
在本系列的第 1 部分,簡單介紹了事務和 EJB 事務的兩個選擇。您可以看到容器管理的事務如何使您專注於您的企業 bean 的業務邏輯,而將事務邏輯和管理的復雜性留給 EJB 容器。
使用容器管理的事務,您只需要關心企業 bean 如何參與到事務中,並通過 EJB 部署描述符的簡單配置來實現這一點。Geronimo 應用服務器、OpenEJB 和 XDoclet 將幫助您簡化如何指定容器管理的設置,並將您從繁重的 EJB 工件編碼工作中解放出來。
繼續關注本系列的第 2 部分,您將了解 bean 管理的事務,在第 3 部分中,我們會將二者綜合使用。