同步、異步、周期以及事務(wù)地執(zhí)行工作 
David Currie 將繼續(xù)他的由三部分組成的系列文章,介紹 Java 2 企業(yè)版(J2EE)連接器架構(gòu)(JCA)最新版本中的增強和變化。在本文中,他將介紹新的 JCA 1.5 工作管理合約,該合約允許資源適配器利用應(yīng)用服務(wù)器的某些功能來調(diào)度和處理工作。在 JCA 的另一個增強 —— 事務(wù)流入 —— 的支持下,企業(yè)信息系統(tǒng)可以在自己的事務(wù)中執(zhí)行這項工作。

JCA 1.5 是 J2EE 連接器體系結(jié)構(gòu)的最新版本,它包含許多重要的改進和一些重要增強。本文是介紹這些變化的由三部分組成的系列文章的第 2 部分,建立在介紹的生命周期管理特性的第 1 部分的基礎(chǔ)上,本文介紹的是 JCA 新的工作管理合約。該合約允許資源適配器為延遲執(zhí)行或定期執(zhí)行的工作創(chuàng)建計時器,并允許計時器使用應(yīng)用服務(wù)器的線程同步或異步執(zhí)行處理。本文將描述事務(wù)流入支持如何在資源適配器導(dǎo)入到服務(wù)器的事務(wù)中進行這種處理,以及資源適配器隨后如何控制事務(wù)的完成。

如果想在現(xiàn)有資源適配器中使用這項功能,或者正在考慮編寫新的 JCA 1.5 資源適配器,那么本文是一份必不可少的讀物。如果要編寫使用資源適配器的應(yīng)用程序,想了解更多幕后的情況,那么本文也會吸引您。

讓工作完成
在本系列的第 1 部分中,我們介紹了 ResourceAdapter 接口,它提供了一種機制,能夠在應(yīng)用服務(wù)器內(nèi)部為資源適配器提供生命周期。您可能還記得 start 方法被用來傳遞一個叫做 BootstrapContext 的對象。上次我們介紹了這個對象,但是 BootstrapContext 接口的三個方法才是工作管理和事務(wù)流入合約的關(guān)鍵,如清單 1 所示:

清單 1. 用來得到三個工具對象的 BootstrapContext 接口
public interface BootstrapContext {

    WorkManager getWorkManager();
    XATerminator getXATerminator();
    Timer createTimer() throws UnavailableException;

}

WorkManager 允許資源適配器對工作進行調(diào)度,在應(yīng)用服務(wù)器線程上同步或異步執(zhí)行調(diào)度。這個工作可以在資源導(dǎo)入的事務(wù)中執(zhí)行,在這種情況下,XATerminator 有助于完成工作。Timer 負責延遲工作或定期工作的執(zhí)行。本文將更深入地研究這三個類,并說明如何使用它們。

WorkManager 接口提供了三套處理工作的方法(doWorkstartWorkscheduleWork),如清單 2 所示:

清單 2. 用來提交工作項目的WorkManager 接口
public interface WorkManager {
    
    void doWork(Work work) throws WorkException;
    void doWork(Work work, long startTimeout, ExecutionContext execContext,
            WorkListener workListener) throws WorkException;

    long startWork(Work work) throws WorkException;
    long startWork(Work work, long startTimeout, ExecutionContext execContext,
            WorkListener workListener) throws WorkException;

    void scheduleWork(Work work) throws WorkException;
    void scheduleWork(Work work, long startTimeout,
            ExecutionContext execContext, WorkListener workListener)
            throws WorkException;
    
}

每個方法接收的第一個參數(shù),都是實現(xiàn) Work 接口的對象的一個實例,如清單 3 所示:

清單 3. 資源適配器實現(xiàn)的 Work 接口
public interface Work extends Runnable {

    void release();
    
}

Work 接口擴展了 Runnable 接口,您應(yīng)當像直接進行 Java 線程編程時所做的那樣,實現(xiàn)run 方法中執(zhí)行的工作。您很快就會看到 release 方法發(fā)揮其作用的地方。

WorkManager 上的 doWork 方法可以讓一些工作同步執(zhí)行、一直受阻塞或者直到某些工作完成才執(zhí)行。這看起來可能不是特別有用——這不就是直接調(diào)用 run 方法時發(fā)生的事情嗎?并不完全如此。首先,它讓應(yīng)用程序服務(wù)器說明現(xiàn)在不是做這項工作的恰當時候。例如,如果在 ResourceAdapterstart 方法的范圍內(nèi)調(diào)用 doWork,那么您可能發(fā)現(xiàn)它將拋出 WorkRejectedException 異常。應(yīng)當盡快從這個方法返回,如果可能的話,應(yīng)當把工作安排成異步處理。

第二,如果應(yīng)用服務(wù)器特別繁忙,那么它可能會推遲這項工作的啟動。可以用第 2 個startTimeout 參數(shù)指明資源適配器準備為工作啟動等候多長時間。如果應(yīng)用服務(wù)器沒能在這個時間內(nèi)啟動工作,那么就會拋出 WorkRejectedException 異常。WorkManager 接口定義了常量 IMMEDIATEINDEFINITE,它們允許資源適配器指明自己根本不準備等候或者準備一直等候下去。

第三,正如下一節(jié)解釋的,有可能讓工作片斷在資源適配器導(dǎo)入的事務(wù)上下文中執(zhí)行,而不是在與當前線程關(guān)聯(lián)的上下文中執(zhí)行。這正是第 3 個參數(shù)的用途,該參數(shù)是一個可選的 ExecutionContext

最后,使用 doWork 方法讓應(yīng)用服務(wù)器對資源適配器執(zhí)行的工作擁有更多控制。在關(guān)閉應(yīng)用服務(wù)器時,如果資源適配器夾在一個漫長的、復(fù)雜的操作中間,那么服務(wù)器不需要等待資源適配器完成,或者被清理干凈。相反,服務(wù)器可以通過調(diào)用 Work 對象的 release 方法,給資源適配器發(fā)信號。然后資源適配器應(yīng)當盡快完成處理。清單 4 顯示了 release 方法使用的一個示例:

清單 4. Work 對象的示例
public class ExampleWork implements Work {

    private volatile boolean _released;

    void run() {
        for (int i = 0; i < 100; i++) {
            if (_released) break;
            // Do something
        }
    }

    void release() {
        _released = true;
    }
    
}

注意清單 4 中使用的關(guān)鍵字 volatile。在 run 方法正進行其處理的同時,會在一個獨立的線程上調(diào)用 release 方法,volatile 修飾符確保 run 方法能夠看到更新的字段。

doWork 方法也可能失敗,拋出一個名稱古怪的 WorkCompletedException 異常。這個異常是在將 run 方法分配給一個線程時拋出的,但這個異常或者是上下文設(shè)置失敗,或者是通過拋出運行時異常退出方法。doWork 方法會提供一個錯誤代碼,表示這些故障發(fā)生的路徑,還把問題原因作為鏈接的異常提供。

為什么等待?
您已經(jīng)看到 doWork 方法允許您在調(diào)用線程阻塞的同時執(zhí)行工作。但是如果您不想等待 —— 也就是說,如果您不想同步執(zhí)行工作,該怎么辦呢?在 Java 2 平臺標準版本(J2SE)環(huán)境中,可以使用多線程實現(xiàn)這一目標。但是,J2EE 規(guī)范讓應(yīng)用程序服務(wù)器使用 Java 2 安全管理器防止應(yīng)用程序離開自己的線程。如果應(yīng)用服務(wù)器的這部分功能是通過 EJB 池或 servlet 池來提供并發(fā)性,那么這樣做是合理的。服務(wù)器也可能想把各種形式的上下文都關(guān)聯(lián)到一個線程上,因為產(chǎn)生新線程時,上下文可能會丟失。最后,正如我以前討論過的,不受服務(wù)器控制的線程會造成有序關(guān)機很難實現(xiàn)。

雖然可以通過使用安全策略,逐個案例地克服這個限制,但是 WorkManager 上的 startWorkscheduleWork 方法可以讓資源適配器異步地處理工作,同時確保應(yīng)用程序服務(wù)器仍然在控制之下。startWork 方法會一直等候,直到工作片斷開始執(zhí)行,但不用等到工作結(jié)束。所以,如果調(diào)用器需要知道工作是否已經(jīng)得以執(zhí)行,但是不需要等到工作完成,那么可以使用這種方法。相比之下,只要該工作調(diào)度被接受,scheduleWork 方法就立即返回。在這種情況下,并不能確保工作真的被執(zhí)行。

清單 2 中的 WorkManager 方法的第 4 個參數(shù)(WorkListener)在用于這些異步方法時最有用。清單 5 顯示了這個接口:

清單 5. 接收事件通知的 WorkListener 接口
public interface WorkListener {

    void workAccepted(WorkEvent e);
    void workRejected(WorkEvent e);
    void workStarted(WorkEvent e);
    void workCompleted(WorkEvent e);
    
}

資源適配器能夠有選擇地傳遞一個監(jiān)聽器,當工作的項目通過接受、啟動或完成這幾個狀態(tài)(失敗的情況下則是拒絕)傳遞過來時,監(jiān)聽器會得到通知。在這個監(jiān)聽器已經(jīng)完成其工作項目,或者出現(xiàn)故障要重新安排工作項目時,可以用它將通知發(fā)送給工作的發(fā)起者。WorkAdapter 類也包含在內(nèi),它提供了所有方法的默認實現(xiàn),因此,子類只需覆蓋自己感興趣的方法即可。

WorkListener 的每個方法都采用 WorkEvent 對象作為參數(shù),如清單 6 所示:

清單 6. WorkEvent 類上的附加方法
public class WorkEvent extends EventObject {

    ...

    public int getType() { ... }
    public Work getWork() { ... }
    public long getStartDuration() { ... }
    public WorkException getException() { ... }
 
}

除了通常的事件方法之外,WorkEvent 類還為這些類型的事件 (接受、拒絕、啟動或完成)以及有問題的工作項目提供了存取器。這使您能夠用一個(多線程的)監(jiān)聽器負責多個工作提交。還有一些方法可以返回啟動工作所花費的時間,而且,在出現(xiàn) workRejectedworkCompleted 時,返回可能發(fā)生的對應(yīng)的 WorkRejectedExceptionWorkCompletedException 異常。

圖 1 顯示了通過 Work 對象傳遞的狀態(tài)。

圖 1. Work 對象的狀態(tài)
Work 對象的狀態(tài)

沿著圖 1 的底部,有三種提交方法,從上到下的點線表示具體的方法返回的生命周期中的時間點。

可以推遲到明天的事為什么要現(xiàn)在做?
doWorkstartWorkscheduleWork 方法可以都立即向 WorkManager 提交任務(wù)。在 WorkManager 執(zhí)行提交之前,可能會發(fā)生延遲,但是調(diào)用者只能控制最大延遲(用啟動超時)。那么如果想讓任務(wù)晚些而不是立即處理,該怎么辦呢?scheduleWork 方法只允許將工作安排到另一個線程,而不是安排到時間軸上的以后某個時間點上。

這正是 BootstrapContext 接口的第 3 個方法發(fā)揮其作用的地方。createTimer 方法使資源適配器能夠得到 java.util.Timer 類的實例。這個類從 1.3 版開始就已經(jīng)成為標準 Java 庫的一部分,如清單 7 所示:

清單 7. Timer 類的方法
public class Timer  {
 
    public void schedule(TimerTask task, long delay) { ... }
    public void schedule(TimerTask task, Date time) { ... }

    public void schedule(TimerTask task, Date firstTime, long period) { ... }
    public void schedule(TimerTask task, long delay, long period) { ... }
    public void scheduleAtFixedRate(TimerTask task, Date firstTime, long period) { ... }
    public void scheduleAtFixedRate(TimerTask task, long delay, long period) { ... }

    public void cancel() { ... }
 
}

可以用 java.util.Timer 類的前兩個方法把任務(wù)安排在指定延遲之后或指定日期和時間發(fā)生。余下的 4 個調(diào)度方法還有額外的 period 參數(shù)。可以用它們對那些初次運行之后需要按照常規(guī)間隔發(fā)生的事件進行調(diào)度,使用周期參數(shù)指定時間間隔。schedulescheduleAtFixedRate 方法是不同的方法,因為這些操作的計時無法保證;像垃圾搜集這樣的操作可能會造成任務(wù)推遲執(zhí)行。如果任務(wù)被延遲,那么 schedule 方法在運行下一個任務(wù)之前仍然會等候一個完整的周期。但是,scheduleAtFixedRate 方法是在前一個任務(wù) 應(yīng)當 運行的固定周期之后運行下一個任務(wù)。所以,如果任務(wù)之間的時間對您非常很重要,那么請使用 schedule。如果絕對時間或累積時間很重要,那么請使用 scheduleAtFixedRate

每個 schedule 方法都采用一個擴展了 TimerTask 類的對象作為自己的第一個參數(shù)。同使用 Work 接口時一樣,這個類擴展了 Runnable ,而且 Timer 會在適當?shù)臅r間調(diào)用 run 方法。TimerTask 類有一個 cancel 方法,可以用它取消對任務(wù)的后續(xù)調(diào)用。或者,也可以調(diào)用 Timer 上的 cancel 方法來取消目前安排的所有任務(wù)。scheduledExecutionTime 方法允許 run 方法將當前實際調(diào)用時間和它應(yīng)當被調(diào)用的時間進行比較。

雖然 TimerTaskrun 方法是在新線程中調(diào)用 ,但是這個線程是 JVM 已經(jīng)分配的線程,處于應(yīng)用服務(wù)器的控制之外。如果您想讓資源適配器進行嚴肅的處理,那么在這個時候,應(yīng)當用 WorkManager 切換到應(yīng)用服務(wù)器的線程。清單 8 顯示的示例采用了這一良好實踐來調(diào)度工作,從當前時間開始每分鐘執(zhí)行一次:

清單 8. 組合使用 TimerWorkManager
final WorkManager workManager = context.getWorkManager();
final Timer timer = context.createTimer();

timer.scheduleAtFixedRate(new TimerTask() {
    public void run() {
        workManager.scheduleWork(new ExampleWork());
    }
}, 0, 60 * 1000);

JCA WorkManager 和 Timer 的替代方案
正如前一小節(jié)提到過的,在 JCA 中,使用 Timer 有一個問題:執(zhí)行 TimerTask 的線程不在應(yīng)用服務(wù)器的控制之下。因為 Timer 是類而不是接口,而且線程實際是在類的構(gòu)造函數(shù)中產(chǎn)生的, 所以應(yīng)用服務(wù)器不能修改這種行為。允許應(yīng)用服務(wù)器實現(xiàn)這一目的一個替代方案就是使用由 IBM 和 BEA 聯(lián)合開發(fā)的 Timer and Work Manager for Application Servers 規(guī)范的接口(請參閱 參考資料) 。

在使用這個規(guī)范的接口時,會從 Java 名稱與目錄服務(wù)接口(JNDI)得到一個 TimerManager 實現(xiàn)。可以通過給管理器安排了一個 TimerListener 實現(xiàn)來返回一個 Timer (是一個commonj.timers.Timer 而不是 java.util.Timer)。在安排好的時間上會調(diào)用 TimerListener,可以用 Timer 執(zhí)行諸如取消預(yù)定操作之類的操作。

顧名思義,這個規(guī)范還提供了 JCA 工作管理的一個替代。這里的接口與 JCA 的那些接口很相似,除了初始的 commmonj.work.WorkManager 是從 JNDI 得到的。這個合約提供的附加行為還包括:阻塞到一個或全部預(yù)定工作完成的能力、在遠程 JVM 上執(zhí)行可以序列化的工作的可能性。與 commonj Timer 一樣,commonj WorkManager 的應(yīng)用不限于資源適配器。任何服務(wù)器端 J2EE 組件都可以使用這項功能。

清單 9 顯示了為了使用 commonj 的類而被重寫的 清單 8 的示例:

清單 9. 使用 commonj 接口
final InitialContext context = new InitialContext();
final WorkManager workManager = (WorkManager) 
        context.lookup("java:comp/env/wm/MyWorkManager");
final TimerManager timerManager = (TimerManager)
	context.lookup("java:comp/env/timer/MyTimer");

timerManager.scheduleAtFixedRate(new TimerListener() {
    public void timerExpired(final Timer timer) {
        try {
            workManager.schedule(new ExampleCommonjWork());
        } catch (final WorkException exception) {
        }
    }
}, 0, 60 * 1000);

ExampleCommonjWork 類與 清單 4ExampleWork 相同,此外,它還實現(xiàn)了 commonj Work 接口要求的額外的 isDaemon 方法。如果工作長期存在,那么該方法應(yīng)當返回 true

導(dǎo)入事務(wù)并完成事務(wù)
在 JCA 1.5 之前,應(yīng)用服務(wù)器總是充當事務(wù)的協(xié)調(diào)者。應(yīng)用程序負責器負責啟動事務(wù),而且在每個資源管理器通過 JCA 連接管理器登記了自己的 XAResource 之后,還通過一起提交或回滾所有資源來協(xié)調(diào)事務(wù)的完成。JCA 1.5 的事務(wù)流入合約允許企業(yè)信息系統(tǒng)(EIS)啟動和完成事務(wù),充當事務(wù)的協(xié)調(diào)者。這樣 EIS 就能通過資源適配器把事務(wù)導(dǎo)入應(yīng)用服務(wù)器,并在事務(wù)范圍內(nèi)在服務(wù)器上執(zhí)行工作。例如,它可能使用 Supports 的容器管理器事務(wù)屬性來調(diào)用消息驅(qū)動 bean(MDB)。這樣,MDB 方法執(zhí)行的工作,包括對其它 EJB 的調(diào)用,就會成為事務(wù)的一部分。

資源適配器通過第 3 個 ExecutionContext 參數(shù)導(dǎo)入事務(wù),在 清單 2 中可以看到它被傳遞給 WorkManager。正如在清單 10 中可以看到的,這個類擁有設(shè)置事務(wù) ID (XID) 和事務(wù)超時的方法:

清單 10. 傳遞給 WorkManagerExecutionContext
public class ExecutionContext {

    public ExecutionContext() { ... }
    public void setXid(Xid xid) { ... }
    public Xid getXid() { ... }
    public void setTransactionTimeout(long timeout)
        throws NotSupportedException { ... }
    public long getTransactionTimeout() { ... }    
    
}

XID 惟一地標識事務(wù),它由三部分構(gòu)成:格式標識符、事務(wù)界定符和分支界定符。如果 EIS 還沒有事務(wù)的 XID,那么就必須根據(jù) XA 規(guī)范(請參閱 參考資料)構(gòu)建一個 XID。然后應(yīng)用服務(wù)器會在調(diào)用工作對象的 run 方法之前把這個事務(wù)與執(zhí)行線程關(guān)聯(lián)起來。

把事務(wù)導(dǎo)入應(yīng)用服務(wù)器之后,接下來就由資源適配器負責把屬于這個事務(wù)的事件通知給服務(wù)器。具體地說,它必須把事務(wù)完成通知給應(yīng)用服務(wù)器。它是通過清單 11 中的 XATerminator 接口做到這一點的,可以從 BootstrapContext 中得到它的實現(xiàn):

清單 11. 用于事務(wù)完成的 XATerminator
public interface XATerminator {

    void commit(Xid xid, boolean onePhase) throws XAException;
    void forget(Xid xid) throws XAException;
    int prepare(Xid xid) throws XAException;
    Xid[] recover(int flag) throws XAException;
    void rollback(Xid xid) throws XAException;
    
}

XATerminator 接口的方法與 XAResource 上的方法對應(yīng),只是在這個例子中,資源適配器需要調(diào)用應(yīng)用程序服務(wù)器。典型情況下,如果事務(wù)中包含不止一個資源,那么資源適配器會調(diào)用 XATerminatorprepare 方法,傳遞與 ExecutionContext 中傳遞的 Xid 相同的 Xid。如果所有的資源都返回 XA_OK (或者 XA_RDONLY),那么資源適配器會接著調(diào)用 commit;否則就會調(diào)用 rollback

結(jié)束語
本文介紹了如何用 WorkManager 接口對工作進行調(diào)度,以便在應(yīng)用程序的控制下處理這些工作。您已經(jīng)看到如何用 Timer 的實例在以后某個時間執(zhí)行工作或定期執(zhí)行工作,還了解了使用 JCA 提供的對象的 commonj 替代方案。您已經(jīng)看到資源適配器如何在自己導(dǎo)入到應(yīng)用服務(wù)器的事務(wù)中執(zhí)行工作,并用 XATerminator 接口控制事務(wù)的完成。在本系列的第 3 篇也是最后一篇文章中,我將介紹 JCA 1.5 消息流入合約,它比較出名的地方是對消息驅(qū)動 bean 的支持。