# Quartz教程 - 5 触发器
触发器(Triggers)用于定义何时和如何执行与 Job 关联的任务。它们决定了任务的调度规则,比如何时执行、执行频率以及在何种条件下触发执行。
我们在前面使用的触发器是SimpleTrigger, SimpleTrigger 只能定义按照指定频率执行的任务,如果要定义复杂的执行规则,SimpleTrigger 是无法支持的,一般我们用的最多的就是 CronTrigger 。
# 5.1 CronTrigger
CronTrigger 是 Quartz 中用于基于 Cron 表达式定义任务执行时间表的触发器类型。它允许用户按照日历时间来定义任务的执行规则,非常灵活且支持各种复杂的调度需求。
# 1 CronTrigger演示
先举个栗子:
任务还是之前的任务类:
MyJob.java
package com.doubibiji.job;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MyJob implements Job {
    private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("执行任务:" + sdf.format(new Date()));
    }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
编写测试类,代码如下:
package com.doubibiji;
import com.doubibiji.job.MyJob;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.concurrent.TimeUnit;
public class TestClass {
    public static void main(String[] args) {
        // 1.定义jobDetail
        JobDetail job = JobBuilder.newJob(MyJob.class)
                .withIdentity("job1", "jGroup1")
                .usingJobData("count", 0)           // 通过key-value形式设置参数
                .build();
        // 2.创建一个 Cron 触发器
        CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                .withIdentity("cronTrigger1", "cGroup1")
                .startAt(DateBuilder.futureDate(10, DateBuilder.IntervalUnit.SECOND)) // 延迟10秒执行
                .withSchedule(CronScheduleBuilder.cronSchedule("*/2 * * * * ?"))
                .build();
        try {
            // 3.定义调度器
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
            // 设置任务和触发器,由触发器进行分派任务
            scheduler.scheduleJob(job, cronTrigger);
            scheduler.start();
            // 让主线程延迟执行,scheduler如果shutdown,则所有的定时任务会取消。
            TimeUnit.SECONDS.sleep(20);
            scheduler.shutdown();
        } catch (SchedulerException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
上面的代码和之前的 HelloWorld 基本是一样的,只是使用了不同的触发器。
上面定义了 CronTrigger 触发器,使用 Cron 表达式 */2 * * * * ? 来定义执行规则,并时候用 startAt 推迟了10秒执行。
这个 Cron 表达式看上去有点懵逼,如果不会写表达式,可以百度一下 Cron 表达式在线工具,有很多网站可以编写 Cron 表达式。
下面简单介绍一下 Cron 表达式 。
# 2 Cron 表达式基础
Cron 表达式由 7 个字段组成,用空格分隔:
秒 分 时 日 月 周 年(可选)
- 秒(Seconds): 0-59 或者使用特殊字符(*、-、,、/)表示范围和间隔。
- 分(Minutes): 0-59 或者使用特殊字符(*、-、,、/)表示范围和间隔。
- 小时(Hours): 0-23 或者使用特殊字符(*、-、,、/)表示范围和间隔。
- 日期(Day of month): 1-31 或者使用特殊字符(*、-、,、/、?、L、W)表示范围和间隔。
- 月份(Month): 1-12 或者使用特殊字符(*、-、,、/)表示范围和间隔。
- 星期(Day of week): 1-7(1 表示周日)或者使用特殊字符(*、-、,、/、?、L、#)表示范围和间隔。
- 年(Year): 可选字段,可以指定特定的年份,通常不填。
特殊字符的含义:
特殊字符的含义:
- 星号 ( - *): 代表“所有值”。当在一个字段中使用星号时,表示该字段的所有可能值。例如,- *在分钟字段中表示每分钟都执行。
- 连字符 ( - -): 用于定义范围。指定一个范围的数值,例如在小时字段中,- 9-17表示从9点到17点的时间范围。
- 逗号 ( - ,): 用于列举多个值。在一个字段中可以指定多个数值,例如在星期字段中,- MON,WED,FRI表示星期一、星期三和星期五。
- 斜线 ( - /): 用于定义间隔。指定一个范围内的间隔值,例如在分钟字段中,- */15表示每隔15分钟执行一次。
- 问号 ( - ?): 在日和星期字段中,问号用于指定未指定值。通常在某一个字段中指定值,但对另一个字段不关心时使用。例如,- * * ? * MON-FRI表示在工作日每分钟都执行。
- L( - L): 在日期和星期字段中,表示“最后”。例如,在日期字段中,- L表示月份的最后一天;在星期字段中,- L表示周六。
- W( - W): 在日期字段中,表示工作日(周一至周五)最接近指定的日期。例如,- 15W表示最接近每月15号的工作日。
- 井号( - #): 在星期字段中,用于确定月份的第几个周几。例如,- 6#3表示每月的第三个周五。
现在来解释一下,例如说有如下表达式:
1  1  1  1  1  ?
秒 分 时 日 月 周
2
上面的每一位都对应了秒、分、时、日、月、周,周使用了 ? 表示没指定周,也就是不限制周,表达式中没有年,年是可以省略的。
所以上面的表达式就是1月1日1时1分1秒执行,也就是每年1月1日1时1分1秒的时候执行。
再看一个表达式:
1  1  *  *  *  ?
秒 分 时 日 月 周
2
上面的表达式只限制了秒和分,每个小时都执行,那么就是每个小时的1分1秒都执行一次。
再看一个表达式:
*/5  *  *  *  *  ?
秒   分 时 日 月 周
2
/ 定义的是间隔,*/5 表示没间隔 5 秒执行一次。
# 3 Cron 表达式示例
- 每天下午 1 点执行任务: - 0 0 13 * * ?1
- 每隔5分钟执行一次: - 0 */5 * * * ?1
- 周一到周五上午 9 点到下午 5 点,每隔半小时执行一次: - 0 */30 9-17 * * MON-FRI1
- 在每月的最后一天,上午10点执行任务: - 0 0 10 L * ?1
# 5.2 MisFire策略
在 Quartz 中,Misfire指的是触发器错过了预定的触发时间。
例如下面一些情况可能会导致任务错过执行:
- 当前没有空闲的线程池资源可用调度器暂停;
- 系统宕机;
- 使用了 @DisallowConcurrentExecution注解,要执行下一次任务了,但是上一次任务还没有执行完成;
- 指定了过期开始的执行时间,现在是08:00:00,指定开始执行时间为06:00:00;
对于 CronTrigger 是否构成 misfire,有两个条件:
- job 到达触发时间时没有被执行;
- job 延迟执行的时间超过了Quartz 配置的 misfireThreshold阈值。如果延迟执行的时间小于阈值,则 Quartz 不认为发生了misfire,会立即执行 job;如果延迟执行的时间大于或者等于阈值,则被判断为misfire,然后会按照指定的策略来执行。如果没有配置 Quartz 的misfireThreshold,默认配置为60秒。
Quartz 针对 CronTrigger 错过触发提供了不同的 Misfire 策略,确保作业能够尽可能地被执行。
Misfire策略由 withMisfireHandlingInstructionXXX()方法来设置,其中 XXX 可以是IgnoreMisfires()、DoNothing()、FireAndProceed(),下面讲解一下。
# 1 withMisfireHandlingInstructionDoNothing()
作用:所有错过的触发都被忽略,并按照原计划执行任务,也就是错过的就错过了,等待下一次执行时间到了再执行。
举个栗子:
Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("trigger1", "group1")
    .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?")
    .withMisfireHandlingInstructionDoNothing())
    .build();
2
3
4
5
# 2 withMisfireHandlingInstructionFireAndProceed()
作用:错过了很多次,但是只会立即执行一次,然后按照原计划执行任务。这个是默认的策略!
举个栗子:
Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("trigger1", "group1")
    .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?")
    .withMisfireHandlingInstructionFireAndProceed())
    .build();
2
3
4
5
# 3 withMisfireHandlingInstructionIgnoreMisfires()
作用:立即执行所有错过的触发,错过了100个,一下子把错过的100个全部执行了,然后按照原计划执行任务。
举个栗子:
Trigger trigger = TriggerBuilder.newTrigger()
    .withIdentity("trigger1", "group1")
    .withSchedule(CronScheduleBuilder.cronSchedule("0/5 * * * * ?")
    .withMisfireHandlingInstructionIgnoreMisfires())
    .build();
2
3
4
5
在实际使用中,根据您的需求选择合适的Misfire策略非常重要,这可以确保即使在意外情况下,任务也能够按照期望的方式执行。
因为现在还没有将 Quartz 的数据持久化到数据库,所以现在使用策略会发现没有效果,继续向后学习,在后面讲到将 Quartz 的数据持久化到数据库后,然后停掉项目,重新启动后,Quart 会重新启动运行,并使用不同的策略恢复任务,就可以看到效果了,例如配置为withMisfireHandlingInstructionIgnoreMisfires(),你会发现项目重新启动后,可能会出现立刻执行了很多次任务,因为这些任务是停掉项目后错过的。
