一文快速入門任務排程框架-Quartz

god23bin發表於2023-04-04

前言

還不會 Quartz?如果你還沒有接觸過Quartz,那麼你可能錯過了一個很棒的任務排程框架!Quartz 提供了一種靈活、可靠的方式來管理和執行定時任務,讓我們們的定時任務更加優雅。本篇文章將為你介紹 Quartz 框架的核心概念、API 和實戰技巧,讓你輕鬆上手。也不用擔心,作為過來人,我會把難懂的概念和術語解釋清楚,讓你看完本篇文章後,就知道該如何操作 Quartz。當然,本篇文章難免有不足之處,在此歡迎大家指出。那廢話少說,下面我們開始吧!

什麼是 Quartz?

Quartz:https://github.com/quartz-scheduler/quartz

官網:http://www.quartz-scheduler.org/

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 有這麼幾個概念,分別是 JobTriggerScheduler。在它的設計實現上,分別是 Job 介面、JobDetail 介面、Trigger 介面、Scheduler 介面。除了 Job 介面的實現類需要我們自己去實現,剩下的都由 Quartz 實現了。

Scheduler

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 元件,你自定義的一個要執行的任務類就可以實現這個介面,實現這個介面的類的物件就可以被排程器進行排程執行。
  • JobDetailJob 的詳情,或者說是定義了一個 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 型別,但最常用的型別是 SimpleTriggerCronTrigger

關於 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.

簡而言之,這有許多好處:

  1. 任務可以獨立於觸發器,它可以在排程器中建立和儲存,並且許多觸發器可以與同一個任務關聯。
  2. 這種松耦合能夠配置任務,在其關聯的觸發器已過期後仍然保留在排程器中,以便之後重新安排,而無需重新定義它。
  3. 這也允許我們修改或替換觸發器的時候無需重新定義其關聯的任務。

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 來建立。

示例

下面舉幾個例子:

  1. 構建一個給定時刻觸發任務的 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();
  1. 構建一個給定時刻觸發任務的 Trigger,每十秒重複觸發十次:
trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .startAt(startAt)  // 如果沒有給定開始時間,那麼就預設現在開始觸發
    .withSchedule(SimpleScheduleBuilder.simpleSchedule() // 透過 simpleSchedule 方法構建 SimpleTrigger
              .withIntervalInSeconds(10) 
              .withRepeatCount(10)) // 每隔10秒重複觸發10次
    .forJob(job) // 透過 JobDetail 本身來識別 Job
    .build();
  1. 構建一個給定時刻觸發任務的 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();
  1. 構建一個給定時刻觸發任務的 Trigger,然後每五分鐘重複一次,直到晚上 22 點:
trigger = newTrigger()
    .withIdentity("trigger7", "group1")
    .withSchedule(SimpleScheduleBuilder.simpleSchedule()
              .withIntervalInMinutes(5)
              .repeatForever())
    .endAt(DateBuilder.dateOf(22, 0, 0))
    .build();
  1. 構建一個給定時刻觸發任務的 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/

示例

  1. 構建一個 Trigger,每天上午8點到下午5點之間每隔一分鐘觸發一次:
Trigger trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(cronSchedule("0 0/1 8-17 * * ?"))
    .forJob("myJob", "group1")
    .build();
  1. 構建一個 Trigger,每天上午10:42觸發:
JobKey myJobKey = job.getKey();

trigger = newTrigger()
    .withIdentity("trigger3", "group1")
    .withSchedule(CronScheduleBuilder.dailyAtHourAndMinute(10, 42))
    .forJob(myJobKey)
    .build();
  1. 構建一個觸發器,該觸發器將在星期三上午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 模式等等。

以上,便是本篇文章的內容,我們下期再見!

參考:http://www.quartz-scheduler.org/

最後的最後

希望各位螢幕前的靚仔靚女們給個三連!你輕輕地點了個贊,那將在我的心裡世界增添一顆明亮而耀眼的星!

我們們下期再見!

相關文章