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

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

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

讓工作完成
在本系列的第 1 部分中,我們介紹了 ResourceAdapter 接口,它提供了一種機(jī)制,能夠在應(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 允許資源適配器對工作進(jìn)行調(diào)度,在應(yīng)用服務(wù)器線程上同步或異步執(zhí)行調(diào)度。這個工作可以在資源導(dǎo)入的事務(wù)中執(zhí)行,在這種情況下,XATerminator 有助于完成工作。Timer 負(fù)責(zé)延遲工作或定期工作的執(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 接口擴(kuò)展了 Runnable 接口,您應(yīng)當(dāng)像直接進(jìn)行 Java 線程編程時所做的那樣,實現(xiàn)run 方法中執(zhí)行的工作。您很快就會看到 release 方法發(fā)揮其作用的地方。

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

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

第三,正如下一節(jié)解釋的,有可能讓工作片斷在資源適配器導(dǎo)入的事務(wù)上下文中執(zhí)行,而不是在與當(dāng)前線程關(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)當(dā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 方法正進(jìn)行其處理的同時,會在一個獨立的線程上調(diào)用 release 方法,volatile 修飾符確保 run 方法能夠看到更新的字段。

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

為什么等待?
您已經(jīng)看到 doWork 方法允許您在調(diào)用線程阻塞的同時執(zhí)行工作。但是如果您不想等待 —— 也就是說,如果您不想同步執(zhí)行工作,該怎么辦呢?在 Java 2 平臺標(biāo)準(zhǔn)版本(J2SE)環(huán)境中,可以使用多線程實現(xiàn)這一目標(biāo)。但是,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)機(jī)很難實現(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)聽器,當(dāng)工作的項目通過接受、啟動或完成這幾個狀態(tài)(失敗的情況下則是拒絕)傳遞過來時,監(jiān)聽器會得到通知。在這個監(jiān)聽器已經(jīng)完成其工作項目,或者出現(xiàn)故障要重新安排工作項目時,可以用它將通知發(fā)送給工作的發(fā)起者。WorkAdapter 類也包含在內(nèi),它提供了所有方法的默認(rèn)實現(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)聽器負(fù)責(zé)多個工作提交。還有一些方法可以返回啟動工作所花費的時間,而且,在出現(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)成為標(biāo)準(zhǔn) 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ā)生的事件進(jìn)行調(diào)度,使用周期參數(shù)指定時間間隔。schedulescheduleAtFixedRate 方法是不同的方法,因為這些操作的計時無法保證;像垃圾搜集這樣的操作可能會造成任務(wù)推遲執(zhí)行。如果任務(wù)被延遲,那么 schedule 方法在運行下一個任務(wù)之前仍然會等候一個完整的周期。但是,scheduleAtFixedRate 方法是在前一個任務(wù) 應(yīng)當(dāng) 運行的固定周期之后運行下一個任務(wù)。所以,如果任務(wù)之間的時間對您非常很重要,那么請使用 schedule。如果絕對時間或累積時間很重要,那么請使用 scheduleAtFixedRate

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

雖然 TimerTaskrun 方法是在新線程中調(diào)用 ,但是這個線程是 JVM 已經(jīng)分配的線程,處于應(yīng)用服務(wù)器的控制之外。如果您想讓資源適配器進(jìn)行嚴(yán)肅的處理,那么在這個時候,應(yīng)當(dāng)用 WorkManager 切換到應(yīng)用服務(wù)器的線程。清單 8 顯示的示例采用了這一良好實踐來調(diào)度工作,從當(dāng)前時間開始每分鐘執(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ù)定工作完成的能力、在遠(yuǎn)程 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)當(dāng)返回 true

導(dǎo)入事務(wù)并完成事務(wù)
在 JCA 1.5 之前,應(yīng)用服務(wù)器總是充當(dāng)事務(wù)的協(xié)調(diào)者。應(yīng)用程序負(fù)責(zé)器負(fù)責(zé)啟動事務(wù),而且在每個資源管理器通過 JCA 連接管理器登記了自己的 XAResource 之后,還通過一起提交或回滾所有資源來協(xié)調(diào)事務(wù)的完成。JCA 1.5 的事務(wù)流入合約允許企業(yè)信息系統(tǒng)(EIS)啟動和完成事務(wù),充當(dāng)事務(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 惟一地標(biāo)識事務(wù),它由三部分構(gòu)成:格式標(biāo)識符、事務(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ù)器之后,接下來就由資源適配器負(fù)責(zé)把屬于這個事務(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 接口對工作進(jìn)行調(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 的支持。