還不會 Quartz?如果你還沒有接觸過Quartz,那麼你可能錯過了一個很棒的任務調度框架!Quartz 提供了一種靈活、可靠的方式來管理和執行定時任務,讓咱們的定時任務更加優雅。 ...
前言
還不會 Quartz?如果你還沒有接觸過Quartz,那麼你可能錯過了一個很棒的任務調度框架!Quartz 提供了一種靈活、可靠的方式來管理和執行定時任務,讓咱們的定時任務更加優雅。本篇文章將為你介紹 Quartz 框架的核心概念、API 和實戰技巧,讓你輕鬆上手。也不用擔心,作為過來人,我會把難懂的概念和術語解釋清楚,讓你看完本篇文章後,就知道該如何操作 Quartz。當然,本篇文章難免有不足之處,在此歡迎大家指出。那廢話少說,下麵我們開始吧!
什麼是 Quartz?
Quartz 是一個功能豐富的開源任務調度框架(job scheduling library)。從最小的獨立的 Java 應用程式到最大的電子商務系統,它幾乎都可以集成。Quartz 可用於創建簡單或複雜的調度,以執行數十、數百個甚至數萬個任務;這些任務被定義為標準 Java 組件,這些組件可以執行你想讓他做的任何事情。Quartz 調度程式包括許多企業級特性,例如支持 JTA 事務(Java Transaction API,簡寫 JTA)和集群。
註意:Job == 任務
JTA,即 Java Transaction API,JTA 允許應用程式執行分散式事務處理——在兩個或多個網路電腦資源上訪問並且更新數據。
為什麼學習 Quartz?
定時任務直接用 Spring 提供的 @Schedule
不行嗎?為什麼還要學習 Quartz?有什麼好處?
是的,一開始我也是這麼想的,但是某些場景,單靠 @Schedule
你就實現不了了。
比如我們需要對定時任務進行增刪改查,是吧,@Schedule
就實現不了,你不可能每次新增一個定時任務都去手動改代碼來添加吧。而 Quartz 就能夠實現對任務的增刪改查。當然,這隻是 Quartz 的好處之一。
Quartz 的特性
運行時環境
- Quartz 可以嵌入另一個獨立的應用程式中運行
- Quartz 可以在應用程式伺服器(比如 Tomcat)中實例化,並參與 XA 事務(XA 是一個分散式事務協議)
- Quartz 可以作為一個獨立程式運行(在其自己的Java虛擬機中),我們通過 RMI(Remote Method Invocation,遠程方法調用)使用它
- Quartz 可以實例化為一個獨立程式集群(具有負載平衡和故障轉移功能),用於執行任務
任務的調度(Job Scheduling)
當一個觸發器(Trigger)觸發時,Job 就會被調度執行,觸發器就是用來定義何時觸發的(也可以說是一個執行計劃),可以有以下任意的組合:
- 在一天中的某個時間(毫秒)
- 在一周中的某些日子
- 在一個月的某些日子
- 在一年中的某些日子
- 重覆特定次數
- 重覆直到特定的時間/日期
- 無限期重覆
- 以延遲間隔重覆
Job 由我們自己去命名,也可以組織到命名組(named groups)中。Trigger 也可以被命名並分組,以便在調度器(Scheduler)中更容易地組織它們。
Job 只需在 Scheduler 中添加一次,就可以有多個 Trigger 進行註冊。
任務的執行(Job Execution)
- 實現了 Job 介面的 Java 類就是 Job,習慣稱為任務類(Job class)。
- 當 Trigger 觸發時,Scheduler 就會通知 0 個或多個實現了 JobListener 和 TriggerListener 介面的 Java 對象。當然,這些 Java 對象在 Job 執行後也會被通知到。
- 當 Job 執行完畢時,會返回一個碼——
JobCompletionCode
,這個 JobCompletionCode 能夠表示 Job 執行成功還是失敗,我們就能通過這個 Code 來判斷後續該做什麼操作,比如重新執行這個 Job。
任務的持久化(Job Persistence)
- Quartz 的設計包括了一個 JobStore 介面,該介面可以為存儲 Job 提供各種機制。
- 通過 JDBCJobStore,可以將 Job 和 Trigger 持久化到關係型資料庫中。
- 通過 RAMJobStore,可以將 Job 和 Trigger 存儲到記憶體中(優點就是無須資料庫,缺點就是這不是持久化的)。
事務
- Quartz 可以通過使用 JobStoreCMT(JDBCJobStore的一個子類)參與 JTA 事務。
- Quartz 可以圍繞任務的執行來管理 JTA 事務(開始並且提交它們),以便任務執行的工作自動發生在 JTA 事務中。
集群
- 故障轉移
- 負載均衡
- Quartz 的內置集群功能依賴於 JDBCJobStore 實現的資料庫持久性。
- Quartz 的 Terracotta 擴展提供了集群功能,而無需備份資料庫。
監聽器和插件
- 應用程式可以通過實現一個或多個監聽器介面來捕獲調度事件以監聽或控制 Job / Trigger 的行為。
- 插件機制,我們可向 Quartz 添加功能,例如保存 Job 執行的歷史記錄,或從文件載入 Job 和 Trigger 的定義。
- Quartz 提供了許多插件和監聽器。
初體驗
引入 Quartz 依賴項
創建一個 Spring Boot 項目,然後引入如下依賴,就可以體驗 Quartz 了。
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
示例
現在知道 Quartz 有這麼幾個概念,分別是 Job
、Trigger
、Scheduler
。在它的設計實現上,分別是 Job 介面、JobDetail 介面、Trigger 介面、Scheduler 介面。除了 Job 介面的實現類需要我們自己去實現,剩下的都由 Quartz 實現了。
Quartz中的調度器(Scheduler)的主要作用就是調度 Job 和 Trigger 的執行。在Quartz中,Job代表需要執行的任務,Trigger代表觸發Job執行的條件和規則。調度器會根據Trigger的配置來確定Job的執行時機。
下麵的代碼包含了一個 Scheduler 的實例對象,接著是調用 start
方法,最後調用 shutdown
方法。
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class QuartzTest {
public static void main(String[] args) {
try {
// 從 Factory 中獲取 Scheduler 實例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 開始並關閉
scheduler.start();
scheduler.shutdown();
} catch (SchedulerException se) {
se.printStackTrace();
}
}
}
一旦我們使用 StdSchedulerFactory.getDefaultScheduler()
獲取 Scheduler 對象後,那麼程式就會一直運行下去,不會終止,直到我們調用了 scheduler.shutdown()
方法才會停止運行。這是因為獲取 Scheduler 對象後,就有許多線程在運行著,所以程式會一直運行下去。
與此同時,控制台會輸出相應的日誌:
10:14:02.442 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
10:14:02.445 [main] INFO org.quartz.simpl.SimpleThreadPool - Job execution threads will use class loader of thread: main
10:14:02.452 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
10:14:02.452 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
10:14:02.453 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized.
10:14:02.453 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
NOT STARTED.
Currently in standby mode.
Number of jobs executed: 0
Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.
10:14:02.453 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
10:14:02.453 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
從日誌中也能看出 Quartz 的一些信息,比如版本、使用的線程池、使用的任務存儲機制(這裡預設是 RAMJobStore)等等信息。
我們想要執行任務的話,就需要把任務的代碼放在 scheduler.start()
和 scheduler.shutdown()
之間。
QuartzTest:
import cn.god23bin.demo.quartz.job.HelloJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
// 這裡導入了 static,下麵才能直接 newJob, newTrigger
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;
public class QuartzTest {
public static void main(String[] args) {
try {
// 從 Factory 中獲取 Scheduler 實例
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 開始並關閉
scheduler.start();
// 定義一個 Job(用JobDetail描述的Job),並將這個 Job 綁定到我們寫的 HelloJob 這個任務類上
JobDetail job = newJob(HelloJob.class)
.withIdentity("job1", "group1") // 名字為 job1,組為 group1
.build();
// 現在觸發任務,讓任務執行,然後每5秒重覆執行一次
Trigger trigger = newTrigger()
.withIdentity("trigger1", "group1")
.startNow()
.withSchedule(simpleSchedule()
.withIntervalInSeconds(5)
.repeatForever())
.build();
// 告知 Quartz 使用我們的 Trigger 去調度這個 Job
scheduler.scheduleJob(job, trigger);
// 為了在 shutdown 之前讓 Job 有足夠的時間被調度執行,所以這裡當前線程睡眠30秒
Thread.sleep(30000);
scheduler.shutdown();
} catch (SchedulerException | InterruptedException se) {
se.printStackTrace();
}
}
}
HelloJob:實現 Job 介面,重寫 execute
方法,實現我們自己的任務邏輯。
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.text.SimpleDateFormat;
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("Hello Job!!! 時間:" + sdf.format(jobExecutionContext.getFireTime()));
}
}
運行程式,輸出如下信息:
10:25:40.069 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.job1', class=cn.god23bin.demo.quartz.job.HelloJob
10:25:40.071 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
10:25:40.071 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.job1
Hello Job!!! 時間:2023-03-28 10:25:40
10:25:45.066 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.job1', class=cn.god23bin.demo.quartz.job.HelloJob
10:25:45.066 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 1 triggers
10:25:45.066 [DefaultQuartzScheduler_Worker-2] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.job1
Hello Job!!! 時間:2023-03-28 10:25:45
# 省略後面輸出的信息,都是一樣的
API 有哪些?
Quartz API 的關鍵介面如下:
Scheduler
:最主要的 API,可以使我們與調度器進行交互,簡單說就是讓調度器做事。Job
:一個 Job 組件,你自定義的一個要執行的任務類就可以實現這個介面,實現這個介面的類的對象就可以被調度器進行調度執行。JobDetail
:Job
的詳情,或者說是定義了一個 Job。JobBuilder
: 用來構建JobDetail
實例的,然後這些實例又定義了 Job 實例。Trigger
: 觸發器,定義Job
的執行計劃的組件。TriggerBuilder
: 用來構建Trigger
實例。
Quartz 涉及到的設計模式:
-
Factory Pattern:
// 從 Factory 中獲取 Scheduler 實例 Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
-
Builder Pattern:
JobDetail job = newJob(HelloJob.class) .withIdentity("job1", "group1") // 名字為 job1,組為 group1 .build();
這裡的
newJob
方法是 JobBuilder 類中的一個靜態方法,就是通過這個來構建 JobDetail 的。/** * Create a JobBuilder with which to define a <code>JobDetail</code>, * and set the class name of the <code>Job</code> to be executed. * * @return a new JobBuilder */ public static JobBuilder newJob(Class <? extends Job> jobClass) { JobBuilder b = new JobBuilder(); b.ofType(jobClass); return b; } /** * Produce the <code>JobDetail</code> instance defined by this * <code>JobBuilder</code>. * * @return the defined JobDetail. */ public JobDetail build() { JobDetailImpl job = new JobDetailImpl(); job.setJobClass(jobClass); job.setDescription(description); if(key == null) key = new JobKey(Key.createUniqueName(null), null); job.setKey(key); job.setDurability(durability); job.setRequestsRecovery(shouldRecover); if(!jobDataMap.isEmpty()) job.setJobDataMap(jobDataMap); return job; }
同樣,構建 Trigger 對象是使用 TriggerBuilder 類以及 SimpleScheduleBuilder 類構建的,Schedule 主要是一個時間安排表,就是定義何時執行任務的時間表。
-
當然,除了上面說的兩種設計模式外,還有其他的設計模式,這裡就不細說了。比如單例模式,觀察者模式。
簡單理解 Job、Trigger、Scheduler
每天中午12點唱、跳、Rap、籃球
- Job:唱、跳、Rap、籃球
- Trigger:每天中午12點為一個觸發點
- Scheduler:自己,我自己調度 Trigger 和 Job,讓自己每天中午12點唱、跳、Rap、籃球
關於 Job
Job 介面源碼:
package org.quartz;
public interface Job {
void execute(JobExecutionContext context) throws JobExecutionException;
}
當該任務的 Trigger 觸發時,那麼 Job 介面的 execute
方法就會被 Scheduler 的某一個工作線程調用。JobExecutionContext 對象就會作為參數傳入這個方法,該對象就提供 Job 實例的一些關於任務運行時的信息。
我們知道,寫完一個 Job 類後,需要將定義一個 JobDetail 綁定到我們的 Job 類:
// 定義一個 Job(用JobDetail描述的Job),並將這個 Job 綁定到我們寫的 HelloJob 這個任務類上
JobDetail job = newJob(HelloJob.class)
.withIdentity("job1", "group1") // 名字為 job1,組為 group1
.build();
在這個過程中,有許多屬性是可以設置的,比如 JobDataMap,這個對象能夠存儲一些任務的狀態信息數據,這個後面說。
Trigger 對象用於觸發任務的執行。當我們想要調度某個任務時,可以實例化 Trigger 並設置一些我們想要的屬性。Trigger 也可以有一個與之相關的 JobDataMap,這對於特定的觸發器觸發時,傳遞一些參數給任務是很有用。Quartz 有幾種不同的 Trigger 類型,但最常用的類型是 SimpleTrigger 和 CronTrigger。
關於 SimpleTrigger 和 CronTrigger
如果我們想要在某個時間點執行一次某個任務,或者想要在給定時間啟動一個任務,並讓它重覆 N 次,執行之間的延遲為 T,那麼就可以使用 SimpleTrigger。
如果我們想根據類似日曆的時間表來執行某個任務,例如每天晚上凌晨 4 點這種,那麼就可以使用 CronTrigger。
為什麼會設計出 Job 和 Trigger 這兩個概念?
在官網上是這樣說的:
Why Jobs AND Triggers? Many job schedulers do not have separate notions of jobs and triggers. Some define a ‘job’ as simply an execution time (or schedule) along with some small job identifier. Others are much like the union of Quartz’s job and trigger objects. While developing Quartz, we decided that it made sense to create a separation between the schedule and the work to be performed on that schedule. This has (in our opinion) many benefits.
For example, Jobs can be created and stored in the job scheduler independent of a trigger, and many triggers can be associated with the same job. Another benefit of this loose-coupling is the ability to configure jobs that remain in the scheduler after their associated triggers have expired, so that that it can be rescheduled later, without having to re-define it. It also allows you to modify or replace a trigger without having to re-define its associated job.
簡而言之,這有許多好處:
- 任務可以獨立於觸發器,它可以在調度器中創建和存儲,並且許多觸發器可以與同一個任務關聯。
- 這種松耦合能夠配置任務,在其關聯的觸發器已過期後仍然保留在調度器中,以便之後重新安排,而無需重新定義它。
- 這也允許我們修改或替換觸發器的時候無需重新定義其關聯的任務。
Job 和 Trigger 的身份標識(Identities)
在上面的代碼中我們也看到了,Job 和 Trigger 都有一個 withIdentity
方法。
JobBuilder 中的 withIdentity
方法:
private JobKey key;
public JobBuilder withIdentity(String name, String group) {
key = new JobKey(name, group);
return this;
}
TriggerBuilder 中的 withIdentity
方法:
private TriggerKey key;
public TriggerBuilder<T> withIdentity(String name, String group) {
key = new TriggerKey(name, group);
return this;
}
當 Job 和 Trigger 註冊到 Scheduler 中時,就會通過這個 key 來標識 Job 和 Trigger。
任務和觸發器的 key(JobKey 和 TriggerKey)允許將它們放入「組」中,這有助於將任務和觸發器進行分組,或者說分類。而且任務或觸發器的 key 的名稱在組中必須是唯一的,他們完整的 key(或標識符)是名稱和組的組合。從上面的代碼中也可以看到,構造方法都是兩個參數的,第一個參數是 name,第二個參數是 group,構造出來的就是整個 key 了。
關於 JobDetail 和 JobDataMap
我們通過寫一個 Job 介面的實現類來編寫我們等待執行的任務,而 Quartz 需要知道你將哪些屬性給了 Job。那 Quartz 是如何知道的呢?Quartz 就是通過 JobDetail 知道的。
註意,我們向 Scheduler 提供了一個 JobDetail 實例, Scheduler 就能知道要執行的是什麼任務,只需在構建 JobDetail 時提供任務的類即可(即 newJob(HelloJob.class)
)。
每次調度程式執行任務時,在調用其 execute
方法之前,它都會創建該任務類的一個新實例。執行完成任務後,對任務類實例的引用將被丟棄,然後該實例將被垃圾回收。
那我們如何為作業實例提供屬性或者配置?我們如何在執行的過程中追蹤任務的狀態?這兩個問題的答案是一樣的:關鍵是 JobDataMap,它是 JobDetail 對象的一部分。
JobDataMap 可以用來保存任意數量的(可序列化的)數據對象,這些對象在任務實例執行時需要使用的。JobDataMap 是 Java 中 Map 介面的一個實現,具有一些用於存儲和檢索原始類型數據的方法。
示例:
PlayGameJob:
public class PlayGameJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobKey key = context.getJobDetail().getKey();
// 獲取JobDataMap,該Map在創建JobDetail的時候設置的
JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
String gameName = jobDataMap.getString("gameName");
float gamePrice = jobDataMap.getFloat("gamePrice");
System.out.println("我玩的" + gameName + "才花費了我" + gamePrice + "塊錢");
}
}
接著使用 usingJobData
設置該任務需要的數據,最後調度該任務:
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
JobDetail job = newJob(PlayGameJob.class)
.withIdentity("myJob", "group1")
.usingJobData("gameName", "GTA5")
.usingJobData("gamePrice", 55.5f)
.build();
Trigger trigger = newTrigger()
.withIdentity("myJob", "group1")
.build();
scheduler.scheduleJob(job, trigger);
Thread.sleep(10000);
scheduler.shutdown();
控制台輸出:
14:18:43.295 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.simpl.PropertySettingJobFactory - Producing instance of Job 'group1.myJob', class=cn.god23bin.demo.quartz.job.PlayGameJob
14:18:43.299 [DefaultQuartzScheduler_QuartzSchedulerThread] DEBUG org.quartz.core.QuartzSchedulerThread - batch acquisition of 0 triggers
14:18:43.300 [DefaultQuartzScheduler_Worker-1] DEBUG org.quartz.core.JobRunShell - Calling execute on job group1.myJob
我玩的GTA5才花費了我55.5塊錢
當然,也可以這樣寫:
JobDataMap jobDataMap = new JobDataMap();
jobDataMap.put("gameName", "GTA5");
jobDataMap.put("gamePrice", 55.5f);
JobDetail job = newJob(PlayGameJob.class)
.withIdentity("myJob", "group1")
.usingJobData(jobDataMap)
.build();
之前還說過,Trigger 也是可以有 JobDataMap 的。當你有這種情況,就是在調度器中已經有一個 Job 了,但是想讓不同的 Trigger 去觸發執行這個 Job,每個不同的 Trigger 觸發時,你想要有不同的數據傳入這個 Job,那麼就可以用到 Trigger 攜帶的 JobDataMap 了。
噢對了!對於我們上面自己寫的 PlayGameJob ,還可以換一種寫法,不需要使用通過 context.getJobDetail().getJobDataMap()
獲取 JobDataMap 對象後再根據 key 獲取對應的數據,直接在這個任務類上寫上我們需要的屬性,提供 getter 和 setter 方法,這樣 Quartz 會幫我們把數據賦值到該對象的屬性上。
PlayGameJob:
// 使用Lombok的註解,幫我們生成 getter 和setter 方法以及無參的構造方法
@Data
@NoArgsConstructor
public class PlayGameJob implements Job {
// Quartz 會把數據註入到任務類定義的屬性上,直接用就可以了
private String gameName;
private float gamePrice;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
JobKey key = context.getJobDetail().getKey();
System.out.println("我玩的" + gameName + "才花費了我" + gamePrice + "塊錢");
}
}
這樣的效果,就是減少了 execute
方法中的代碼量。
如何理解 Job 實例?
這個確實會有一些困惑,比如一開始說的 Job 介面,還有 JobDetail 介面,而且為什麼會說成 JobDetail 對象是 Job 的實例?是吧。
想要理解,舉個例子:
現在我們寫了一個發送消息的 Job 實現類——SendMessageJob。
接著我們創建了多個 JobDetail 對象,這些對象都有不同的定義,比如有叫做 SendMessageToLeBron 的 JobDetail、有 SendMessageToKobe 的 JobDetail,這兩個 JobDetail 都有它各自的 JobDataMap 傳遞給我們的 Job 實現類。
當 Trigger 觸發時,Scheduler 將載入與其關聯的 JobDetail(任務定義),並通過 Scheduler上配置的 JobFactory 實例化它所引用的任務類(SendMessageJob)。預設的 JobFactory 只是在任務類上調用newInstance() ,然後嘗試在與 JobDataMap 中鍵的名稱匹配的類中的屬性名,進而調用 setter 方法將 JobDataMap 中的值賦值給對應的屬性。
在 Quartz 的術語中,我們將每個 JobDetail 對象稱為「Job 定義或者 JobDetail 實例」,將每個正在執行的任務稱為「Job 實例或 Job 定義的實例」。
一般情況下,如果我們只使用「任務」這個詞,我們指的是一個名義上的任務,簡而言之就是我們要做的事情,也可以指 JobDetail。當我們提到實現 Job 介面的類時,我們通常使用術語「任務類」。
兩個需要知道的註解
JobDataMap 可以說是任務狀態的數據,這裡的數據和併發也有點關係。Quartz 提供了幾個註解,這幾個註解會影響到 Quartz 在這方面的動作。
@DisallowConcurrentExecution 註解是用在任務類上的。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface DisallowConcurrentExecution {
}
DisallowConcurrentExecution
這個註解的作用就是告知 Quartz 這個任務定義的實例(JobDetail 實例)不能併發執行,舉個例子,就上面的 SendMessageToLeBron 的 JobDetail 實例,是不能併發執行的,但它是可以與 SendMessageToKobe 的 JobDetail 的實例同時執行。需要註意的是它指的不是任務類的實例(Job 實例)。
@PersistJobDataAfterExecution 註解也是用在任務類上的。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PersistJobDataAfterExecution {
}
@PersistJobDataAfterExecution
這個註解的作用是告知 Quartz 在 execute
方法成功完成後更新 JobDetail 的 JobDataMap 的存儲副本(沒有引發異常),以便同一任務的下一次執行能接收更新後的值,而不是最初存儲的值。
與 @DisallowConcurrentExecution
註解一樣,這是適用於任務定義實例(JobDetail 實例),而不是任務類實例(Job 實例)。
關於 Trigger
我們需要瞭解 Trigger 有哪些屬性可以去設置,從最開始的初體驗中,我們給 Trigger 設置了一個 TriggerKey 用來標識這個 Trigger 實例,實際上,它還有好幾個屬性給我們設置。
共用的屬性
上面也說過 Trigger 有不同的類型(比如 SimpleTrigger 和 CronTrigger),不過,即使是不同的類型,也有相同的屬性。
- jobKey:作為 Trigger 觸發時應執行的任務的標識。
- startTime:記錄下首次觸發的時間;對於某些觸發器,它是指定觸發器應該在何時觸發。
- endTime:觸發器不再生效的時間
- ...
還有更多,下麵說一些重要的。
priority
優先順序,這個屬性可以設置 Trigger 觸發的優先順序,值越大則優先順序越高,就優先被觸發執行任務。當然這個是在同一時間調度下才會有這個優先順序比較的,如果你有一個 A 任務在 6 點觸發,有一個 B 任務在 7 點觸發,即使你的 B 任務的優先順序比 A 任務的高,也沒用,6 點 的 A 任務總是會比 7點 的 B 任務先觸發。
misfireInstruction
misfire instruction,錯失觸髮指令,也就是說當某些情況下,導致觸發器沒有觸發,那麼就會執行這個指令,預設是一個「智能策略」的指令,它能夠根據不同的 Trigger 類型執行不同的行為。
當 Scheduler 啟動的時候,它就會先搜尋有沒有錯過觸發的 Trigger,有的話就會基於 Trigger 配置的錯失觸髮指令來更新 Trigger 的信息。
calendar
Quartz 中也有一個 Calendar 對象,和 Java 自帶的不是同一個。
在設置 Trigger 的時候,如果我們想排除某些日期時間,那麼就可以使用這個 Calendar 對象。
SimpleTrigger
如果我們想在特定的時間點執行一次任務,或者在特定的時刻執行一次,接著定時執行,那麼 SimpleTrigger 就能滿足我們的需求。
SimpleTrigger 包含了這麼幾個屬性:
- startTime:開始時間
- endTime:結束時間
- repeatCount:重覆次數,可以是 0,正整數,或者是一個常量
SimpleTrigger.REPEAT_INDEFINITELY
- repeatInterval:重覆的時間間隔,必須是 0,或者是一個正的長整型的值(long 類型的值),表示毫秒,即多少毫秒後重覆觸發
SimpleTrigger 的實例對象可以由 TriggerBuilder 和 SimpleScheduleBuilder 來創建。
示例
下麵舉幾個例子:
- 構建一個給定時刻觸發任務的 Trigger,不會重覆觸發:
// 今天22點30分0秒
Date startAt = DateBuilder.dateOf(22, 30, 0);
// 通過強轉構建一個 SimpleTrigger
SimpleTrigger trigger = (SimpleTrigger) newTrigger()
.withIdentity("trigger1", "group1")
.startAt(startAt) // 開始的日期時間
.forJob("job1", "group1") // 通過 job 的 name 和 group 識別 job
.build();
- 構建一個給定時刻觸發任務的 Trigger,每十秒重覆觸發十次:
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.startAt(startAt) // 如果沒有給定開始時間,那麼就預設現在開始觸發
.withSchedule(SimpleScheduleBuilder.simpleSchedule() // 通過 simpleSchedule 方法構建 SimpleTrigger
.withIntervalInSeconds(10)
.withRepeatCount(10)) // 每隔10秒重覆觸發10次
.forJob(job) // 通過 JobDetail 本身來識別 Job
.build();
- 構建一個給定時刻觸發任務的 Trigger,在未來五分鐘內觸發一次:
Date futureDate = DateBuilder.futureDate(5, DateBuilder.IntervalUnit.MINUTE);
JobKey jobKey = job.getKey();
trigger = (SimpleTrigger) newTrigger()
.withIdentity("trigger5", "group1")
.startAt(futureDate) // 使用 DateBuilder 創建一個未來的時間
.forJob(jobKey) // 通過 jobKey 識別 job
.build();
- 構建一個給定時刻觸發任務的 Trigger,然後每五分鐘重覆一次,直到晚上 22 點:
trigger = newTrigger()
.withIdentity("trigger7", "group1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(5)
.repeatForever())
.endAt(DateBuilder.dateOf(22, 0, 0))
.build();
- 構建一個給定時刻觸發任務的 Trigger,然後每下一個小時整點觸發,然後每2小時重覆一次,一直重覆下去:
trigger = newTrigger()
.withIdentity("trigger8") // 這裡沒有指定 group 的話,那麼 "trigger8" 就會在預設的 group 中
.startAt(DateBuilder.evenHourDate(null)) // 下一個整點時刻 (分秒為零 ("00:00"))
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInHours(2)
.repeatForever())
.forJob("job1", "group1")
.build();
錯失觸髮指令
比如我在要觸發任務的時候,機器宕機了,當機器重新跑起來後怎麼辦呢?
當 Trigger 錯失觸發時間去觸發任務時,那麼 Quartz 就需要執行 Misfire Instruction
,SimpleTrigger 有如下的以常量形式存在的 Misfire 指令:
- MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
- MISFIRE_INSTRUCTION_FIRE_NOW
- MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
- MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
- MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
- MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT
我們知道,所有的 Trigger,SimpleTrigger 也好,CronTrigger 也好,不管是什麼類型,都有一個 Trigger.MISFIRE_INSTRUCTION_SMART_POLICY
可以使用,如果我們使用這個指令,那麼 SimpleTrigger 就會動態地在上面 6 個指令中選擇,選擇的行為取決於我們對於 SimpleTrigger 的設置。
當我們在構建 Trigger 的時候,就可以給 Trigger 設置上 Misfire 指令:
trigger = newTrigger()
.withIdentity("trigger7", "group1")
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(5)
.repeatForever()
.withMisfireHandlingInstructionNextWithExistingCount())
.build();
CronTrigger
使用 CronTrigger,我們可以指定觸發任務的時間安排(schedule),例如,每周五中午,或 每個工作日和上午9:30, 甚至 每周一,周三上午9:00到上午10:00之間每隔5分鐘 和 1月的星期五 。
CronTrigger 也有一個 startTime,用於指定計劃何時生效,以及一個(可選的)endTime,用於指定何時停止這個任務的執行。
cron 表達式
cron 表達式有 6 位,是必須的,從左到右分別表示:秒、分、時、日、月、周
。
當然也可以是 7 位,最後一位就是年(可選項):秒、分、時、日、月、周、年
。
取值說明:正常認識,秒分都是 0 - 59,時則是 0 - 23,日則是 1 - 31,月在這邊則是 0-11,周則是 1 - 7(這裡的1指的是星期日)。年則只有 1970 - 2099
月份可以指定為0到11之間的值,或者使用字元串 JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV 和 DEC
星期幾可以使用字元串 SUN,MON,TUE,WED,THU,FRI 和 SAT 來表示
詳細可參考這裡:簡書-Cron表達式的詳細用法
Cron 生成工具:cron.qqe2.com/
示例
- 構建一個 Trigger,每天上午8點到下午5點之間每隔一分鐘觸發一次:
Trigger trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(cronSchedule("0 0/1 8-17 * * ?"))
.forJob("myJob", "group1")
.build();
- 構建一個 Trigger,每天上午10:42觸發:
JobKey myJobKey = job.getKey();
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(10, 42))
.forJob(myJobKey)
.build();
- 構建一個觸發器,該觸發器將在星期三上午10點42分在TimeZone中觸發,而不是系統的預設值:
JobKey myJobKey = job.getKey();
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(CronScheduleBuilder
.weeklyOnDayAndHourAndMinute(DateBuilder.WEDNESDAY, 10, 42)
.inTimeZone(TimeZone.getTimeZone("America/Los_Angeles")))
.forJob(myJobKey)
.build();
錯失觸髮指令
對於 CronTrigger,它有 3 個 Misfire 指令
- MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY
- MISFIRE_INSTRUCTION_DO_NOTHING
- MISFIRE_INSTRUCTION_FIRE_NOW
我們在構建 Tirgger 的時候就可以給這個 Trigger 指定它的 Misfire 指令:
trigger = newTrigger()
.withIdentity("trigger3", "group1")
.withSchedule(cronSchedule("0 0/2 8-17 * * ?")
.withMisfireHandlingInstructionFireAndProceed())
.forJob("myJob", "group1")
.build();
關於CRUD
存儲定時任務
存儲定時任務,方便後續使用,通過 Scheduler 的 addJob
方法
void addJob(JobDetail jobDetail, boolean replace) throws SchedulerException;
該方法會添加一個沒有與 Trigger 關聯的 Job 到 Scheduler 中,然後這個 Job 是處於休眠的狀態直到它被 Trigger 觸發進行執行,或者使用 Scheduler.triggerJob()
指定了這個 Job,這個 Job 才會被喚醒。
JobDetail job1 = newJob(MyJobClass.class)
.withIdentity("job1", "group1")
.storeDurably() // Job 必須被定義為 durable 的
.build();
scheduler.addJob(job1, false);
更新已存儲的定時任務
addJob
方法的第二個參數-replace,就是用在這裡,設置為 true,那麼就是更新操作。
JobDetail job1 = newJob(MyJobClass.class)
.withIdentity("job1", "group1")
.build();
// store, and set overwrite flag to 'true'
scheduler.addJob(job1, true);
更新觸發器
替換已存在的 Trigger:
// 定義一個新的 Trigger
Trigger trigger = newTrigger()
.withIdentity("newTrigger", "group1")
.startNow()
.build();
// 讓 Scheduler 根據 Key 去移除舊的 Trigger, 然後將新的 Trigger 放上去
scheduler.rescheduleJob(new TriggerKey("oldTrigger", "group1"), trigger);
更新已存在的 Trigger:
// 根據 Key 檢索已存在的 Trigger
Trigger oldTrigger = scheduler.getTrigger(new TriggerKey("oldTrigger", "group1");
// 獲取 TriggerBuilder
TriggerBuilder tb = oldTrigger.getTriggerBuilder();
// 更新觸發動作,並構建新的 Trigger
// (other builder methods could be called, to change the trigger in any desired way)
Trigger newTrigger = tb.withSchedule(simpleSchedule()
.withIntervalInSeconds(10)
.withRepeatCount(10)
.build();
// 重新用新的 Trigger 調度 Job
scheduler.rescheduleJob(oldTrigger.getKey(), newTrigger);
取消定時任務
使用 Scheduler 的 deleteJob
方法,入參為一個 TriggerKey,即 Trigger 標識,這樣就能取消特定的 Trigger 去觸發對應的任務,因為一個 Job 可能有多個 Trigger。
scheduler.unscheduleJob(new TriggerKey("trigger1", "group1"));
使用 Scheduler 的 deleteJob
方法,入參為一個 JobKey,即 Job 標識,這樣就能刪除這個 Job 並取消對應的 Trigger 進行觸發。
scheduler.deleteJob(new JobKey("job1", "group1"));
獲取調度器中的所有定時任務
思路:通過 scheduler 獲取任務組,然後遍歷任務組,進而遍歷組中的任務。
// 遍歷每一個任務組
for(String group: scheduler.getJobGroupNames()) {
// 遍歷組中的每一個任務
for(JobKey jobKey : scheduler.getJobKeys(GroupMatcher.groupEquals(group))) {
System.out.println("通過標識找到了 Job,標識的 Key 為: " + jobKey);
}
}
獲取調度器中的所有觸發器
思路:同上。
// 遍歷每一個觸發器組
for(String group: scheduler.getTriggerGroupNames()) {
// 遍歷組中的每一個觸發器
for(TriggerKey triggerKey : scheduler.getTriggerKeys(GroupMatcher.groupEquals(group))) {
System.out.println("通過標識找到了 Trigger,標識的 Key 為: " + triggerKey);
}
}
獲取某一個定時任務的觸發器列表
因為一個任務可以有多個觸發器,所以是獲取觸發器列表。
List<Trigger> jobTriggers = scheduler.getTriggersOfJob(new JobKey("jobName", "jobGroup"));
總結
想要使用 Quartz,那麼就引入它的依賴。
從使用上來說:
- 對於一個任務,我們可以寫一個任務類,即實現了 Job 介面的 Java 類,並重寫
execute
方法。接著需要一個 JobDetail 來描述這個 Job,或者說把這個 Job 綁定到這個 JobDetail 上。然後我們就需要一個 Trigger,這個 Trigger 是用來表示何使觸發任務的,可以說是一個執行計劃,在何時如何觸發,Trigger 是有好幾種類型的,目前常用的就是 SimpleTrigger 和 CronTrigger。最後,在把 JobDetail 和 Trigger 扔給 Scheduler,讓它去組織調度; - 對於一個觸發器,它有對應的類型,以及對應的 Misfire 指令,一般在創建 Trigger 的時候,就指定上這些信息;
- 對於它們的 CRUD,都是使用調度器進行操作的,比如往調度器中添加任務,更新任務。
從 Quartz 的設計上來說,它有涉及到多種設計模式,包括 Builder 模式,Factory 模式等等。
以上,便是本篇文章的內容,我們下期再見!
最後的最後
希望各位屏幕前的靚仔靚女們
給個三連!你輕輕地點了個贊,那將在我的心裡世界增添一顆明亮而耀眼的星!
咱們下期再見!