綜觀目前的 Web 應(yīng)用,多數(shù)應(yīng)用都具備任務(wù)調(diào)度的功能。本文由淺入深介紹了幾種任務(wù)調(diào)度的 Java 實(shí)現(xiàn)方法,包括 Timer,Scheduler, Quartz 以及 JCron Tab,并對其優(yōu)缺點(diǎn)進(jìn)行比較,目的在于給需要開發(fā)任務(wù)調(diào)度的程序員提供有價(jià)值的參考。

 

前言

任務(wù)調(diào)度是指基于給定時(shí)間點(diǎn),給定時(shí)間間隔或者給定執(zhí)行次數(shù)自動(dòng)執(zhí)行任務(wù)。本文由淺入深介紹四種任務(wù)調(diào)度的 Java 實(shí)現(xiàn):

  • Timer
  • ScheduledExecutor
  • 開源工具包 Quartz
  • 開源工具包 JCronTab

此外,為結(jié)合實(shí)現(xiàn)復(fù)雜的任務(wù)調(diào)度,本文還將介紹 Calendar 的一些使用方法。

Timer

相信大家都已經(jīng)非常熟悉 java.util.Timer 了,它是最簡單的一種實(shí)現(xiàn)任務(wù)調(diào)度的方法,下面給出一個(gè)具體的例子:

01 package com.ibm.scheduler; 
02  import java.util.Timer; 
03  import java.util.TimerTask; 
04   
05  public class TimerTest extends TimerTask { 
06   
07  private String jobName = ""
08   
09  public TimerTest(String jobName) { 
10  super(); 
11  this.jobName = jobName; 
12  
13   
14  @Override 
15  public void run() { 
16  System.out.println("execute " + jobName); 
17  
18   
19  public static void main(String[] args) { 
20  Timer timer = new Timer(); 
21  long delay1 = 1 * 1000
22  long period1 = 1000
23  // 從現(xiàn)在開始 1 秒鐘之后,每隔 1 秒鐘執(zhí)行一次 job1 
24  timer.schedule(new TimerTest("job1"), delay1, period1); 
25  long delay2 = 2 * 1000
26  long period2 = 2000
27  // 從現(xiàn)在開始 2 秒鐘之后,每隔 2 秒鐘執(zhí)行一次 job2 
28  timer.schedule(new TimerTest("job2"), delay2, period2); 
29  
30  
31 /**
32  輸出結(jié)果: 
33  execute job1 
34  execute job1 
35  execute job2 
36  execute job1 
37  execute job1 
38  execute job2 
39 */

 

使用 Timer 實(shí)現(xiàn)任務(wù)調(diào)度的核心類是 Timer 和 TimerTask。其中 Timer 負(fù)責(zé)設(shè)定 TimerTask 的起始與間隔執(zhí)行時(shí)間。使用者只需要?jiǎng)?chuàng)建一個(gè) TimerTask 的繼承類,實(shí)現(xiàn)自己的 run 方法,然后將其丟給 Timer 去執(zhí)行即可。

Timer 的設(shè)計(jì)核心是一個(gè) TaskList 和一個(gè) TaskThread。Timer 將接收到的任務(wù)丟到自己的 TaskList 中,TaskList 按照 Task 的最初執(zhí)行時(shí)間進(jìn)行排序。TimerThread 在創(chuàng)建 Timer 時(shí)會啟動(dòng)成為一個(gè)守護(hù)線程。這個(gè)線程會輪詢所有任務(wù),找到一個(gè)最近要執(zhí)行的任務(wù),然后休眠,當(dāng)?shù)竭_(dá)最近要執(zhí)行任務(wù)的開始時(shí)間點(diǎn),TimerThread 被喚醒并執(zhí)行該任務(wù)。之后 TimerThread 更新最近一個(gè)要執(zhí)行的任務(wù),繼續(xù)休眠。

Timer 的優(yōu)點(diǎn)在于簡單易用,但由于所有任務(wù)都是由同一個(gè)線程來調(diào)度,因此所有任務(wù)都是串行執(zhí)行的,同一時(shí)間只能有一個(gè)任務(wù)在執(zhí)行,前一個(gè)任務(wù)的延遲或異常都將會影響到之后的任務(wù)。

 

 

ScheduledExecutor

鑒于 Timer 的上述缺陷,Java 5 推出了基于線程池設(shè)計(jì)的 ScheduledExecutor。其設(shè)計(jì)思想是,每一個(gè)被調(diào)度的任務(wù)都會由線程池中一個(gè)線程去執(zhí)行,因此任務(wù)是并發(fā)執(zhí)行的,相互之間不會受到干擾。需 要注意的是,只有當(dāng)任務(wù)的執(zhí)行時(shí)間到來時(shí),ScheduedExecutor 才會真正啟動(dòng)一個(gè)線程,其余時(shí)間 ScheduledExecutor 都是在輪詢?nèi)蝿?wù)的狀態(tài)。

01 package com.ibm.scheduler;
02 import java.util.concurrent.Executors;
03 import java.util.concurrent.ScheduledExecutorService;
04 import java.util.concurrent.TimeUnit;
05   
06 public class ScheduledExecutorTest implements Runnable {
07     private String jobName = "";
08   
09     public ScheduledExecutorTest(String jobName) {
10         super();
11         this.jobName = jobName;
12     }
13   
14     @Override
15     public void run() {
16         System.out.println("execute " + jobName);
17     }
18   
19     public static void main(String[] args) {
20         ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
21   
22         long initialDelay1 = 1;
23         long period1 = 1;
24         // 從現(xiàn)在開始1秒鐘之后,每隔1秒鐘執(zhí)行一次job1
25         service.scheduleAtFixedRate(
26                 new ScheduledExecutorTest("job1"), initialDelay1,
27                 period1, TimeUnit.SECONDS);
28   
29         long initialDelay2 = 1;
30         long delay2 = 1;
31         // 從現(xiàn)在開始2秒鐘之后,每隔2秒鐘執(zhí)行一次job2
32         service.scheduleWithFixedDelay(
33                 new ScheduledExecutorTest("job2"), initialDelay2,
34                 delay2, TimeUnit.SECONDS);
35     }
36 }
37 /**
38 輸出結(jié)果:
39 execute job1
40 execute job1
41 execute job2
42 execute job1
43 execute job1
44 execute job2
45 */

 

上面代碼展示了 ScheduledExecutorService 中兩種最常用的調(diào)度方法 ScheduleAtFixedRate 和 ScheduleWithFixedDelay。ScheduleAtFixedRate 每次執(zhí)行時(shí)間為上一次任務(wù)開始起向后推一個(gè)時(shí)間間隔,即每次執(zhí)行時(shí)間為 :initialDelay, initialDelay+period, initialDelay+2*period, …;ScheduleWithFixedDelay 每次執(zhí)行時(shí)間為上一次任務(wù)結(jié)束起向后推一個(gè)時(shí)間間隔,即每次執(zhí)行時(shí)間為:initialDelay, initialDelay+executeTime+delay, initialDelay+2*executeTime+2*delay。由此可見,ScheduleAtFixedRate 是基于固定時(shí)間間隔進(jìn)行任務(wù)調(diào)度,ScheduleWithFixedDelay 取決于每次任務(wù)執(zhí)行的時(shí)間長短,是基于不固定時(shí)間間隔進(jìn)行任務(wù)調(diào)度。  

 

用 ScheduledExecutor 和 Calendar 實(shí)現(xiàn)復(fù)雜任務(wù)調(diào)度

Timer 和 ScheduledExecutor 都僅能提供基于開始時(shí)間與重復(fù)間隔的任務(wù)調(diào)度,不能勝任更加復(fù)雜的調(diào)度需求。比如,設(shè)置每星期二的 16:38:10 執(zhí)行任務(wù)。該功能使用 Timer 和 ScheduledExecutor 都不能直接實(shí)現(xiàn),但我們可以借助 Calendar 間接實(shí)現(xiàn)該功能。

001 package com.ibm.scheduler;
002   
003 import java.util.Calendar;
004 import java.util.Date;
005 import java.util.TimerTask;
006 import java.util.concurrent.Executors;
007 import java.util.concurrent.ScheduledExecutorService;
008 import java.util.concurrent.TimeUnit;
009   
010 public class ScheduledExceutorTest2 extends TimerTask {
011   
012     private String jobName = "";
013   
014     public ScheduledExceutorTest2(String jobName) {
015         super();
016         this.jobName = jobName;
017     }
018   
019     @Override
020     public void run() {
021         System.out.println("Date = "+new Date()+", execute " + jobName);
022     }
023   
024     /**
025      * 計(jì)算從當(dāng)前時(shí)間currentDate開始,滿足條件dayOfWeek, hourOfDay, 
026      * minuteOfHour, secondOfMinite的最近時(shí)間
027      * @return
028      */
029     public Calendar getEarliestDate(Calendar currentDate, int dayOfWeek,
030             int hourOfDay, int minuteOfHour, int secondOfMinite) {
031         //計(jì)算當(dāng)前時(shí)間的WEEK_OF_YEAR,DAY_OF_WEEK, HOUR_OF_DAY, MINUTE,SECOND等各個(gè)字段值
032         int currentWeekOfYear = currentDate.get(Calendar.WEEK_OF_YEAR);
033         int currentDayOfWeek = currentDate.get(Calendar.DAY_OF_WEEK);
034         int currentHour = currentDate.get(Calendar.HOUR_OF_DAY);
035         int currentMinute = currentDate.get(Calendar.MINUTE);
036         int currentSecond = currentDate.get(Calendar.SECOND);
037   
038         //如果輸入條件中的dayOfWeek小于當(dāng)前日期的dayOfWeek,則WEEK_OF_YEAR需要推遲一周
039         boolean weekLater = false;
040         if (dayOfWeek < currentDayOfWeek) {
041             weekLater = true;
042         } else if (dayOfWeek == currentDayOfWeek) {
043             //當(dāng)輸入條件與當(dāng)前日期的dayOfWeek相等時(shí),如果輸入條件中的
044             //hourOfDay小于當(dāng)前日期的
045             //currentHour,則WEEK_OF_YEAR需要推遲一周   
046             if (hourOfDay < currentHour) {
047                 weekLater = true;
048             } else if (hourOfDay == currentHour) {
049                  //當(dāng)輸入條件與當(dāng)前日期的dayOfWeek, hourOfDay相等時(shí),
050                  //如果輸入條件中的minuteOfHour小于當(dāng)前日期的
051                 //currentMinute,則WEEK_OF_YEAR需要推遲一周
052                 if (minuteOfHour < currentMinute) {
053                     weekLater = true;
054                 } else if (minuteOfHour == currentSecond) {
055                      //當(dāng)輸入條件與當(dāng)前日期的dayOfWeek, hourOfDay, 
056                      //minuteOfHour相等時(shí),如果輸入條件中的
057                     //secondOfMinite小于當(dāng)前日期的currentSecond,
058                     //則WEEK_OF_YEAR需要推遲一周
059                     if (secondOfMinite < currentSecond) {
060                         weekLater = true;
061                     }
062                 }
063             }
064         }
065         if (weekLater) {
066             //設(shè)置當(dāng)前日期中的WEEK_OF_YEAR為當(dāng)前周推遲一周
067             currentDate.set(Calendar.WEEK_OF_YEAR, currentWeekOfYear + 1);
068         }
069         // 設(shè)置當(dāng)前日期中的DAY_OF_WEEK,HOUR_OF_DAY,MINUTE,SECOND為輸入條件中的值。
070         currentDate.set(Calendar.DAY_OF_WEEK, dayOfWeek);
071         currentDate.set(Calendar.HOUR_OF_DAY, hourOfDay);
072         currentDate.set(Calendar.MINUTE, minuteOfHour);
073         currentDate.set(Calendar.SECOND, secondOfMinite);
074         return currentDate;
075   
076     }
077   
078     public static void main(String[] args) throws Exception {
079   
080         ScheduledExceutorTest2 test = new ScheduledExceutorTest2("job1");
081         //獲取當(dāng)前時(shí)間
082         Calendar currentDate = Calendar.getInstance();
083         long currentDateLong = currentDate.getTime().getTime();
084         System.out.println("Current Date = " + currentDate.getTime().toString());
085         //計(jì)算滿足條件的最近一次執(zhí)行時(shí)間
086         Calendar earliestDate = test
087                 .getEarliestDate(currentDate, 3, 16, 38, 10);
088         long earliestDateLong = earliestDate.getTime().getTime();
089         System.out.println("Earliest Date = "
090                 + earliestDate.getTime().toString());
091         //計(jì)算從當(dāng)前時(shí)間到最近一次執(zhí)行時(shí)間的時(shí)間間隔
092         long delay = earliestDateLong - currentDateLong;
093         //計(jì)算執(zhí)行周期為一星期
094         long period = 7 * 24 * 60 * 60 * 1000;
095         ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
096         //從現(xiàn)在開始delay毫秒之后,每隔一星期執(zhí)行一次job1
097         service.scheduleAtFixedRate(test, delay, period,
098                 TimeUnit.MILLISECONDS);
099   
100     }
101
102 /**
103 輸出結(jié)果:
104 Current Date = Wed Feb 02 17:32:01 CST 2011
105 Earliest Date = Tue Feb 8 16:38:10 CST 2011
106 Date = Tue Feb 8 16:38:10 CST 2011, execute job1
107 Date = Tue Feb 15 16:38:10 CST 2011, execute job1
108 */

 

清單 3 實(shí)現(xiàn)了每星期二 16:38:10 調(diào)度任務(wù)的功能。其核心在于根據(jù)當(dāng)前時(shí)間推算出最近一個(gè)星期二 16:38:10 的絕對時(shí)間,然后計(jì)算與當(dāng)前時(shí)間的時(shí)間差,作為調(diào)用 ScheduledExceutor 函數(shù)的參數(shù)。計(jì)算最近時(shí)間要用到 java.util.calendar 的功能。首先需要解釋 calendar 的一些設(shè)計(jì)思想。Calendar 有以下幾種唯一標(biāo)識一個(gè)日期的組合方式:

 YEAR + MONTH + DAY_OF_MONTH 
 YEAR + MONTH + WEEK_OF_MONTH + DAY_OF_WEEK 
 YEAR + MONTH + DAY_OF_WEEK_IN_MONTH + DAY_OF_WEEK 
 YEAR + DAY_OF_YEAR 
 YEAR + DAY_OF_WEEK + WEEK_OF_YEAR 

上述組合分別加上 HOUR_OF_DAY + MINUTE + SECOND 即為一個(gè)完整的時(shí)間標(biāo)識。本例采用了最后一種組合方式。輸入為 DAY_OF_WEEK, HOUR_OF_DAY, MINUTE, SECOND 以及當(dāng)前日期 , 輸出為一個(gè)滿足 DAY_OF_WEEK, HOUR_OF_DAY, MINUTE, SECOND 并且距離當(dāng)前日期最近的未來日期。計(jì)算的原則是從輸入的 DAY_OF_WEEK 開始比較,如果小于當(dāng)前日期的 DAY_OF_WEEK,則需要向 WEEK_OF_YEAR 進(jìn)一, 即將當(dāng)前日期中的 WEEK_OF_YEAR 加一并覆蓋舊值;如果等于當(dāng)前的 DAY_OF_WEEK, 則繼續(xù)比較 HOUR_OF_DAY;如果大于當(dāng)前的 DAY_OF_WEEK,則直接調(diào)用 java.util.calenda 的 calendar.set(field, value) 函數(shù)將當(dāng)前日期的 DAY_OF_WEEK, HOUR_OF_DAY, MINUTE, SECOND 賦值為輸入值,依次類推,直到比較至 SECOND。讀者可以根據(jù)輸入需求選擇不同的組合方式來計(jì)算最近執(zhí)行時(shí)間。

可以看出,用上述方法實(shí)現(xiàn)該任務(wù)調(diào)度比較麻煩,這就需要一個(gè)更加完善的任務(wù)調(diào)度框架來解決這些復(fù)雜的調(diào)度問題。幸運(yùn)的是,開源工具包 Quartz 與 JCronTab 提供了這方面強(qiáng)大的支持。

 

 

Quartz

Quartz 可以滿足更多更復(fù)雜的調(diào)度需求,首先讓我們看看如何用 Quartz 實(shí)現(xiàn)每星期二 16:38 的調(diào)度安排:

01 package com.ibm.scheduler;
02 import java.util.Date;
03   
04 import org.quartz.Job;
05 import org.quartz.JobDetail;
06 import org.quartz.JobExecutionContext;
07 import org.quartz.JobExecutionException;
08 import org.quartz.Scheduler;
09 import org.quartz.SchedulerFactory;
10 import org.quartz.Trigger;
11 import org.quartz.helpers.TriggerUtils;
12   
13 public class QuartzTest implements Job {
14   
15     @Override
16     //該方法實(shí)現(xiàn)需要執(zhí)行的任務(wù)
17     public void execute(JobExecutionContext arg0) throws JobExecutionException {
18         System.out.println("Generating report - "
19                 + arg0.getJobDetail().getFullName() + ", type ="
20                 + arg0.getJobDetail().getJobDataMap().get("type"));
21         System.out.println(new Date().toString());
22     }
23     public static void main(String[] args) {
24         try {
25             // 創(chuàng)建一個(gè)Scheduler
26             SchedulerFactory schedFact = 
27             new org.quartz.impl.StdSchedulerFactory();
28             Scheduler sched = schedFact.getScheduler();
29             sched.start();
30             // 創(chuàng)建一個(gè)JobDetail,指明name,groupname,以及具體的Job類名,
31             //該Job負(fù)責(zé)定義需要執(zhí)行任務(wù)
32             JobDetail jobDetail = new JobDetail("myJob", "myJobGroup",
33                     QuartzTest.class);
34             jobDetail.getJobDataMap().put("type", "FULL");
35             // 創(chuàng)建一個(gè)每周觸發(fā)的Trigger,指明星期幾幾點(diǎn)幾分執(zhí)行
36             Trigger trigger = TriggerUtils.makeWeeklyTrigger(3, 16, 38);
37             trigger.setGroup("myTriggerGroup");
38             // 從當(dāng)前時(shí)間的下一秒開始執(zhí)行
39             trigger.setStartTime(TriggerUtils.getEvenSecondDate(new Date()));
40             // 指明trigger的name
41             trigger.setName("myTrigger");
42             // 用scheduler將JobDetail與Trigger關(guān)聯(lián)在一起,開始調(diào)度任務(wù)
43             sched.scheduleJob(jobDetail, trigger);
44   
45         } catch (Exception e) {
46             e.printStackTrace();
47         }
48     }
49
50 /**
51 輸出結(jié)果:
52 Generating report - myJobGroup.myJob, type =FULL
53 Tue Feb 8 16:38:00 CST 2011
54 Generating report - myJobGroup.myJob, type =FUL
55 Tue Feb 15 16:38:00 CST 2011
56 */

 

清單 4 非常簡潔地實(shí)現(xiàn)了一個(gè)上述復(fù)雜的任務(wù)調(diào)度。Quartz 設(shè)計(jì)的核心類包括 Scheduler, Job 以及 Trigger。其中,Job 負(fù)責(zé)定義需要執(zhí)行的任務(wù),Trigger 負(fù)責(zé)設(shè)置調(diào)度策略,Scheduler 將二者組裝在一起,并觸發(fā)任務(wù)開始執(zhí)行。

Job

使用者只需要?jiǎng)?chuàng)建一個(gè) Job 的繼承類,實(shí)現(xiàn) execute 方法。JobDetail 負(fù)責(zé)封裝 Job 以及 Job 的屬性,并將其提供給 Scheduler 作為參數(shù)。每次 Scheduler 執(zhí)行任務(wù)時(shí),首先會創(chuàng)建一個(gè) Job 的實(shí)例,然后再調(diào)用 execute 方法執(zhí)行。Quartz 沒有為 Job 設(shè)計(jì)帶參數(shù)的構(gòu)造函數(shù),因此需要通過額外的 JobDataMap 來存儲 Job 的屬性。JobDataMap 可以存儲任意數(shù)量的 Key,Value 對,例如:

1 jobDetail.getJobDataMap().put("myDescription", "my job description"); 
2 jobDetail.getJobDataMap().put("myValue", 1998); 
3 ArrayList<String> list = new ArrayList<String>(); 
4 list.add("item1"); 
5 jobDetail.getJobDataMap().put("myArray", list);

 

JobDataMap 中的數(shù)據(jù)可以通過下面的方式獲取:
01 public class JobDataMapTest implements Job {
02   
03     @Override
04     public void execute(JobExecutionContext context)
05             throws JobExecutionException {
06         //從context中獲取instName,groupName以及dataMap
07         String instName = context.getJobDetail().getName();
08         String groupName = context.getJobDetail().getGroup();
09         JobDataMap dataMap = context.getJobDetail().getJobDataMap();
10         //從dataMap中獲取myDescription,myValue以及myArray
11         String myDescription = dataMap.getString("myDescription");
12         int myValue = dataMap.getInt("myValue");
13         ArrayList<String> myArray = (ArrayListlt;Strin>) dataMap.get("myArray");
14         System.out.println("
15                 Instance =" + instName + ", group = " + groupName
16                 + ", description = " + myDescription + ", value =" + myValue
17                 + ", array item0 = " + myArray.get(0));
18   
19     }
20 }
21 /**
22 輸出結(jié)果:
23 Instance = myJob, group = myJobGroup, 
24 description = my job description, 
25 value =1998, array item0 = item1
26 */

 

Trigger

Trigger 的作用是設(shè)置調(diào)度策略。Quartz 設(shè)計(jì)了多種類型的 Trigger,其中最常用的是 SimpleTrigger 和 CronTrigger。

SimpleTrigger 適用于在某一特定的時(shí)間執(zhí)行一次,或者在某一特定的時(shí)間以某一特定時(shí)間間隔執(zhí)行多次。上述功能決定了 SimpleTrigger 的參數(shù)包括 start-time, end-time, repeat count, 以及 repeat interval。

Repeat count 取值為大于或等于零的整數(shù),或者常量 SimpleTrigger.REPEAT_INDEFINITELY。

Repeat interval 取值為大于或等于零的長整型。當(dāng) Repeat interval 取值為零并且 Repeat count 取值大于零時(shí),將會觸發(fā)任務(wù)的并發(fā)執(zhí)行。

Start-time 與 dnd-time 取值為 java.util.Date。當(dāng)同時(shí)指定 end-time 與 repeat count 時(shí),優(yōu)先考慮 end-time。一般地,可以指定 end-time,并設(shè)定 repeat count 為 REPEAT_INDEFINITELY。

以下是 SimpleTrigger 的構(gòu)造方法:

 public SimpleTrigger(String name, 
                       String group, 
                       Date startTime, 
                       Date endTime, 
                       int repeatCount, 
                       long repeatInterval) 

舉例如下:

創(chuàng)建一個(gè)立即執(zhí)行且僅執(zhí)行一次的 SimpleTrigger:

1 SimpleTrigger trigger=
2  new SimpleTrigger("myTrigger", "myGroup", new Date(), null, 0, 0L);

創(chuàng)建一個(gè)半分鐘后開始執(zhí)行,且每隔一分鐘重復(fù)執(zhí)行一次的 SimpleTrigger:

1 SimpleTrigger trigger=
2  new SimpleTrigger("myTrigger", "myGroup"
3     new Date(System.currentTimeMillis()+30*1000), null, 0, 60*1000);

創(chuàng)建一個(gè) 2011 年 6 月 1 日 8:30 開始執(zhí)行,每隔一小時(shí)執(zhí)行一次,一共執(zhí)行一百次,一天之后截止的 SimpleTrigger:

01 Calendar calendar = Calendar.getInstance(); 
02 calendar.set(Calendar.YEAR, 2011); 
03 calendar.set(Calendar.MONTH, Calendar.JUNE); 
04 calendar.set(Calendar.DAY_OF_MONTH, 1); 
05 calendar.set(Calendar.HOUR, 8); 
06 calendar.set(Calendar.MINUTE, 30); 
07 calendar.set(Calendar.SECOND, 0); 
08 calendar.set(Calendar.MILLISECOND, 0); 
09 Date startTime = calendar.getTime(); 
10 Date endTime = new Date (calendar.getTimeInMillis() +24*60*60*1000); 
11 SimpleTrigger trigger=new SimpleTrigger("myTrigger"
12        "myGroup", startTime, endTime, 100, 60*60*1000);

上述最后一個(gè)例子中,同時(shí)設(shè)置了 end-time 與 repeat count,則優(yōu)先考慮 end-time,總共可以執(zhí)行二十四次。

CronTrigger 的用途更廣,相比基于特定時(shí)間間隔進(jìn)行調(diào)度安排的 SimpleTrigger,CronTrigger 主要適用于基于日歷的調(diào)度安排。例如:每星期二的 16:38:10 執(zhí)行,每月一號執(zhí)行,以及更復(fù)雜的調(diào)度安排等。

CronTrigger 同樣需要指定 start-time 和 end-time,其核心在于 Cron 表達(dá)式,由七個(gè)字段組成:

 Seconds 
 Minutes 
 Hours 
 Day-of-Month 
 Month 
 Day-of-Week 
 Year (Optional field) 

舉例如下:

創(chuàng)建一個(gè)每三小時(shí)執(zhí)行的 CronTrigger,且從每小時(shí)的整點(diǎn)開始執(zhí)行:

 0 0 0/3  * * ? 

創(chuàng)建一個(gè)每十分鐘執(zhí)行的 CronTrigger,且從每小時(shí)的第三分鐘開始執(zhí)行:

 0 3/10 * * * ? 

創(chuàng)建一個(gè)每周一,周二,周三,周六的晚上 20:00 到 23:00,每半小時(shí)執(zhí)行一次的 CronTrigger:

 0 0/30 20-23 ? * MON-WED,SAT 

創(chuàng)建一個(gè)每月最后一個(gè)周四,中午 11:30-14:30,每小時(shí)執(zhí)行一次的 trigger:

 0 30 11-14/1 ? * 5L 

解釋一下上述例子中各符號的含義:

首先所有字段都有自己特定的取值,例如,Seconds 和 Minutes 取值為 0 到 59,Hours 取值為 0 到 23,Day-of-Month 取值為 0-31, Month 取值為 0-11,或者 JAN,F(xiàn)EB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV, DEC,Days-of-Week 取值為 1-7 或者 SUN, MON, TUE, WED, THU, FRI, SAT。每個(gè)字段可以取單個(gè)值,多個(gè)值,或一個(gè)范圍,例如 Day-of-Week 可取值為“MON,TUE,SAT”,“MON-FRI”或者“TUE-THU,SUN”。

通配符 * 表示該字段可接受任何可能取值。例如 Month 字段賦值 * 表示每個(gè)月,Day-of-Week 字段賦值 * 表示一周的每天。

/ 表示開始時(shí)刻與間隔時(shí)段。例如 Minutes 字段賦值 2/10 表示在一個(gè)小時(shí)內(nèi)每 20 分鐘執(zhí)行一次,從第 2 分鐘開始。

? 僅適用于 Day-of-Month 和 Day-of-Week。? 表示對該字段不指定特定值。適用于需要對這兩個(gè)字段中的其中一個(gè)指定值,而對另一個(gè)不指定值的情況。一般情況下,這兩個(gè)字段只需對一個(gè)賦值。

L 僅適用于 Day-of-Month 和 Day-of-Week。L 用于 Day-of-Month 表示該月最后一天。L 單獨(dú)用于 Day-of-Week 表示周六,否則表示一個(gè)月最后一個(gè)星期幾,例如 5L 或者 THUL 表示該月最后一個(gè)星期四。

W 僅適用于 Day-of-Month,表示離指定日期最近的一個(gè)工作日,例如 Day-of-Month 賦值為 10W 表示該月離 10 號最近的一個(gè)工作日。

# 僅適用于 Day-of-Week,表示該月第 XXX 個(gè)星期幾。例如 Day-of-Week 賦值為 5#2 或者 THU#2,表示該月第二個(gè)星期四。

CronTrigger 的使用如下:

 CronTrigger cronTrigger = new CronTrigger("myTrigger", "myGroup"); 
 try { 
     cronTrigger.setCronExpression("0 0/30 20-13 ? * MON-WED,SAT"); 
 } catch (Exception e) { 
     e.printStackTrace(); 
 } 

Job 與 Trigger 的松耦合設(shè)計(jì)是 Quartz 的一大特點(diǎn),其優(yōu)點(diǎn)在于同一個(gè) Job 可以綁定多個(gè)不同的 Trigger,同一個(gè) Trigger 也可以調(diào)度多個(gè) Job,靈活性很強(qiáng)。

Listener

除了上述基本的調(diào)度功能,Quartz 還提供了 listener 的功能。主要包含三種 listener:JobListener,TriggerListener 以及 SchedulerListener。當(dāng)系統(tǒng)發(fā)生故障,相關(guān)人員需要被通知時(shí),Listener 便能發(fā)揮它的作用。最常見的情況是,當(dāng)任務(wù)被執(zhí)行時(shí),系統(tǒng)發(fā)生故障,Listener 監(jiān)聽到錯(cuò)誤,立即發(fā)送郵件給管理員。下面給出 JobListener 的實(shí)例:


清單 7. JobListener 的實(shí)現(xiàn)
				 
 import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
import org.quartz.SchedulerException;


public class MyListener implements JobListener{

	@Override
	public String getName() {
		return "My Listener";
	}
	@Override
	public void jobWasExecuted(JobExecutionContext context,
			JobExecutionException jobException) {
		if(jobException != null){
			try {
				//停止Scheduler
				context.getScheduler().shutdown();
				System.out.println("
                Error occurs when executing jobs, shut down the scheduler ");
                // 給管理員發(fā)送郵件…
			} catch (SchedulerException e) {
				e.printStackTrace();
			}
		}
	}
}

從清單 7 可以看出,使用者只需要?jiǎng)?chuàng)建一個(gè) JobListener 的繼承類,重載需要觸發(fā)的方法即可。當(dāng)然,需要將 listener 的實(shí)現(xiàn)類注冊到 Scheduler 和 JobDetail 中:

 sched.addJobListener(new MyListener()); 
 jobDetail.addJobListener("My Listener"); // listener 的名字

使用者也可以將 listener 注冊為全局 listener,這樣便可以監(jiān)聽 scheduler 中注冊的所有任務(wù) :

 sched.addGlobalJobListener(new MyListener()); 

為了測試 listener 的功能,可以在 job 的 execute 方法中強(qiáng)制拋出異常。清單 7 中,listener 接收到異常,將 job 所在的 scheduler 停掉,阻止后續(xù)的 job 繼續(xù)執(zhí)行。scheduler、jobDetail 等信息都可以從 listener 的參數(shù) context 中檢索到。

清單 7 的輸出結(jié)果為:

 Generating report - myJob.myJob, type =FULL 
 Tue Feb 15 18:57:35 CST 2011 
 2011-2-15 18:57:35 org.quartz.core.JobRunShell run 
信息 : Job myJob.myJob threw a JobExecutionException: 
 org.quartz.JobExecutionException 
 at com.ibm.scheduler.QuartzListenerTest.execute(QuartzListenerTest.java:22) 
 at org.quartz.core.JobRunShell.run(JobRunShell.java:191) 
 at org.quartz.simpl.SimpleThreadPool$WorkerThread.run(SimpleThreadPool.java:516) 
 2011-2-15 18:57:35 org.quartz.core.QuartzScheduler shutdown 
信息 : Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutting down. 
 Error occurs when executing jobs, shut down the scheduler 

TriggerListener、SchedulerListener 與 JobListener 有類似的功能,只是各自觸發(fā)的事件不同,如 JobListener 觸發(fā)的事件為:

Job to be executed, Job has completed execution 等

TriggerListener 觸發(fā)的事件為:

Trigger firings, trigger mis-firings, trigger completions 等

SchedulerListener 觸發(fā)的事件為:

add a job/trigger, remove a job/trigger, shutdown a scheduler 等

讀者可以根據(jù)自己的需求重載相應(yīng)的事件。

JobStores

Quartz 的另一顯著優(yōu)點(diǎn)在于持久化,即將任務(wù)調(diào)度的相關(guān)數(shù)據(jù)保存下來。這樣,當(dāng)系統(tǒng)重啟后,任務(wù)被調(diào)度的狀態(tài)依然存在于系統(tǒng)中,不會丟失。默認(rèn)情況 下,Quartz 采用的是 org.quartz.simpl.RAMJobStore,在這種情況下,數(shù)據(jù)僅能保存在內(nèi)存中,系統(tǒng)重啟后會全部丟失。若想持久化數(shù)據(jù),需要采用 org.quartz.simpl.JDBCJobStoreTX。

實(shí)現(xiàn)持久化的第一步,是要?jiǎng)?chuàng)建 Quartz 持久化所需要的表格。在 Quartz 的發(fā)布包 docs/dbTables 中可以找到相應(yīng)的表格創(chuàng)建腳本。Quartz 支持目前大部分流行的數(shù)據(jù)庫。本文以 DB2 為例,所需要的腳本為 tables_db2.sql。首先需要對腳本做一點(diǎn)小的修改,即在開頭指明 Schema:

 SET CURRENT SCHEMA quartz; 

為了方便重復(fù)使用 , 創(chuàng)建表格前首先刪除之前的表格:
drop table qrtz_job_details;

 drop table qrtz_job_listeners; 

然后創(chuàng)建數(shù)據(jù)庫 sched,執(zhí)行 tables_db2.sql 創(chuàng)建持久化所需要的表格。

第二步,配置數(shù)據(jù)源。數(shù)據(jù)源與其它所有配置,例如 ThreadPool,均放在 quartz.properties 里:


清單 8. Quartz 配置文件
				 
 # Configure ThreadPool 
 org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool 
 org.quartz.threadPool.threadCount =  5 
 org.quartz.threadPool.threadPriority = 4 

 # Configure Datasources 
 org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX 
 org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate 
 org.quartz.jobStore.dataSource = db2DS 
 org.quartz.jobStore.tablePrefix = QRTZ_ 

 org.quartz.dataSource.db2DS.driver = com.ibm.db2.jcc.DB2Driver 
 org.quartz.dataSource.db2DS.URL = jdbc:db2://localhost:50001/sched 
 org.quartz.dataSource.db2DS.user = quartz 
 org.quartz.dataSource.db2DS.password = passw0rd 
 org.quartz.dataSource.db2DS.maxConnections = 5 

使用時(shí)只需要將 quatz.properties 放在 classpath 下面,不用更改一行代碼,再次運(yùn)行之前的任務(wù)調(diào)度實(shí)例,trigger、job 等信息便會被記錄在數(shù)據(jù)庫中。

將清單 4 中的 makeWeeklyTrigger 改成 makeSecondlyTrigger,重新運(yùn)行 main 函數(shù),在 sched 數(shù)據(jù)庫中查詢表 qrtz_simple_triggers 中的數(shù)據(jù)。其查詢語句為“db2 ‘ select repeat_interval, times_triggered from qrtz_simple_triggers ’”。結(jié)果 repeat_interval 為 1000,與程序中設(shè)置的 makeSecondlyTrigger 相吻合,times_triggered 值為 21。

停掉程序,將數(shù)據(jù)庫中記錄的任務(wù)調(diào)度數(shù)據(jù)重新導(dǎo)入程序運(yùn)行:

01 package com.ibm.scheduler; 
02 import org.quartz.Scheduler; 
03 import org.quartz.SchedulerException; 
04 import org.quartz.SchedulerFactory; 
05 import org.quartz.Trigger; 
06 import org.quartz.impl.StdSchedulerFactory; 
07  
08 public class QuartzReschedulerTest { 
09 public static void main(String[] args) throws SchedulerException { 
10 // 初始化一個(gè) Schedule Factory 
11 SchedulerFactory schedulerFactory = new StdSchedulerFactory(); 
12 // 從 schedule factory 中獲取 scheduler 
13 Scheduler scheduler = schedulerFactory.getScheduler(); 
14 // 從 schedule factory 中獲取 trigger 
15 Trigger trigger = scheduler.getTrigger("myTrigger", "myTriggerGroup"); 
16 // 重新開啟調(diào)度任務(wù)
17 scheduler.rescheduleJob("myTrigger", "myTriggerGroup", trigger); 
18 scheduler.start(); 
19
20 }

 

 

上面代碼中,schedulerFactory.getScheduler() 將 quartz.properties 的內(nèi)容加載到內(nèi)存,然后根據(jù)數(shù)據(jù)源的屬性初始化數(shù)據(jù)庫的鏈接,并將數(shù)據(jù)庫中存儲的數(shù)據(jù)加載到內(nèi)存。之后,便可以在內(nèi)存中查詢某一具體的 trigger,并將其重新啟動(dòng)。這時(shí)候重新查詢 qrtz_simple_triggers 中的數(shù)據(jù),發(fā)現(xiàn) times_triggered 值比原來增長了。

 

 

JCronTab

習(xí)慣使用 unix/linux 的開發(fā)人員應(yīng)該對 crontab 都不陌生。Crontab 是一個(gè)非常方便的用于 unix/linux 系統(tǒng)的任務(wù)調(diào)度命令。JCronTab 則是一款完全按照 crontab 語法編寫的 java 任務(wù)調(diào)度工具。

首先簡單介紹一下 crontab 的語法,與上面介紹的 Quartz 非常相似,但更加簡潔 , 集中了最常用的語法。主要由六個(gè)字段組成(括弧中標(biāo)識了每個(gè)字段的取值范圍):

 Minutes (0-59)
 Hours   (0-23) 
 Day-of-Month (1-31)
 Month    (1-12/JAN-DEC) 
 Day-of-Week  (0-6/SUN-SAT)
 Command 

與 Quartz 相比,省略了 Seconds 與 Year,多了一個(gè) command 字段,即為將要被調(diào)度的命令。JCronTab 中也包含符號“*”與“/”, 其含義與 Quartz 相同。

舉例如下:

每天 12 點(diǎn)到 15 點(diǎn) , 每隔 1 小時(shí)執(zhí)行一次 Date 命令:

 0 12-15/1 * * * Date 

每月 2 號凌晨 1 點(diǎn)發(fā)一封信給 zhjingbj@cn.ibm.com:

 0 1 2 * * mail -s “good” zhjingbj@cn.ibm.com 

每周一,周二,周三,周六的晚上 20:00 到 23:00,每半小時(shí)打印“normal”:

 0/30 20-23 * * MON-WED,SAT echo “normal”

JCronTab 借鑒了 crontab 的語法,其區(qū)別在于 command 不再是 unix/linux 的命令,而是一個(gè) Java 類。如果該類帶參數(shù),例如“com.ibm.scheduler.JCronTask2#run”,則定期執(zhí)行 run 方法;如果該類不帶參數(shù),則默認(rèn)執(zhí)行 main 方法。此外,還可以傳參數(shù)給 main 方法或者構(gòu)造函數(shù),例如“com.ibm.scheduler.JCronTask2#run Hello World“表示傳兩個(gè)參數(shù) Hello 和 World 給構(gòu)造函數(shù)。

JCronTab 與 Quartz 相比,其優(yōu)點(diǎn)在于,第一,支持多種任務(wù)調(diào)度的持久化方法,包括普通文件、數(shù)據(jù)庫以及 XML 文件進(jìn)行持久化;第二,JCronTab 能夠非常方便地與 Web 應(yīng)用服務(wù)器相結(jié)合,任務(wù)調(diào)度可以隨 Web 應(yīng)用服務(wù)器的啟動(dòng)自動(dòng)啟動(dòng);第三,JCronTab 還內(nèi)置了發(fā)郵件功能,可以將任務(wù)執(zhí)行結(jié)果方便地發(fā)送給需要被通知的人。

JCronTab 與 Web 應(yīng)用服務(wù)器的結(jié)合非常簡單,只需要在 Web 應(yīng)用程序的 web.xml 中添加如下行:

01 <servlet
02    <servlet-name>LoadOnStartupServlet</servlet-name
03    <servlet-class>org.jcrontab.web.loadCrontabServlet</servlet-class
04    <init-param
05  <param-name>PROPERTIES_FILE</param-name
06  <param-value>D:/Scheduler/src/jcrontab.properties</param-value
07    </init-param
08    <load-on-startup>1</load-on-startup
09  </servlet
10  <!-- Mapping of the StartUp Servlet --> 
11  <servlet-mapping
12    <servlet-name>LoadOnStartupServlet</servlet-name
13  <url-pattern>/Startup</url-pattern
14  </servlet-mapping>

 

在清單 10 中,需要注意兩點(diǎn):第一,必須指定 servlet-class 為 org.jcrontab.web.loadCrontabServlet,因?yàn)樗钦麄€(gè)任務(wù)調(diào)度的入口;第二,必須指定一個(gè)參數(shù)為 PROPERTIES_FILE,才能被 loadCrontabServlet 識別。

接下來,需要撰寫 D:/Scheduler/src/jcrontab.properties 的內(nèi)容,其內(nèi)容根據(jù)需求的不同而改變。

當(dāng)采用普通文件持久化時(shí),jcrontab.properties 的內(nèi)容主要包括:

 org.jcrontab.data.file = D:/Scheduler/src/crontab 
 org.jcrontab.data.datasource = org.jcrontab.data.FileSource 

其中數(shù)據(jù)來源 org.jcrontab.data.datasource 被描述為普通文件,即 org.jcrontab.data.FileSource。具體的文件即 org.jcrontab.data.file 指明為 D:/Scheduler/src/crontab。

Crontab 描述了任務(wù)的調(diào)度安排:

 */2 * * * * com.ibm.scheduler.JCronTask1 
 * * * * * com.ibm.scheduler.JCronTask2#run Hello World 

其中包含了兩條任務(wù)的調(diào)度,分別是每兩分鐘執(zhí)行一次 JCronTask1 的 main 方法,每一分鐘執(zhí)行一次 JCronTask2 的 run 方法。

01 package com.ibm.scheduler;
02   
03 import java.util.Date;
04   
05 public class JCronTask1 {
06   
07     private static int count = 0;
08   
09     public static void main(String[] args) {
10         System.out.println("--------------Task1-----------------");
11         System.out.println("Current Time = " + new Date() + ", Count = "
12                 + count++);
13     }
14 }
15   
16 package com.ibm.scheduler;
17   
18 import java.util.Date;
19   
20 public class JCronTask2 implements Runnable {
21   
22     private static int count = 0;
23   
24     private static String[] args;
25   
26     public JCronTask2(String[] args) {
27         System.out.println("--------------Task2-----------------");
28         System.out.println("Current Time = " + new Date() + ", Count = "
29                 + count++);
30         JCronTask2.args = args;
31     }
32   
33     @Override
34     public void run() {
35         System.out.println("enter into run method");
36         if (args != null && args.length > 0) {
37             for (int i = 0; i < args.length; i++) {
38          System.out.print("This is arg " + i + " " + args[i] + "\n");
39             }
40         }
41     }
42 }

 

 

到此為止,基于普通文件持久化的 JCronTab 的實(shí)例就全部配置好了。啟動(dòng) Web 應(yīng)用服務(wù)器,便可以看到任務(wù)調(diào)度的輸出結(jié)果:
 --------------Task2----------------- 
 Current Time = Tue Feb 15 09:22:00 CST 2011, Count = 0 
 enter into run method 
 This is arg 0 Hello 
 This is arg 1 World 
 --------------Task1----------------- 
 Current Time = Tue Feb 15 09:22:00 CST 2011, Count = 0 
 --------------Task2----------------- 
 Current Time = Tue Feb 15 09:23:00 CST 2011, Count = 1 
 enter into run method 
 This is arg 0 Hello 
 This is arg 1 World 
 --------------Task2----------------- 
 Current Time = Tue Feb 15 09:24:00 CST 2011, Count = 2 
 enter into run method 
 This is arg 0 Hello 
 This is arg 1 World 
 --------------Task1----------------- 
 Current Time = Tue Feb 15 09:24:00 CST 2011, Count = 1 

通過修改 jcrontab.properties 中 datasource,可以選擇采用數(shù)據(jù)庫或 xml 文件持久化,感興趣的讀者可以參考 進(jìn)階學(xué)習(xí) JCronTab

此外,JCronTab 還內(nèi)置了發(fā)郵件功能,可以將任務(wù)執(zhí)行結(jié)果方便地發(fā)送給需要被通知的人。其配置非常簡單,只需要在 jcontab.properties 中添加幾行配置即可:

 org.jcrontab.sendMail.to= Ther email you want to send to 
 org.jcrontab.sendMail.from=The email you want to send from 
 org.jcrontab.sendMail.smtp.host=smtp server 
 org.jcrontab.sendMail.smtp.user=smtp username 
 org.jcrontab.sendMail.smtp.password=smtp password 

結(jié)束語

本文介紹了四種常用的對任務(wù)進(jìn)行調(diào)度的 Java 實(shí)現(xiàn)方法,即 Timer,ScheduledExecutor, Quartz 以及 JCronTab。文本對每種方法都進(jìn)行了實(shí)例解釋,并對其優(yōu)缺點(diǎn)進(jìn)行比較。對于簡單的基于起始時(shí)間點(diǎn)與時(shí)間間隔的任務(wù)調(diào)度,使用 Timer 就足夠了;如果需要同時(shí)調(diào)度多個(gè)任務(wù),基于線程池的 ScheduledTimer 是更為合適的選擇;當(dāng)任務(wù)調(diào)度的策略復(fù)雜到難以憑借起始時(shí)間點(diǎn)與時(shí)間間隔來描述時(shí),Quartz 與 JCronTab 則體現(xiàn)出它們的優(yōu)勢。熟悉 Unix/Linux 的開發(fā)人員更傾向于 JCronTab,且 JCronTab 更適合與 Web 應(yīng)用服務(wù)器相結(jié)合。Quartz 的 Trigger 與 Job 松耦合設(shè)計(jì)使其更適用于 Job 與 Trigger 的多對多應(yīng)用場景。

原文轉(zhuǎn)自:IBM developerWorks