概述
JTS 只是一個組件事務監視器(有時也稱為 對象事務監視器(object transaction monitor)),或稱為 CTM。
JTS 和 J2EE 的事務支持設計受 CORBA 對象事務服務(CORBA Object Transaction Service,OTS)的影響很大。實際上,JTS 實現 OTS 並充當 java 事務 API(Java Transaction API)― 一種用來定義事務邊界的低級 API ― 和 OTS 之間的接口。使用 OTS 代替創建一個新對象事務協定遵循了現有標準,並使 J2EE 和 CORBA 能夠互相兼容。
乍一看,從程式化事務監視器到 CTM 的轉變好像只是術語名稱改變了一下。然而,差別不止這一點。當 CTM 中的事務提交或回滾時,與事務相關的對象所做的全部更改都一起被提交或取消。但 CTM 怎么知道對象在事務期間做了什麼事?象 EJB 組件之類的事務性組件並沒有 commit() 或 rollback() 方法,它們也沒向事務監視器註冊自己做了什麼事。那么 J2EE 組件執行的操作如何變成事務的一部分呢?
透明的資源徵用
當應用程式狀態被組件操縱時,它仍然存儲在事務性資源管理器(例如,資料庫和訊息佇列伺服器)中,這些事務性資源管理器可以註冊為分散式事務中的資源管理器。在第 1 部分中,我們討論了如何在單個事務中徵用多個資源管理器,事務管理器如何協調這些資源管理器。資源管理器知道如何把應用程式狀態中的變化與特定的事務關聯起來。
但這只是把問題的焦點從組件轉移到了資源管理器 ― 容器如何斷定什麼資源與該事務有關,可以供它徵用?請考慮下面的代碼,在典型的 EJB 會話 bean 中您可能會發現這樣的代碼:
清單 1. bean 管理的事務的透明資源徵用
InitialContext ic = new InitialContext();
UserTransaction ut = ejbContext.getUserTransaction();
ut.begin();
DataSource db1 = (DataSource) ic.lookup("java:comp/ENV/OrdersDB");
DataSource db2 = (DataSource) ic.lookup("java:comp/env/InventoryDB");
Connection con1 = db1.getConnection();
Connection con2 = db2.getConnection();
// perform updates to OrdersDB using connection con1
// perform updates to InventoryDB using connection con2
ut.commit();
注意,這個示例中沒有徵用當前事務中 JDBC 連線的代碼 ― 容器會為我們完成這個任務。我們來看一下它是如何發生的。
資源管理器的三種類型
當一個 EJB 組件想訪問資料庫、訊息佇列伺服器或者其它一些事務性資源時,它需要到資源管理器的連線(通常是使用 JNDI)。而且,J2EE 規範只認可三種類型的事務性資源 ― JDBC 資料庫、JMS 訊息佇列伺服器和“其它通過 JCA 訪問的事務性服務”。後面一種服務(比如 ERP 系統)必須通過 JCA(J2EE Connector Architecture,J2EE 連線器體系結構)訪問。對於這些類型資源中的每一種,容器或提供者都會幫我們把資源徵調到事務中。
在清單 1 中, con1 和 con2 好象是普通的 JDBC 連線,比如那些從 DriverManager.getConnection() 返回的連線。我們從一個 JDBC DataSource 得到這些連線,JDBC DataSource 可以通過查找 JNDI 中的數據源名稱得到。EJB 組件中被用來查找數據源( java:comp/env/OrdersDB )的名稱是特定於組件的;組件的部署描述符的 resource-ref 部分將其映射為容器管理的一些應用程式級 DataSource 的 JNDI 名稱。
隱藏的 JDBC 驅動器
每個 J2EE 容器都可以創建有事務意識的池態 DataSource 對象,但 J2EE 規範並不向您展示如何創建,因為這不在 J2EE 規範內。瀏覽 J2EE 文檔時,您找不到任何關於如何創建 JDBC 數據源的內容。相反,您不得不為您的容器查閱該文檔。創建一個數據源可能需要向屬性或配置檔案添加一個數據源定義,或者也可以通過 GUI 管理工具完成,這取決於您的容器。
每個容器(或連線池管理器,如 PoolMan)都提供它自己的創建 DataSource 機制,JTA 魔術就隱藏在這個機制中。連線池管理器從指定的 JDBC 驅動器得到一個 Connection ,但在將它返回到應用程式之前,將它與一個也實現 Connection 的虛包包在一起,將自己置入應用程式和底層連線之間。當創建連線或者執行 JDBC 操作時,包裝器詢問事務管理器當前執行緒是不是正在事務的上下文中執行,如果事務中有 Connection 的話,就自動徵用它。
其它類型的事務性資源,JMS 訊息佇列和 JCA 連線器,依靠相似的機制將資源徵用隱藏起來,使用戶看不到。如果要使 JMS 佇列在部署時對 J2EE 應用程式可用,您就要再次使用特定於提供者的機制來創建受管 JMS 對象(佇列連線工廠和目標),然後在 JNDI 名稱空間內發布這些對象。提供者創建的受管對象包含與 JDBC 包裝器(由容器提供的連線池管理器添加)相似的自動徵用代碼。
透明的事務控制
兩種類型的 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 裝配描述符樣本
<assembly-descriptor>
...
<container-transaction>
<method>
<ejb-name>MyBean</ejb-name>
<method-name>*</method-name>
</method>
<trans-attribute>Required</trans-attribute>
</container-transaction>
<container-transaction>
<method>
<ejb-name>MyBean</ejb-name>
<method-name>updateName</method-name>
</method>
<trans-attribute>RequiresNew</trans-attribute>
</container-transaction>
...
</assembly-descriptor>
功能強大,但很危險
與清單 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 的執行路徑。