# 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-FRI
1在每月的最后一天,上午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(),你会发现项目重新启动后,可能会出现立刻执行了很多次任务,因为这些任务是停掉项目后错过的。