我將把 java.util.Timer 和 java.util.TimerTask 統稱為 Java 計時器框架,它們使程序員可以很容易地計劃簡單的任務(注意這些類也可用于 J2ME 中)。在 Java 2 SDK, Standard Edition, Version 1.3 中引入這個框架之前,開發人員必須編寫自己的調度程序,這需要花費很大精力來處理線程和復雜的 Object.wait() 方法。不過,Java 計時器框架沒有足夠的能力來滿足許多應用程序的計劃要求。甚至一項需要在每天同一時間重復執行的任務,也不能直接使用 Timer 來計劃,因為在夏令時開始和結束時會出現時間跳躍。
本文展示了一個通用的 Timer 和 TimerTask 計劃框架,從而允許更靈活的計劃任務。這個框架非常簡單 ?? 它包括兩個類和一個接口 ?? 并且容易掌握。如果您習慣于使用 Java 定時器框架,那么您應該可以很快地掌握這個計劃框架(有關 Java 定時器框架的更多信息,請參閱 參考資料)。
計劃單次任務
計劃框架建立在 Java 定時器框架類的基礎之上。因此,在解釋如何使用計劃框架以及如何實現它之前,我們將首先看看如何用這些類進行計劃。
想像一個煮蛋計時器,在數分鐘之后(這時蛋煮好了)它會發出聲音提醒您。清單 1 中的代碼構成了一個簡單的煮蛋計時器的基本結構,它用 Java 語言編寫:
清單 1. EggTimer 類
package org.tiling.scheduling.examples; import java.util.Timer; public class EggTimer { public EggTimer(int minutes) { public void start() { public static void main(String[] args) { } |
EggTimer 實例擁有一個 Timer 實例,用于提供必要的計劃。用 start() 方法啟動煮蛋計時器后,它就計劃了一個 TimerTask,在指定的分鐘數之后執行。時間到了,Timer 就在后臺調用 TimerTask 的 start() 方法,這會使它發出聲音。在取消計時器后這個應用程序就會中止。
計劃重復執行的任務
通過指定一個固定的執行頻率或者固定的執行時間間隔,Timer 可以對重復執行的任務進行計劃。不過,有許多應用程序要求更復雜的計劃。例如,每天清晨在同一時間發出叫醒鈴聲的鬧鐘不能簡單地使用固定的計劃頻率 86400000 毫秒(24 小時),因為在鐘撥快或者撥慢(如果您的時區使用夏令時)的那些天里,叫醒可能過晚或者過早。解決方案是使用日歷算法計算每日事件下一次計劃發生的時間。而這正是計劃框架所支持的?紤]清單 2 中的 AlarmClock 實現(有關計劃框架的源代碼以及包含這個框架和例子的 JAR 文件,請參閱 參考資料):
清單 2. AlarmClock 類
package org.tiling.scheduling.examples; import java.text.SimpleDateFormat; import java.util.Date; import org.tiling.scheduling.Scheduler; public class AlarmClock { private final Scheduler scheduler = new Scheduler(); public AlarmClock(int hourOfDay, int minute, int second) { public void start() { public static void main(String[] args) { |
注意這段代碼與煮蛋計時器應用程序非常相似。AlarmClock 實例擁有一個 Scheduler (而不是 Timer)實例,用于提供必要的計劃。啟動后,這個鬧鐘對 SchedulerTask (而不是 TimerTask)進行調度用以發出報警聲。這個鬧鐘不是計劃一個任務在固定的延遲時間后執行,而是用 DailyIterator 類描述其計劃。在這里,它只是計劃任務在每天上午 7:00 執行。下面是一個正常運行情況下的輸出:
Wake up! It′s 24 Aug 2003 07:00:00.023 Wake up! It′s 25 Aug 2003 07:00:00.001 Wake up! It′s 26 Aug 2003 07:00:00.058 Wake up! It′s 27 Aug 2003 07:00:00.015 Wake up! It′s 28 Aug 2003 07:00:00.002 |
...
DailyIterator 實現了 ScheduleIterator,這是一個將 SchedulerTask 的計劃執行時間指定為一系列 java.util.Date 對象的接口。然后 next() 方法按時間先后順序迭代 Date 對象。返回值 null 會使任務取消(即它再也不會運行)?? 這樣的話,試圖再次計劃將會拋出一個異常。清單 3 包含 ScheduleIterator 接口:
清單 3. ScheduleIterator 接口
package org.tiling.scheduling; import java.util.Date; public interface ScheduleIterator { |
DailyIterator 的 next() 方法返回表示每天同一時間(上午 7:00)的 Date 對象,如清單 4 所示。所以,如果對新構建的 next() 類調用 next(),那么將會得到傳遞給構造函數的那個日期當天或者后面一天的 7:00 AM。再次調用 next() 會返回后一天的 7:00 AM,如此重復。為了實現這種行為,DailyIterator 使用了 java.util.Calendar 實例。構造函數會在日歷中加上一天,對日歷的這種設置使得第一次調用 next() 會返回正確的 Date。注意代碼沒有明確地提到夏令時修正,因為 Calendar 實現(在本例中是 GregorianCalendar)負責對此進行處理,所以不需要這樣做。
清單 4. DailyIterator 類
package org.tiling.scheduling.examples.iterators; import org.tiling.scheduling.ScheduleIterator; import java.util.Calendar; /** public DailyIterator(int hourOfDay, int minute, int second) { public DailyIterator(int hourOfDay, int minute, int second, Date date) { public Date next() { } |
實現計劃框架
在上一節,我們學習了如何使用計劃框架,并將它與 Java 定時器框架進行了比較。下面,我將向您展示如何實現這個框架。除了 清單 3 中展示的 ScheduleIterator 接口,構成這個框架的還有另外兩個類 ?? Scheduler 和 SchedulerTask 。這些類實際上在內部使用 Timer 和 SchedulerTask,因為計劃其實就是一系列的單次定時器。清單 5 和 6 顯示了這兩個類的源代碼:
清單 5. Scheduler
package org.tiling.scheduling; import java.util.Date; public class Scheduler { class SchedulerTimerTask extends TimerTask { private final Timer timer = new Timer(); public Scheduler() { public void cancel() { public void schedule(SchedulerTask schedulerTask, Date time = iterator.next(); private void reschedule(SchedulerTask schedulerTask, Date time = iterator.next(); } |
清單 6 顯示了 SchedulerTask 類的源代碼:
package org.tiling.scheduling; import java.util.TimerTask; public abstract class SchedulerTask implements Runnable { final Object lock = new Object(); int state = VIRGIN; TimerTask timerTask; protected SchedulerTask() { public abstract void run(); public boolean cancel() { public long scheduledExecutionTime() { } |
就像煮蛋計時器,Scheduler 的每一個實例都擁有 Timer 的一個實例,用于提供底層計劃。Scheduler 并沒有像實現煮蛋計時器時那樣使用一個單次定時器,它將一組單次定時器串接在一起,以便在由 ScheduleIterator 指定的各個時間執行 SchedulerTask 類。
考慮 Scheduler 上的 public schedule() 方法 ?? 這是計劃的入口點,因為它是客戶調用的方法(在 取消任務 一節中將描述僅有的另一個 public 方法 cancel())。通過調用 ScheduleIterator 接口的 next(),發現第一次執行 SchedulerTask 的時間。然后通過調用底層 Timer 類的單次 schedule() 方法,啟動計劃在這一時刻執行。為單次執行提供的 TimerTask 對象是嵌入的 SchedulerTimerTask 類的一個實例,它包裝了任務和迭代器(iterator)。在指定的時間,調用嵌入類的 run() 方法,它使用包裝的任務和迭代器引用以便重新計劃任務的下一次執行。reschedule() 方法與 schedule() 方法非常相似,只不過它是 private 的,并且執行一組稍有不同的 SchedulerTask 狀態檢查。重新計劃過程反復重復,為每次計劃執行構造一個新的嵌入類實例,直到任務或者調度程序被取消(或者 JVM 關閉)。
類似于 TimerTask,SchedulerTask 在其生命周期中要經歷一系列的狀態。創建后,它處于 VIRGIN 狀態,這表明它從沒有計劃過。計劃以后,它就變為 SCHEDULED 狀態,再用下面描述的方法之一取消任務后,它就變為 CANCELLED 狀態。管理正確的狀態轉變 ?? 如保證不對一個非 VIRGIN 狀態的任務進行兩次計劃 ?? 增加了 Scheduler 和 SchedulerTask 類的復雜性。在進行可能改變任務狀態的操作時,代碼必須同步任務的鎖對象。
取消任務
取消計劃任務有三種方式。第一種是調用 SchedulerTask 的 cancel() 方法。這很像調用 TimerTask 的 cancel()方法:任務再也不會運行了,不過已經運行的任務仍會運行完成。 cancel() 方法的返回值是一個布爾值,表示如果沒有調用 cancel() 的話,計劃的任務是否還會運行。更準確地說,如果任務在調用 cancel() 之前是 SCHEDULED 狀態,那么它就返回 true。如果試圖再次計劃一個取消的(甚至是已計劃的)任務,那么 Scheduler 就會拋出一個 IllegalStateException。
取消計劃任務的第二種方式是讓 ScheduleIterator 返回 null。這只是第一種方式的簡化操作,因為 Scheduler 類調用 SchedulerTask 類的 cancel()方法。如果您想用迭代器而不是任務來控制計劃停止時間時,就用得上這種取消任務的方式了。
第三種方式是通過調用其 cancel() 方法取消整個 Scheduler。這會取消調試程序的所有任務,并使它不能再計劃任何任務。
擴展 cron 實用程序
可以將計劃框架比作 UNIX 的 cron 實用程序,只不過計劃次數的規定是強制性而不是聲明性的。例如,在 AlarmClock 實現中使用的 DailyIterator 類,它的計劃與 cron 作業的計劃相同,都是由以 0 7 * * * 開始的 crontab 項指定的(這些字段分別指定分鐘、小時、日、月和星期)。
不過,計劃框架比 cron 更靈活。想像一個在早晨打開熱水的 HeatingController 應用程序。我想指示它“在每個工作日上午 8:00 打開熱水,在周未上午 9:00 打開熱水”。使用 cron,我需要兩個 crontab 項(0 8 * * 1,2,3,4,5 和 0 9 * * 6,7)。而使用 ScheduleIterator 的解決方案更簡潔一些,因為我可以使用復合(composition)來定義單一迭代器。清單 7 顯示了其中的一種方法:
清單 7. 用復合定義單一迭代器
int[] weekdays = new int[] { Calendar.MONDAY, Calendar.TUESDAY, Calendar.WEDNESDAY, Calendar.THURSDAY, Calendar.FRIDAY }; int[] weekend = new int[] { Calendar.SATURDAY, Calendar.SUNDAY }; ScheduleIterator i = new CompositeIterator( new ScheduleIterator[] { new RestrictedDailyIterator(8, 0, 0, weekdays), new RestrictedDailyIterator(9, 0, 0, weekend) } ); |
RestrictedDailyIterator 類很像 DailyIterator,只不過它限制為只在一周的特定日子里運行,而一個 CompositeIterator 類取得一組 ScheduleIterators,并將日期正確排列到單個計劃中。這些類的源代碼請參閱 參考資料。
有許多計劃是 cron 無法生成的,但是 ScheduleIterator 實現卻可以。例如,“每個月的最后一天”描述的計劃可以用標準 Java 日歷算法來實現(用 Calendar 類),而用 cron 則無法表達它。應用程序甚至無需使用 Calendar 類。在本文的源代碼(請參閱 參考資料)中,我加入了一個安全燈控制器的例子,它按“在日落之前 15 分鐘開燈”這一計劃運行。這個實現使用了 Calendrical Calculations Software Package (請參閱 參考資料),用于計算當地(給定經度和緯度)的日落時間。
文章來源于領測軟件測試網 http://www.kjueaiud.com/