所有類型的 Java 應用程序一般都需要計劃重復執行的任務。企業應用程序需要計劃每日的日志或者晚間批處理過程。一個 J2SE 或者 J2ME 日歷應用程序需要根據用戶的約定計劃鬧鈴時間。不過,標準的調度類 Timer 和 TimerTask 沒有足夠的靈活性,無法支持通常需要的計劃任務類型。在本文中,Java 開發人員 Tom White 向您展示了如何構建一個簡單通用的計劃框架,以用于執行任意復雜的計劃任務。
我將把 java.util.Timer 和 java.util.TimerTask 統稱為 Java 計時器框架,它們使程序員可以很容易地計劃簡單的任務(注意這些類也可用于 J2ME 中)。在 Java 2 SDK, Standard Edition, Version 1.3 中引入這個框架之前,開發人員必須編寫自己的調度程序,這需要花費很大精力來處理線程和復雜的 Object.wait() 方法。不過,Java 計時器框架沒有足夠的能力來滿足許多應用程序的計劃要求。甚至一項需要在每天同一時間重復執行的任務,也不能直接使用 Timer 來計劃,因為在夏令時開始和結束時會出現時間跳躍。
本文展示了一個通用的 Timer 和 TimerTask 計劃框架,從而允許更靈活的計劃任務。這個框架非常簡單 —— 它包括兩個類和一個接口 —— 并且容易掌握。如果您習慣于使用 Java 定時器框架,那么您應該可以很快地掌握這個計劃框架。
計劃單次任務 計劃框架建立在 Java 定時器框架類的基礎之上。因此,在解釋如何使用計劃框架以及如何實現它之前,我們將首先看看如何用這些類進行計劃。
想像一個煮蛋計時器,在數分鐘之后(這時蛋煮好了)它會發出聲音提醒您。清單 1 中的代碼構成了一個簡單的煮蛋計時器的基本結構,它用 Java 語言編寫:
清單 1. EggTimer 類
package org.tiling.scheduling.examples;
import java.util.Timer; import java.util.TimerTask;
public class EggTimer { private final Timer timer = new Timer(); private final int minutes;
public EggTimer(int minutes) { this.minutes = minutes; }
public void start() { timer.schedule(new TimerTask() { public void run() { playSound(); timer.cancel(); } private void playSound() { System.out.println("Your egg is ready!"); // Start a new thread to play a sound... } }, minutes * 60 * 1000); }
public static void main(String[] args) { EggTimer eggTimer = new EggTimer(2); eggTimer.start(); }
} |
EggTimer 實例擁有一個 Timer 實例,用于提供必要的計劃。用 start() 方法啟動煮蛋計時器后,它就計劃了一個 TimerTask,在指定的分鐘數之后執行。時間到了,Timer 就在后臺調用 TimerTask 的 start() 方法,這會使它發出聲音。在取消計時器后這個應用程序就會中止。
計劃重復執行的任務 通過指定一個固定的執行頻率或者固定的執行時間間隔,Timer 可以對重復執行的任務進行計劃。不過,有許多應用程序要求更復雜的計劃。例如,每天清晨在同一時間發出叫醒鈴聲的鬧鐘不能簡單地使用固定的計劃頻率 86400000 毫秒(24 小時),因為在鐘撥快或者撥慢(如果您的時區使用夏令時)的那些天里,叫醒可能過晚或者過早。解決方案是使用日歷算法計算每日事件下一次計劃發生的時間。 而這正是計劃框架所支持的。考慮清單 2 中的 AlarmClock 實現:
清單 2. AlarmClock 類
package org.tiling.scheduling.examples;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.tiling.scheduling.Scheduler; import org.tiling.scheduling.SchedulerTask; import org.tiling.scheduling.examples.iterators.DailyIterator;
public class AlarmClock {
private final Scheduler scheduler = new Scheduler(); private final SimpleDateFormat dateFormat = new SimpleDateFormat("dd MMM yyyy HH:mm:ss.SSS"); private final int hourOfDay, minute, second;
public AlarmClock(int hourOfDay, int minute, int second) { this.hourOfDay = hourOfDay; this.minute = minute; this.second = second; }
public void start() { scheduler.schedule(new SchedulerTask() { public void run() { soundAlarm(); } private void soundAlarm() { System.out.println("Wake up! " + "It"s " + dateFormat.format(new Date())); // Start a new thread to sound an alarm... } }, new DailyIterator(hourOfDay, minute, second)); }
public static void main(String[] args) { AlarmClock alarmClock = new AlarmClock(7, 0, 0); alarmClock.start(); } } |
注意這段代碼與煮蛋計時器應用程序非常相似。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 { public Date next(); } |
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; import java.util.Date;
/** * A DailyIterator class returns a sequence of dates on subsequent days * representing the same time each day. */ public class DailyIterator implements ScheduleIterator { private final int hourOfDay, minute, second; private final Calendar calendar = Calendar.getInstance();
public DailyIterator(int hourOfDay, int minute, int second) { this(hourOfDay, minute, second, new Date()); }
public DailyIterator(int hourOfDay, int minute, int second, Date date) { this.hourOfDay = hourOfDay; this.minute = minute; this.second = second; calendar.setTime(date); calendar.set(Calendar.HOUR_OF_DAY, hourOfDay); calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.SECOND, second); calendar.set(Calendar.MILLISECOND, 0); if (!calendar.getTime().before(date)) { calendar.add(Calendar.DATE, -1); } }
public Date next() { calendar.add(Calendar.DATE, 1); return calendar.getTime(); }
} |