既然您已經在 第 1 部分 學習了有關 Enterprise JavaBeans (EJB) 容器管理的事務的所有細節,那麼您可能對 EJB bean 管理的事務以及如何在 Apache Geronimo 應用服務器中實現它感到好奇。在由三部分組成的 系列文章 的這一期文章(第 2 部分)中,您可以獲得所有細節。
簡介
本系列分為三部分,將探索 Geronimo 和 OpenEJB 可以為您提供什麼幫助,以及在 EJB 2.1 中現在可以實現的 EJB 事務概念(讓您順利進入 EJB 3.0)。第 2 部分(即本文)將詳細描述 EJB bean 管理的事務,研究可以生成的兩種 bean 管理的事務(Java Transaction API (JTA) 和 Java Database Connectivity (JDBC) 事務)的用法。您將了解這些事務的回滾和如何在 Geronimo 中使用 bean 管理的事務。
現在簡要回顧一下第 1 部分中涵蓋的內容:使用 EJB 事務,您可以從下列兩個選項中進行選擇:
您可以將事務實現委托給 EJB 容器。這就是容器管理的事務,也是本系列文章第 1 部分的焦點。請參閱 第 1 部分,獲得對事務和容器管理的事務的簡介。
在提交事務和將其回滾時,允許通過編程方式管理企業 bean。應用程序開發人員必須明確將事務邏輯編入企業 bean 代碼來實現它。這就是 bean 管理的事務,是本系列的第 2 部分(也就是本文)的焦點。
在第 3 部分中,您將了解與容器管理的事務和 bean 管理的事務有關的難題和附加特性。
bean 管理的事務
與 bean 管理的事務相比較,容器管理的事務更簡單且代碼更少。但是在使用容器管理的事務時存在以下情況:您的企業 bean 方法既可以參與到事務中,也可以不參與。如果您需要更粗略的邏輯,並在基於特定有效性邏輯的情況下使用粗略邏輯提交事務或回滾事務,那麼您應該使用 bean 管理的事務。bean 管理的事務可使您對事務邊界進行全面控制。
會話 bean 或消息驅動 bean (MDB) 可以使用 bean 管理的事務。實體 bean 不能使用 bean 管理的事務。這是因為 EJB 容器控制了加載或存儲實體 bean 的數據的時間。
在 bean 管理的事務中,容器必須允許 bean 實例在一個方法中連續執行幾個事務,但是要記住,不能執行嵌套事務。如果您試圖啟動一個新事務而 bean 實例還沒有提交前一個事務,那麼容器將拋出一個異常。
您可以使用兩種類型的 bean 管理的事務:
JTA 事務
JDBC 事務
我們將在下一節中查看這兩種事務。
JTA 事務
JTA 是事務管理器和分布式事務處理系統所涉及的其他組件之間的一個接口規范。通過使用接口,不必使用事務管理器的特有 API 就可以單獨地劃分事務。
所有 EJB 容器都必須支持 JTA API。對於一名開發人員,這允許您將事務指令傳達給 EJB 容器,並以通用、簡單的方式提供事務指令。此方法使您不必擔心事務服務的底層工作。
javax.transaction.UserTransaction
使用 bean 管理的 JTA 事務時,可使用 javax.transaction.UserTransaction 接口來劃分事務。在該接口上,有三個有趣的方法:
begin() —— 創建一個新的事務,並將該事務與當前線程關聯起來。
commit() —— 提交與當前線程有關聯的事務。
rollback() —— 強行回滾與當前線程有關聯的事務。
在 begin() 和 commit() 之間發生的所有更新都是在一個事務中完成的。
UserTransaction 接口上的其他方法包括:
getStatus() —— 檢索與當前線程有關的事務的狀態。返回值是 javax.transaction.Status 接口上定義的常數。
setRollbackOnly() —— 強行使事務終止。
setTransactionTimeout(int) —— 設置事務中止前能運行的最大次數。這在避免死鎖時很有用。
請參閱本文的 參考資料 部分,獲得 Sun 公司的 JavaDocs 中有關 UserTransaction 和 UserStatus 接口的鏈接。
如何使用 bean 方法獲得 UserTransaction 的最初引用呢?基於企業 bean 的類型,您可從 bean 上下文中獲得接口:
對於會話 bean,可從 javax.ejb.EJBContext 調用 getUserTransaction()。
對於 MDB,可從 MessageDrivenContext.getUserTransaction() 調用 getUserTransaction()。
您也可以通過 JNDI 檢索接口。清單 1 顯示了一個例子。
清單 1. 如何通過 JNDI 獲取 UserTransaction 接口
public MySessionBean implements SessionBean {
public someMethodOnMyBean()
{
Context initCtx = new InitialContext();
UserTransaction utx = (UserTransaction)initCtx.lookup(
"java:comp/UserTransaction");
utx.begin();
...
utx.commit();
}
...
}
通常,在同一個方法中啟動事務並提交該事務是一個好主意。這有助於您跟蹤事務開始和結束的地方。同樣,要使事務開放的時間盡可能的短。事務需要系統資源,因此如果保持事務長時間的開放,可能會影響多用戶性能。
使用 JTA 事務的最大好處是它允許您跨越多個不同數據庫的多個更新。但要記住,JTA 實現不支持嵌套事務。
清單 2 顯示了會話 bean 更新兩個不同數據庫的例子。數據源是通過 JNDI 進行檢索的。您可以像往常一樣檢索數據庫連接並准備語句。
(注意,我在演示 JTA 事務的基本實現(用粗體顯示)。為了實現這一點,我將用頂級方法來顯示所有代碼。不要嘗試使用任何重用形式的普通抽象。)
清單 2. 會話 bean Java 代碼示例
public class MySessionEJB implements SessionBean {
EJBContext ejbContext;
public void updateTwoDatabases(...) {
DataSource dbOneDataSource = null;
DataSource dbTwoDataSource = null;
Connection dbOneConnection = null;
Connection dbTwoConnection= null;
Statement dbOneStatement = null;
Statement dbTwoStatement = null;
String sqlUpdateDbOne = null;
String sqlUpdateDbTwo = null;
javax.transaction.UserTransaction ut = null;
try {
InitialContext initCtx = new InitialContext();
// retrieve our first Connection and
// prepare a statement
dbOneDataSource = (javax.sql.DataSource)
initCtx.lookup("java:comp/env/jdbc/dbOneDS");
dbOneConnection = dbOneDataSource.getConnection();
// setup SQL here,
// perhaps we're using parameterized queries
sqlUpdateDbOne = ...
dbOneStatement =
dbOneConnection.prepareStatement(sqlUpdateDbOne);
// retrieve our second Connection and
// prepare a statement
dbTwoDataSource = (javax.sql.DataSource)
initCtx.lookup("java:comp/env/jdbc/dbTwoDS");
dbTwoConnection = dbTwoDataSource.getConnection();
// setup SQL here,
// perhaps we're using parameterized queries
sqlUpdateDbTwo = ...
dbTwoStatement =
dbTwoConnection.prepareStatement(sqlUpdateDbTwo);
// Now setup a JTA transaction to encompass both
// database updates //
ut = ejbContext.getUserTransaction();
// Start our transaction here
ut.begin();
dbOneStatement.executeUpdate();
dbTwoStatement.executeUpdate();
// commit the transaction
ut.commit();
// cleanup
dbOneStatement.close();
dbTwoStatement.close();
dbOneConnection.close();
dbTwoConnection.close();
}
catch (Exception e) {
//something went wrong
try {
//rollback the exception
ut.rollback();
} catch (SystemException se) {
//Rollback has failed!
throw new EJBException
("Attempted to rollback, but it failed: "
+ se.getMessage());
}
//Lastly throw an exception to report what went wrong
throw new EJBException
("Transaction failed: " + ex.getMessage());
}
}
如您所見,JTA bean 管理的事務的基本實現非常簡單。在事務啟動、提交和回滾時,所有您要做的就是明確控制事務。
JDBC 事務
如果您過去曾經編寫過 JDBC 代碼,那麼實現 JDBC 事務對於您來說應該很熟悉。只有在具有現有的 JDBC 代碼,並且必須在企業 bean 實現中使用它時,才使用 JDBC 事務,不過,這被認為是傳統處理方式。讓我們更近一步了解這類事務。
在執行一個正常地 JDBC 更新時,所有 SQL 語句都被自動提交。為了自己控制事務邊界,在使用前可通過在 Connection 對象上發布 setAutoCommmit(false) 語句將該功能關閉。如果需要更好控制 SQL 執行,請使用該選項。
由 java.sql.Connection 接口提供 commit 和 rollback 方法,並由數據庫的事務管理器(而不是 EJB 容器)來控制 JDBC 事務。下面的 清單 3 闡明了這一點,顯示了一個虛構的會話 bean 方法,該方法使用標准 JDBC 預備語句更新數據庫中客戶的名字。
注意,這裡沒有抽象。所有代碼都是使用頂級 bean 編寫的。不能使用重用和零委托。請不要將這作為一種方法使用。記住,我將演示如何聲明事務邊界,以便更好地實現 JDBC 遺留代碼的事務控制。
清單 3. JDBC 事務的用法
public void updateFirstName (int customerId, String firstName) {
try {
//getConnection creates a JDBC connection for us
Connection con = getConnection();
con.setAutoCommit(false);
String sql = "update customer set firstName = ? where customerId = ?";
PreparedStatement updateCustomerName = con.prepareStatement(sql);
updateCustomerName.setString(1,firstName);
updateCustomerName.setInt(2,customerId);
updateCustomerName.executeUpdate();
con.commit();
} catch (Exception ex) {
try {
con.rollback();
throw new EJBException("Customer name update
failed: " + ex.getMessage());
} catch (SQLException sqx) {
throw new EJBException("Rollback failed: " +
sqx.getMessage());
}
}
}
因此可以用來封裝遺留 JDBC 代碼的常用方法是:
在連接對象上將 setAutoCommit 屬性設置為 false。
在執行 SQL 之後調用連接上的 commit 方法。
如果出現任何錯誤,則調用連接上的 rollback 方法。
此外,JDBC 事務不是實現 bean 管理的事務的首選方式。如果不需要重用現有 JDBC 代碼,則應該考慮使用 JTA 事務。
提交事務或回滾事務
因為由您來控制事務邊界,所以當提交或回滾 bean 管理的事務時,有些特定的規則您必須知道:
在從業務方法中返回或發生 ejbTimeout 之前,無狀態會話 bean 必須提交或回滾事務。容器可能會檢測事務啟動時的情況,但是不會提交事務。在這種情況下,容器將回滾事務並拋出一個異常。
在從業務方法中返回之前,有狀態會話 bean 不必提交或回滾事務。這是因為事務可以跨越幾個客戶端調用。有狀態會話 bean 表示了會話狀態。EJB 容器將等待會話 bean 實例提交或回滾事務。
在消息監聽器方法或 ejbTimeout 方法返回之前,MDB 必須提交事務。在這種情況下,容器將回滾事務並拋出一個異常。
注意,對於 JTA 事務,如果數據庫在多個調用之間打開或關閉數據庫連接,則仍將保留事務。但是,對於 JDBC 事務,將不保留事務。
避免使用的方法
在處於事務中時,不要調用 java.sql.Connection 或 javax.jms.Session 接口的 commit() 或 rollback()。
同樣,不要調用 EJBContext 接口的 getRollBackOnly() 和 setRollBackOnly()。容器將拋出一個異常,原因是:
您可以通過調用 javax.transaction.UserTransaction 的 getStatus() 方法來獲得事務的狀態。這等同於調用 getRollBackOnly。
可以使用 javax.transaction.UserTransaction 接口的 rollback() 方法來回滾事務。這等同於調用 setRollbackonly()。
Geronimo 配置
要與 Geronimo 一起使用 bean 管理的事務時,沒有很多配置工作需要您做!因為事務邊界是通過編程控制的,您要像本文前一小節中所顯示的那樣,在代碼中處理大部分工作。
OpenEJB 是 Geronimo 的 EJB 容器實現。您只需要負責配置好 OpenEJB 來使用 bean 管理的事務即可。可以用部署描述符對每個企業 bean 進行配置。
要使用 bean 管理的事務,則需要使用 EJB 部署描述符中的 <transaction-type> 元素來指定值 Bean。
您可以使用 XDoclet 來生成實現和使用 EJB 框架所需的編程工件中更單調的方面。這包括 EJB 部署描述符。因此要指定 bean 管理的事務,則需要使用 XDoclet 的類似 JavaDoc 的標識語言。
要為每個企業 bean 指示 bean 管理的事務,可以使用 XDoclet 標識來設置 @ejb.bean transaction-type="Bean"。
清單 4 顯示了一個會話 bean,它使用 XDoclet 來聲明事務類型標識(粗體顯示)。
清單 4. bean 管理的會話 bean 的 XDoclet 標識
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"
* transaction-type="Bean"
* ...
* ...
*/
public abstract class SampleSessionBean implements javax.ejb.SessionBean {
...
}
注意:用於指定容器管理的事務屬性的 XDoclet 標記與上述標記非常相似,因此不會被迷惑:
容器管理的事務 @ejb.transaction type="Required"
bean 管理的事務 @ejb.bean transaction-type="Bean"
如果沒有使用 XDoclet 標識指定 @ejb.bean 標簽,將使用默認事務邊界(這是容器管理的事務)。
清單 5 是名為 SampleSession 的無狀態會話 bean 生成 ejbjar.xml 的清單示例。在 Java 代碼中指定了 @ejb.bean transaction-type="Bean"。只需要注意 <transaction-type> 屬性(粗體)即可!
清單 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>Bean</transaction-type>
</session>
...
結束語
本文是由三部分組成的系列文章的第 2 部分,在本文中,回顧了 bean 管理的事務和有關使用兩種 bean 管理的事務(JTA 和 JDBC)的細節,還學習了如何在 Apache Geronimo 中使用 OpenEJB 實現 bean 管理的事務。繼續關注本系列的第 3 部分,我們將綜合使用 bean 管理的事務和容器管理的事務,並了解您面對兩種方法時的實現選擇。