1. 程式人生 > >Quartz任務排程(1)概念例析快速入門

Quartz任務排程(1)概念例析快速入門

Quartz框架需求引入

在現實開發中,我們常常會遇到需要系統在特定時刻完成特定任務的需求,在《spring學習筆記(14)引介增強詳解:定時器例項:無侵入式動態增強類功能》,我們通過引介增強來簡單地模擬實現了一個定時器。它可能只需要我們自己維護一條執行緒就足以實現定時監控。但在實際開發中,我們遇到的需求會複雜很多,可能涉及多點任務排程,需要我們多執行緒併發協作、執行緒池的維護、對執行時間規則進行更細粒度的規劃、執行執行緒現場的保持與恢復等等。如果我們選擇自己來造輪子,可能會遇到許多難題。這時候,引入Quartz任務排程框架會是一個很好的選擇,它:
1. 允許我們靈活而細粒度地設定任務觸發時間,比如常見的每隔多長時間,或每天特定時刻、特定日子(如節假日),都能靈活地配置這些成相應時間點來觸發我們的任務
2. 提供了排程環境的持久化機制,可以將任務內容儲存到資料庫中,在完成後刪除記錄,這樣就能避免因系統故障而任務尚未執行且不再執行的問題。
3. 提供了元件式的偵聽器、各種外掛、執行緒池等。通過偵聽器,我們可以引入我們的事件機制,配合上非同步呼叫,既能為我們的業務處理類解耦,同時還可能提升使用者體驗,關於事件機制的基本概念、解耦特性及其非同步呼叫可移步參考我的另一篇文章

《spring學習筆記(15)趣談spring 事件:實現業務邏輯解耦,非同步呼叫提升使用者體驗 》

例項解析概念

在quartz中,有幾個核心類和介面:Job、JobDetail、Trigger、Calendar、Scheduler。
下面我們結合例項來分析這些類的角色定位。
現在我們有一個新聞網站,它有一張任務日誌表,記錄著我們的不同任務,比如每隔三十分鐘要根據文章的閱讀量和評論量來生成我們的最熱文章列表。在每天早晚12點,定時從其他新聞網站扒取一定量新聞,在每週一晚上12點到3點進行論壇封閉維護,而如果遇到節假日則不維護等。在以上例項中:

  1. 生成最熱文章扒取新聞
    論壇封閉維護都是我們的Job,它定義了我們的需要執行的任務,是一個抽象的介面.
  2. 如果我們要具體到每隔三十分鐘生成最熱文章早晚12點扒取新聞等,在我們的具體任務執行時刻,我們就需要能夠描述Job及其他相關靜態資訊jobDetail,它相當於是我們的Job+具體實現細節
  3. Trigger則描述了Job執行的時間觸發規則,比如每隔三十分鐘早晚12點
  4. 而這裡的Calendar可以看成是一些日曆特定時間點的集合,比如我們這裡遇到節假日則不維護,節假日如國慶節、愚人節等等,都是我們的日曆特定時間點。
  5. Scheduler就是我們的任務日誌表,它是一個容器,記載(容納)了我們前面的工作、觸發時間等內容。

具體用法詳解

通過例項,我們對quartz的核心類有了較清晰的功能定位,根據Quratz的不同版本,這幾個核心類有較大改動,具體的操作不太相同,但是思路是相同的;比如1.+版本jar包中,JobDetail是個類,直接通過構造方法與Job類關聯。SimpleTrigger和CornTrigger是類;在2.+jar包中,JobDetail是個介面,SimpleTrigger和CornTrigger是介面。下面詳細地分析它們的具體用法:

1. Job

Job是一個介面,只有一個void execute(JobExecutionContext jec)方法,JobExecutionContext提供了我們的任務排程上下文資訊,比如,我們可以通過JobExecutionContext獲取job相對應的JobDetail、Trigger等資訊,我們在配置自己的內容時,需要實現此類,並在execute中重寫我們的任務內容。下面是我們的扒取新聞工作例項:

public class pickNewsJob implements Job {

    @Override
    public void execute(JobExecutionContext jec) throws JobExecutionException {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        System.out.println("在"+sdf.format(new Date())+"扒取新聞");
    }
}

2. JobDetail

Quartz在每次執行任務時,都會建立一個Job例項,併為其配置上下文資訊,jobDetail有一個成員屬性JobDataMap,存放了我們Job執行時的具體資訊,在後面我們會詳細提到。
1. 在1.+版本中,它作為一個類,常用的構造方法有:JobDetail(String name, String group, Class jobClass),我們需要指定job的名稱,組別和實現了Job介面的自定義任務類。例項如JobDetail jobDetail =new JobDetail("job1", "jgroup1", pickNewsJob.class);
2. 而在2.+版本中,我們則通過一下方法建立 JobBuilder.newJob(自定義任務類).withIdentity(任務名稱,組名).build();例項如JobDetail jobDetail = JobBuilder.newJob(pickNewsJob.class).withIdentity(“job1”,”group1”).build();`

3. Scheduler

先講Scheduler,方便後講解Trigger時測試。
Scheduler作為我們的“任務記錄表”,裡面(可以)配置大量的Trigger和JobDetail,兩者在 Scheduler中擁有各自的組及名稱,組及名稱是Scheduler查詢定位容器中某一物件的依據,Trigger的組及名稱必須唯一,JobDetail的組和名稱也必須唯一(但可以和Trigger的組和名稱相同,因為它們是不同型別的)。Scheduler可以將Trigger繫結到某一JobDetail中,這樣當Trigger觸發時,對應的Job就被執行。一個Job可以對應多個Trigger,但一個Trigger只能對應一個Job。可以通過SchedulerFactory建立一個Scheduler例項。下面是使用Schduler的例項:

SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
//將jobDetail和Trigger註冊到一個scheduler裡,建立起兩者的關聯關係
scheduler.scheduleJob(jobDetail,Trigger);

scheduler.start();//開始任務排程

在一個scheduler被建立後,它處於”STAND-BY”模式,在觸發任何job前需要使用它的start()方法來啟動。同樣的,如果我們想根據我們的業務邏輯來停止定時方案執行,可以使用scheduler.standby()方法

3. Trigger

Trigger描述了Job執行的時間觸發規則,主要有SimpleTrigger和CronTrigger兩個子類。

1. SimpleTrigger

如果嵌入事件機制只觸發一次,或意圖使Job以固定時間間隔觸發,則使用SimpleTrigger較合適,它有多個建構函式,其中一個最複雜的建構函式為:
SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date
endTime, int repeatCount, long repeatInterval)
引數依次為觸發器名稱、觸發器所在組名、工作名、工作所在組名、開始日期、結束日期、重複次數、重複間隔。
1. 如果我們不需同時設定這麼多屬性,可呼叫其他只有部分引數的構造方法,其他引數也可以通過set方法動態設定。
2. 這裡需要注意的是,如果到了我們設定的endTime,即時重複次數repeatCount還沒有達到我們預設定的次數。任務也不會再此執行。
下面是1.+版本的建立例項

//建立一個觸發器,使任務從現在開始、每隔兩秒執行一次,共執行10次
SimpleTrigger simpleTrigger = new SimpleTrigger("triiger1");//至少需要設定名字以標識當前觸發器,否則在呼叫時會報錯
simpleTrigger.setStartTime(new Date());
simpleTrigger.setRepeatInterval(2000);
simpleTrigger.setRepeatCount(10);

下面是2.+版本的建立例項

SimpleTrigger simpleTrigger = TriggerBuilder
                .newTrigger()
                .withIdentity("trigger1")//配置觸發器名稱
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(10, 2))//配置重複次數和間隔時間
                .startNow()//設定從當前開始
                .build();//建立操作

通過TriggerBuilder,我們可以通過方法方便地配置觸發器的各種引數。

2. CronTrigger

通過Cron表示式定義複雜的時間排程方案,具體內容我們在下一篇詳細提到

4. Calendar

在實際的開發中,我們可能需要根據節假日來調整我們的任務排程方案。例項如下:

//第一步:建立節假日類
 // ②四一愚人節    
Calendar foolDay = new GregorianCalendar();    //這裡的Calendar是 java.util.Calendar。根據當前時間所在的預設時區建立一個“日子”
foolDay.add(Calendar.MONTH, 4);    
foolDay.add(Calendar.DATE, 1);    
// ③國慶節    
Calendar nationalDay = new GregorianCalendar();    
nationalDay.add(Calendar.MONTH, 10);    
nationalDay.add(Calendar.DATE, 1);  

//第二步:建立AnnualCalendar,它的作用是排除排除每一年中指定的一天或多天
AnnualCalendar holidays = new AnnualCalendar();
//設定排除日期有兩種方法
// 第一種:排除的日期,如果設定為false則為包含(included)   
holidays.setDayExcluded(foolDay, true);  
holidays.setDayExcluded(nationalDay, true); 
//第二種,建立一個數組。
ArrayList<Calendar> calendars = new ArrayList<Calendar>();
calendars.add(foolDay);
calendars.add(nationalDay);
holidays.setDaysExcluded(calendars);

//第三步:將holidays新增進我們的觸發器
simpleTrigger.setCalendarName("holidays");

//第四步:設定好然後需要在我們的scheduler中註冊
scheduler.addCalendar("holidays",holidays, false,false);,注意這裡的第一個引數為calendarName,需要和觸發器中新增的Calendar名字像對應。

在這裡,除了可以使用AnnualCalendar外,還有CronCalendar(表示式),DailyCalendar(指定的時間範圍內的每一天),HolidayCalendar(排除節假日),MonthlyCalendar(排除月份中的數天),WeeklyCalendar(排除星期中的一天或多天)

至此,我們的核心類基本講解完畢,下面附上我們的完整測試程式碼:

/*********************1.+版本*********************/
public class pickNewsJob implements Job {

    @Override
    public void execute(JobExecutionContext jec) throws JobExecutionException {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        System.out.println("在" + sdf.format(new Date()) + "扒取新聞");
    }

    public static void main(String args[]) throws SchedulerException {
        JobDetail jobDetail =new JobDetail("job1", "jgroup1", pickNewsJob.class); 
        SimpleTrigger simpleTrigger = new SimpleTrigger("triiger1");
        simpleTrigger.setStartTime(new Date());
        simpleTrigger.setRepeatInterval(2000);
        simpleTrigger.setRepeatCount(10);
        simpleTrigger.setCalendarName("holidays");

        //設定需排除的特殊假日
        AnnualCalendar holidays = new AnnualCalendar();
        // 四一愚人節
        Calendar foolDay = new GregorianCalendar(); // 這裡的Calendar是 ava.util.Calendar。根據當前時間所在的預設時區建立一個“日子”
        foolDay.add(Calendar.MONTH, 4);
        foolDay.add(Calendar.DATE, 1);
        // 國慶節
        Calendar nationalDay = new GregorianCalendar();
        nationalDay.add(Calendar.MONTH, 10);
        nationalDay.add(Calendar.DATE, 1);
        //排除的日期,如果設定為false則為包含(included)
        holidays.setDayExcluded(foolDay, true);
        holidays.setDayExcluded(nationalDay, true);
        /*方法2:通過陣列設定
        ArrayList<Calendar> calendars = new ArrayList<Calendar>();
        calendars.add(foolDay);
        calendars.add(nationalDay);
        holidays.setDaysExcluded(calendars);*/

        //建立scheduler
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        scheduler.addCalendar("holidays", holidays, false, false);

        scheduler.scheduleJob(jobDetail, simpleTrigger);
        scheduler.start();

    }
}
/*******************2.+版本***************/
public class pickNewsJob implements Job {

    @Override
    public void execute(JobExecutionContext jec) throws JobExecutionException {
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        System.out.println("在"+sdf.format(new Date())+"扒取新聞");
    }

    public static void main(String args[]) throws SchedulerException {
        JobDetail jobDetail = JobBuilder.newJob(pickNewsJob.class)
                .withIdentity("job1", "group1").build();
        SimpleTrigger simpleTrigger = TriggerBuilder
                .newTrigger()
                .withIdentity("trigger1")
                .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(10, 2))
                .startNow()
                .build();

        //設定需排除的特殊假日
        AnnualCalendar holidays = new AnnualCalendar();
        // 四一愚人節
        Calendar foolDay = new GregorianCalendar(); // 這裡的Calendar是 ava.util.Calendar。根據當前時間所在的預設時區建立一個“日子”
        foolDay.add(Calendar.MONTH, 4);
        foolDay.add(Calendar.DATE, 1);
        // 國慶節
        Calendar nationalDay = new GregorianCalendar();
        nationalDay.add(Calendar.MONTH, 10);
        nationalDay.add(Calendar.DATE, 1);
        //排除的日期,如果設定為false則為包含(included)
        holidays.setDayExcluded(foolDay, true);
        holidays.setDayExcluded(nationalDay, true);
        /*方法2:通過陣列設定
        ArrayList<Calendar> calendars = new ArrayList<Calendar>();
        calendars.add(foolDay);
        calendars.add(nationalDay);
        holidays.setDaysExcluded(calendars);*/

        //建立scheduler
        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        Scheduler scheduler = schedulerFactory.getScheduler();
        scheduler.addCalendar("holidays", holidays, false, false);

        scheduler.scheduleJob(jobDetail, simpleTrigger);
        scheduler.start();
    }
}

可見,兩個不同版本的主要區別在於JobDetail和Triiger的配置。

此外,除了使用scheduler.scheduleJob(jobDetail, simpleTrigger)來建立jobDetail和simpleTrigger的關聯外,在1.+版本中的配置還可以採用如下所示方式

simpleTrigger.setJobName("job1");//jobName和我們前面jobDetail的的名字一致
simpleTrigger.setJobGroup("jgroup1");//jobGroup和我們之前jobDetail的組名一致
scheduler.addJob(jobDetail, true);//註冊jobDetail,此時jobDetail必須已指定job名和組名,否則會拋異常Trigger's related Job's name cannot be null
scheduler.scheduleJob(simpleTrigger);//註冊triiger必須在註冊jobDetail之後,否則會拋異常Trigger's related Job's name cannot be null

這裡還需要注意的是,如果我們使用scheduler.addCalendar("holidays", holidays, false, false)必須在向scheduler註冊trigger之前scheduler.scheduleJob(simpleTrigger),否則會拋異常:Calendar not found: holidays

而在2.+版本中,我嘗試在建立triiger時用forJob(“job1”, “jgroup1”)來繫結job名和組名

SimpleTrigger simpleTrigger = TriggerBuilder
    .newTrigger()
    .withIdentity("trigger1")
    .forJob("job1", "jgroup1")//在這裡繫結
    .withSchedule(SimpleScheduleBuilder.repeatSecondlyForTotalCount(10, 2))
    .startNow()
    .build();

//後面是一樣的
scheduler.addJob(jobDetail, true);
scheduler.scheduleJob(simpleTrigger);

在執行時,卻會丟擲異常: Jobs added with no trigger must be durable.
顯然是繫結失敗了,目前暫未找到解決方法,如果有找到解決方法的朋友,懇請告訴我一下,十分感謝!