任務調度框架Quartz

任務調度框架 Quartz 文檔

概述

各種企業應用幾乎都會碰到任務調度的需求,就拿論壇來說:每隔半個小時生成精華文章的 RSS 文件,每天凌晨統計論壇用戶的積分排名,每隔 30 分鐘執行鎖定用戶解鎖任務。

對於一個典型的 MIS 系統來說,在每月 1 號凌晨統計上個月各部門的業務數據生成月報表,每半個小時查詢用戶是否已經有快到期的待處理業務 …… ,這樣的例子俯拾皆是,不勝枚舉。

任務調度本身涉及到多線程併發、運行時間規則制定和解析、場景保持與恢復、線程池維護等諸多方面的工作。如果直接使用自定義線程這種刀耕火種的原始辦法,開發任務調度程序是一項頗具挑戰性的工作。 Java 開源的好處就是:領域問題都能找到現成的解決方案。

OpenSymphony 所提供的 Quartz 2001 年發佈版本以來已經被衆多項目作爲任務調度的解決方案, Quartz 在提供巨大靈活性的同時並未犧牲其簡單性,它所提供的強大功能使你可以應付絕大多數的調度需求。

Quartz 在開源任務調度框架中的翹首,它提供了強大任務調度機制,難能可貴的是它同時保持了使用的簡單性。 Quartz 允許開發人員靈活地定義觸發器的調度時間表,並可以對觸發器和任務進行關聯映射。

此外, Quartz 提供了調度運行環境的持久化機制,可以保存並恢復調度現場,即使系統因故障關閉,任務調度現場數據並不會丟失。此外, Quartz 還提供了組件式的偵聽器、各種插件、線程池等功能。

瞭解 Quartz 體系結構

Quartz 對任務調度的領域問題進行了高度的抽象,提出了調度器、任務和觸發器這 3 個核心的概念,並在 org.quartz 通過接口和類對重要的這些核心概念進行描述:

●Job :是一個接口,只有一個方法 void execute(JobExecutionContext context) ,開發者實現該接口定義運行任務, JobExecutionContext 類提供了調度上下文的各種信息。 Job 運行時的信息保存在 JobDataMap 實例中;

●JobDetail Quartz 在每次執行 Job 時,都重新創建一個 Job 實例,所以它不直接接受一個 Job 的實例,相反它接收一個 Job 實現類,以便運行時通過 newInstance() 的反射機制實例化 Job 。因此需要通過一個類來描述 Job 的實現類及其它相關的靜態信息,如 Job 名字、描述、關聯監聽器等信息, JobDetail 承擔了這一角色。

通過該類的構造函數可以更具體地瞭解它的功用: JobDetail(java.lang.String name, java.lang.String group, java.lang.Class jobClass) ,該構造函數要求指定 Job 的實現類,以及任務在 Scheduler 中的組名和 Job 名稱;

●Trigger :是一個類,描述觸發 Job 執行的時間觸發規則。主要有 SimpleTrigger CronTrigger 這兩個子類。當僅需觸發一次或者以固定時間間隔週期執行, SimpleTrigger 是最適合的選擇;而 CronTrigger 則可以通過 Cron 表達式定義出各種複雜時間規則的調度方案:如每早晨 9:00 執行,週一、週三、週五下午 5:00 執行等;

●Calendar org.quartz.Calendar java.util.Calendar 不同,它是一些日曆特定時間點的集合(可以簡單地將 org.quartz.Calendar 看作 java.util.Calendar 的集合 ——java.util.Calendar 代表一個日曆時間點,無特殊說明後面的 Calendar 即指 org.quartz.Calendar )。一個 Trigger 可以和多個 Calendar 關聯,以便排除或包含某些時間點。

假設,我們安排每週星期一早上 10:00 執行任務,但是如果碰到法定的節日,任務則不執行,這時就需要在 Trigger 觸發機制的基礎上使用 Calendar 進行定點排除。針對不同時間段類型, Quartz org.quartz.impl.calendar 包下提供了若干個 Calendar 的實現類,如 AnnualCalendar MonthlyCalendar WeeklyCalendar 分別針對每年、每月和每週進行定義;

●Scheduler :代表一個 Quartz 的獨立運行容器, Trigger JobDetail 可以註冊到 Scheduler 中,兩者在 Scheduler 中擁有各自的組及名稱,組及名稱是 Scheduler 查找定位容器中某一對象的依據, Trigger 的組及名稱必須唯一, JobDetail 的組和名稱也必須唯一(但可以和 Trigger 的組和名稱相同,因爲它們是不同類型的)。 Scheduler 定義了多個接口方法,允許外部通過組及名稱訪問和控制容器中 Trigger JobDetail

Scheduler 可以將 Trigger 綁定到某一 JobDetail 中,這樣當 Trigger 觸發時,對應的 Job 就被執行。一個 Job 可以對應多個 Trigger ,但一個 Trigger 只能對應一個 Job 。可以通過 SchedulerFactory 創建一個 Scheduler 實例。 Scheduler 擁有一個 SchedulerContext ,它類似於 ServletContext ,保存着 Scheduler 上下文信息, Job Trigger 都可以訪問 SchedulerContext 內的信息。 SchedulerContext 內部通過一個 Map ,以鍵值對的方式維護這些上下文數據, SchedulerContext 爲保存和獲取數據提供了多個 put() getXxx() 的方法。可以通過 Scheduler# getContext() 獲取對應的 SchedulerContext 實例;

●ThreadPool Scheduler 使用一個線程池作爲任務運行的基礎設施,任務通過共享線程池中的線程提高運行效率。

Job 有一個 StatefulJob 子接口,代表有狀態的任務,該接口是一個沒有方法的標籤接口,其目的是讓 Quartz 知道任務的類型,以便採用不同的執行方案。無狀態任務在執行時擁有自己的 JobDataMap 拷貝,對 JobDataMap 的更改不會影響下次的執行。而有狀態任務共享共享同一個 JobDataMap 實例,每次任務執行對 JobDataMap 所做的更改會保存下來,後面的執行可以看到這個更改,也即每次執行任務後都會對後面的執行發生影響。

正因爲這個原因,無狀態的 Job 可以併發執行,而有狀態的 StatefulJob 不能併發執行,這意味着如果前次的 StatefulJob 還沒有執行完畢,下一次的任務將阻塞等待,直到前次任務執行完畢。有狀態任務比無狀態任務需要考慮更多的因素,程序往往擁有更高的複雜度,因此除非必要,應該儘量使用無狀態的 Job

如果 Quartz 使用了數據庫持久化任務調度信息,無狀態的 JobDataMap 僅會在 Scheduler 註冊任務時保持一次,而有狀態任務對應的 JobDataMap 在每次執行任務後都會進行保存。

Trigger 自身也可以擁有一個 JobDataMap ,其關聯的 Job 可以通過 JobExecutionContext#getTrigger().getJobDataMap() 獲取 Trigger 中的 JobDataMap 。不管是有狀態還是無狀態的任務,在任務執行期間對 Trigger JobDataMap 所做的更改都不會進行持久,也即不會對下次的執行產生影響。

Quartz 擁有完善的事件和監聽體系,大部分組件都擁有事件,如任務執行前事件、任務執行後事件、觸發器觸發前事件、觸發後事件、調度器開始事件、關閉事件等等,可以註冊相應的監聽器處理感興趣的事件。

1 描述了 Scheduler 的內部組件結構, SchedulerContext 提供 Scheduler 全局可見的上下文信息,每一個任務都對應一個 JobDataMap ,虛線表達的 JobDataMap 表示對應有狀態的任務:

1 Scheduler 結構圖

一個 Scheduler 可以擁有多個 Triger 組和多個 JobDetail 組,註冊 Trigger JobDetail 時,如果不顯式指定所屬的組, Scheduler 將放入到默認組中,默認組的組名爲 Scheduler.DEFAULT_GROUP 。組名和名稱組成了對象的全名,同一類型對象的全名不能相同。

Scheduler 本身就是一個容器,它維護着 Quartz 的各種組件並實施調度的規則。 Scheduler 還擁有一個線程池,線程池爲任務提供執行線程 —— 這比執行任務時簡單地創建一個新線程要擁有更高的效率,同時通過共享節約資源的佔用。通過線程池組件的支持,對於繁忙度高、壓力大的任務調度, Quartz 將可以提供良好的伸縮性。

提示: Quartz 完整下載包 examples 目錄下擁有 10 多個實例,它們是快速掌握 Quartz 應用很好的實例。

使用 SimpleTrigger

SimpleTrigger 擁有多個重載的構造函數,用以在不同場合下構造出對應的實例:

●SimpleTrigger(String name, String group) :通過該構造函數指定 Trigger 所屬組和名稱;

●SimpleTrigger(String name, String group, Date startTime) :除指定 Trigger 所屬組和名稱外,還可以指定觸發的開發時間;

●SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval) :除指定以上信息外,還可以指定結束時間、重複執行次數、時間間隔等參數;

●SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval) :這是最複雜的一個構造函數,在指定觸發參數的同時,還通過 jobGroup jobName ,讓該 Trigger Scheduler 中的某個任務關聯起來。

通過實現 org.quartz..Job 接口,可以使 Java 類化身爲可調度的任務。代碼清單 1 提供了 Quartz 任務的一個示例:

代碼清單 1 SimpleJob :簡單的 Job 實現類

package com.baobaotao.basic.quartz;

import java.util.Date;

import org.quartz.Job;

import org.quartz.JobExecutionContext;

import org.quartz.JobExecutionException;

public class SimpleJob implements Job {

實例 Job 接口方法

public void execute(JobExecutionContext jobCtx) throws JobExecutionException {

System.out.println(jobCtx.getTrigger().getName()+ " triggered. time is:" + (new Date()));

}

}

這個類用一條非常簡單的輸出語句實現了 Job 接口的 execute(JobExecutionContext context) 方法,這個方法可以包含想要執行的任何代碼。下面,我們通過 SimpleTrigger SimpleJob 進行調度:

代碼清單 2 SimpleTriggerRunner :使用 SimpleTrigger 進行調度

package com.baobaotao.basic.quartz;

import java.util.Date;

import org.quartz.JobDetail;

import org.quartz.Scheduler;

import org.quartz.SchedulerFactory;

import org.quartz.SimpleTrigger;

import org.quartz.impl.StdSchedulerFactory;

public class SimpleTriggerRunner {

public static void main(String args[]) {

try {

創建一個 JobDetail 實例,指定 SimpleJob

JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class );

通過 SimpleTrigger 定義調度規則:馬上啓動,每 2 秒運行一次,共運行 100

SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");

simpleTrigger.setStartTime(new Date());

simpleTrigger.setRepeatInterval(2000);

simpleTrigger.setRepeatCount(100);

通過 SchedulerFactory 獲取一個調度器實例

SchedulerFactory schedulerFactory = new StdSchedulerFactory();

Scheduler scheduler = schedulerFactory.getScheduler();

scheduler.scheduleJob(jobDetail, simpleTrigger); 註冊並進行調度

scheduler.start(); 調度啓動

} catch (Exception e) {

e.printStackTrace();

}

}

}

首先在 處通過 JobDetail 封裝 SimpleJob ,同時指定 Job Scheduler 中所屬組及名稱,這裏,組名爲 jGroup1 ,而名稱爲 job1_1

處創建一個 SimpleTrigger 實例,指定該 Trigger Scheduler 中所屬組及名稱。接着設置調度的時間規則。

最後,需要創建 Scheduler 實例,並將 JobDetail Trigger 實例註冊到 Scheduler 中。這裏,我們通過 StdSchedulerFactory 獲取一個 Scheduler 實例,並通過 scheduleJob(JobDetail jobDetail, Trigger trigger) 完成兩件事:

1) JobDetail Trigger 註冊到 Scheduler 中;

2) Trigger 指派給 JobDetail ,將兩者關聯起來。

Scheduler 啓動後, Trigger 將定期觸發並執行 SimpleJob execute(JobExecutionContext jobCtx) 方法,然後每 10 秒重複一次,直到任務被執行 100 次後停止。

還可以通過 SimpleTrigger setStartTime(java.util.Date startTime) setEndTime(java.util.Date endTime) 指定運行的時間範圍,當運行次數和時間範圍衝突時,超過時間範圍的任務運行不被執行。如可以通過 simpleTrigger.setStartTime(new Date(System.currentTimeMillis() + 60000L)) 指定 60 秒鐘以後開始。

除了通過 scheduleJob(jobDetail, simpleTrigger) 建立 Trigger JobDetail 的關聯,還有另外一種關聯 Trigger JobDetail 的方式:

JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class);

SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");

simpleTrigger.setJobGroup("jGroup1"); -1 :指定關聯的 Job 組名

simpleTrigger.setJobName("job1_1"); -2 :指定關聯的 Job 名稱

scheduler.addJob(jobDetail, true); 註冊 JobDetail

scheduler.scheduleJob(simpleTrigger); 註冊指定了關聯 JobDetail Trigger

在這種方式中, Trigger 通過指定 Job 所屬組及 Job 名稱,然後使用 Scheduler scheduleJob(Trigger trigger) 方法註冊 Trigger 。有兩個值得注意的地方:

通過這種方式註冊的 Trigger 實例必須已經指定 Job 組和 Job 名稱,否則調用註冊 Trigger 的方法將拋出異常;

引用的 JobDetail 對象必須已經存在於 Scheduler 中。也即,代碼中 的先後順序不能互換。

在構造 Trigger 實例時,可以考慮使用 org.quartz.TriggerUtils 工具類,該工具類不但提供了衆多獲取特定時間的方法,還擁有衆多獲取常見 Trigger 的方法,如 makeSecondlyTrigger(String trigName) 方法將創建一個每秒執行一次的 Trigger ,而 makeWeeklyTrigger(String trigName, int dayOfWeek, int hour, int minute) 將創建一個每星期某一特定時間點執行一次的 Trigger 。而 getEvenMinuteDate(Date date) 方法將返回某一時間點一分鐘以後的時間。

使用 CronTrigger

CronTrigger 能夠提供比 SimpleTrigger 更有具體實際意義的調度方案,調度規則基於

發佈了62 篇原創文章 · 獲贊 2 · 訪問量 5993
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章