本文正在参加「金石方案」

前语

还不会 Quartz?假如你还没有触摸过Quartz,那么你或许失去了一个很棒的使命调度结构!Quartz 供给了一种灵活、牢靠的方式来办理和履行守时使命,让我们的守时使命更加优雅。本篇文章将为你介绍 Quartz 结构的中心概念、API 和实战技巧,让你轻松上手。也不用担心,作为过来人,我会把难明的概念和术语解释清楚,让你看完本篇文章后,就知道该怎么操作 Quartz。当然,本篇文章不免有不足之处,在此欢迎我们指出。那废话少说,下面我们开端吧!

什么是 Quartz?

Quartz:github.com/quartz-sche…

官网: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 完成了。

一文快速入门任务调度框架-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 组件,你自界说的一个要履行的使命类就能够完成这个接口,完成这个接口的类的目标就能够被调度器进行调度履行。
  • 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 形式等等。

以上,就是本篇文章的内容,我们下期再会!

参考:www.quartz-scheduler.org/

最终的最终

期望各位屏幕前的靓仔靓女们给个三连!你轻轻地点了个赞,那将在我的心里世界增添一颗亮堂而耀眼的星!

我们下期再会!