一、何为Quartz
Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,它可以与J2EE与J2SE应用程序相结合也可以单独使用。Quartz可以用来创建简单或为运行十个,百个,甚至是好几万个Jobs这样复杂的程序。Jobs可以做成标准的Java组件或 EJBs。
二、使用Quartz
2.1 依赖导入
Spring是整合了Quartz的,所以我们在Spring工程中导入以下依赖即可:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2.2 核心组件
1)任务Job(接口):即想要调用的任务类,需要实现org.quartz.job接口,并重写execute()方法,任务调度时会执行execute()方法。(最新版本继承QuartzJobBean类,重写executeInternal方法)
2)触发器Trigger(接口):即执行任务的触发器,当满足什么条件时会去执行你的任务Job,主要分为根据时长间隔执行的SimpleTrigger和根据日历执行的CronTrigger。
3)调度器Scheduler(接口):即将Trigger和Job绑定之后,根据Trigger中的设定,负责进行Job调度的组件。
我们首先完成一个任务类:
package all.demo.test;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MyJob extends QuartzJobBean // 或者 implements Job
{ // 实现Job接口
private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 重写父类的execute方法
// @Override
// public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// // 这里就是任务!放任务的业务逻辑!
// // 实时获取天气、订单定时取消等
// String now = simpleDateFormat.format(new Date());
// System.out.println("MyJob is running at " + now);
// }
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
String now = simpleDateFormat.format(new Date());
System.out.println("MyJob is running at " + now);
}
}
接着我们在主程序中设置触发器和调度器:
package all.demo.test;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.Date;
public class QuartzTest {
public static void main(String[] args) {
try {
// 1. 创建任务调度器Scheduler,需要从工厂类中获取,需要注意的是Scheduler是一个接口,不能直接new
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// 2. 创建触发器,定义了任务调度的时机(延迟、循环、定时...)
// SimpleTrigger
// CronTrigger 依据时间表达式构造的触发器
SimpleTrigger // 创建简单触发器,也可以写成Trigger
trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1") // 给定触发器唯一标识,设置触发器名称和组名
.startAt(new Date(System.currentTimeMillis() + 3000)) // 设置触发器开始时间
.withSchedule(
SimpleScheduleBuilder // 简单调度方式
.simpleSchedule() // 创建调度规则对象
.withIntervalInSeconds(3) // 设置间隔时间,单位为秒
.withRepeatCount(10) // 设置循环次数
) // 设置触发器调度方式,比如循环、延迟、定时等,即调度规则
//.endAt() // 设置触发器结束时间
.build();
// // 3. JobBuilder创建任务对象
// JobBuilder jobBuilder = JobBuilder.newJob(MyJob.class) // 创建任务对象
// .withIdentity("job1", "group1"); // 给定任务唯一标识,设置任务名称和组名
// // 4. JobDetail才是真正的任务对象,它里面包含了任务名称、组名、任务执行类等
// JobDetail jobDetail = jobBuilder.build();
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("job1", "group1")
.build();
// 5. 将任务和触发器关联起来
scheduler.scheduleJob(job, trigger);
// 启动任务
scheduler.start();
// 在start()之后,在shutdown()之前,需要对任务进行装载
// 暂停所有任务(还有其他的也可以暂停)
// scheduler.pauseAll();
// 恢复所有任务
// scheduler.resumeAll();
// 停止调度器
// scheduler.shutdown();
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
}
我们运行一下程序:
MyJob is running at 2024-09-18 16:42:04
MyJob is running at 2024-09-18 16:42:07
MyJob is running at 2024-09-18 16:42:10
MyJob is running at 2024-09-18 16:42:13
MyJob is running at 2024-09-18 16:42:16
MyJob is running at 2024-09-18 16:42:19
MyJob is running at 2024-09-18 16:42:22
MyJob is running at 2024-09-18 16:42:25
MyJob is running at 2024-09-18 16:42:28
MyJob is running at 2024-09-18 16:42:31
MyJob is running at 2024-09-18 16:42:34
以上就是一个定时任务的简单应用。
2.3 实现原理
首先,我们是要有一个Scheduler对任务的调度进行管理,那么只要我们有了合适的Job和Trigger的一个组合,我们将它们放到其中,即可对这个任务进行管理。
其次,我们看下Job,对于一个任务类来说,里面放的就是我们要想实现功能的具体业务逻辑代码,例如实时获取天气预报、超时订单清理等。具体来说我们只需要实现Job接口(或者去继承QuartzJobBean),重写当中的execute(executeInternal)方法,完成业务逻辑代码即可。这样一来,我们可以写出很多的任务。
接着,与这些任务(这些任务就好比死的)相关联的是Trigger,一个触发器。触发器那就规定了对于一个任务它的执行是符合于什么样的规律。触发器分为两类,SimpleTrigger和CronTrigger。第一个是根据时长间隔来触发的,好比对于接受的每一个订单,在我们业务处理的过程中就应该为其设置上一个好比间隔10分钟的触发器,在10分钟后对超时的订单进行业务逻辑处理。而第二个是可以设置定点触发的,好比每周三晚上10:00。举个例子,我们在一些网站上登录时会设置我们的生日,我们就可以设计一个每天晚上00:00的一个触发器,完成一个查询所有用户今天生日从而给他们发送一条祝福短信的业务。
那么,我们有了一些设计好的任务以及设定好的规则。其次,就是将我们的任务Job用JobDetail包装起来,用包装起来的JobDetail和一个Trigger得到一个组合。将这个组合注册到Scheduler中,那么就会依照这种规则去执行这个任务。
其中,对于相同的任务我们可以和不同的Trigger组合,从而对一个任务可能有多种规则与之匹配。但是一个Trigger只能与一个任务JobDetail匹配。想实现相同规则,就在写一个Trigger。
三、细说Quartz
3.1 Scheduler
Scheduler对任务进行调度和管理。
在Quartz中Scheduler由StdSchedulerFactory所创建(其他方法暂不介绍)。使用的是StdScheduler。
3.2 Trigger
Trigger是用于定义调度时间的元素,也就是按照什么样的规则去执行任务。
Quartz中提供了四种类型的Trigger:
SimpleTrigger、CronTrigger、DateIntervalTrigger、NthIncludeDayTtigger。后两个不常用。
以上四种可以满足绝大部分的需求。
3.2.1 SimpleTrigger - 简单
可以满足的是:在具体的时间点执行一次;或在具体时间点执行,然后以指定的时间间隔重复执行若干次。简单说就是能做到1.延时任务(超时订单处理)、2.循环任务(每隔指定周期循环一次,可以无限循环、也可以循环指定的次数、还可以循坏到指定的时间点)、3.定时任务(每晚12:00发送生日祝福)。
常见例子:
package all.demo.test;
import org.quartz.DateBuilder;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.TriggerBuilder;
import java.util.Date;
public class QuartzSimpleTest {
public static void main(String[] args) {
// 使用强制类型转化,获得一个SimpleTrigger对象
// 指定开始时间,但是不重复
SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1") // 设置触发器名称和组名
.startAt(new Date(System.currentTimeMillis())) // 设置触发器开始时间
.forJob("job1", "group1") // 设置触发器关联的任务名称和组名
.build();
// 使用SimpleScheduleBuilder创建一个SimpleTrigger对象,设置重复执行5次,每1秒执行一次
SimpleTrigger trigger2 = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("trigger2", "group2")
.startAt(new Date(System.currentTimeMillis()))
.withSchedule(
SimpleScheduleBuilder.simpleSchedule() // 设置SimpleTrigger的调度器
.withRepeatCount(5) // 重复5次
.withIntervalInSeconds(1) // 每1秒执行一次 // .var
)
.forJob("job2", "group2")
.build();
// 5分钟后开始触发,只触发一次
SimpleTrigger trigger3 = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("trigger3", "group3")
// futureDate方法创建一个Date对象,表示5分钟后
.startAt(DateBuilder.futureDate(5, DateBuilder.IntervalUnit.MINUTE))
.forJob("job3", "group3")
.build();
// 立即触发,每隔5分钟触发一次,直到22;00
SimpleTrigger trigger4 = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("trigger4", "group4")
.startNow()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInMinutes(5) // 每5分钟执行一次
.repeatForever() // 不停止,一直重复
)
.endAt(DateBuilder.dateOf(22, 0, 0)) // 结束时间
.forJob("job4", "group4")
.build();
// 建立一个触发器,将在下一个小时的整点触发,然后每2小时重复一次
SimpleTrigger trigger5 = (SimpleTrigger) TriggerBuilder.newTrigger()
.withIdentity("trigger5", "group5")
.startAt(DateBuilder.evenHourDate(null)) // 下一个小时整点触发
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInHours(2) // 每2小时重复一次
.repeatForever() // 不停止,一直重复
)
.forJob("job5", "group5")
.build();
}
}
3.2.2 CronTrigger - 强大
使用Cron表达式来定义触发时间的规则。Cron表达式即一个基于日历的字符串表达。
3.2.2.1 Cron表达式
详见Cron常用表达式详解_cron表达式-CSDN博客、cron表达式详解-CSDN博客。
在线生成器Cron - 在线Cron表达式生成器 (ciding.cc)。
而CronTrigger差别不大,只需修改个别地方。
package all.demo.test;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.DateBuilder;
import org.quartz.TriggerBuilder;
import java.util.Date;
public class QuartzCronTest {
public static void main(String[] args) {
// 创建一个触发器,在开始10秒后,每5秒执行一次,直到15点整
CronTrigger cronTrigger = (CronTrigger) TriggerBuilder.newTrigger()
.withIdentity("cronTrigger", "group1")
.startAt(DateBuilder.futureDate(10, DateBuilder.IntervalUnit.SECOND))
.withSchedule(
// 每5秒执行一次
CronScheduleBuilder.cronSchedule("0/5 * * * * ?")
)
.endAt(DateBuilder.dateOf(15, 0, 0))
.build();
}
}
3.3 Misfire策略
我们已经将触发器及任务绑定到了一个调度器上,并且在执行的过程中,调度器突发故障停了,或者说线程池不足(任务的调度会占用线程)。就会导致我们的触发器时间到了却没有执行任务的情况——Misfire。
而处理这种情况的解决方法我们称为Misfire策略。对所有类型的Trigger,Quartz有指定默认的Misfire策略。(MISFIRE_INSTRUCTION_SMART_POLICY,即当恢复调度器时立即执行Misfire的任务,然后继续执行任务)
而对于可以持久化的Trigger(存到数据库),我们可以通过指定Misfire策略,来定义其恢复之后的执行策略。
3.4 Job与JobDetail
任务类实现Job接口,接着包装在JobDetail中从而与Trigger组合绑定到Scheduler中。
其中我们可以使用usingDateMap方法设置Trigger、JobDetail信息,从而在任务类中通过JobExecutionContext获取信息。
任务类:
package all.demo.test;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MyJob extends QuartzJobBean // 或者 implements Job
{ // 实现Job接口
private SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 重写父类的execute方法
// @Override
// public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
// // 这里就是任务!放任务的业务逻辑!
// // 实时获取天气、订单定时取消等
// String now = simpleDateFormat.format(new Date());
// System.out.println("MyJob is running at " + now);
// }
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
// 我们发现这个方法有个参数,这个参数就是JobExecutionContext,这个对象中封装了任务的信息,包括任务名称、组名称、参数等。
// 获取JobDetail或者Trigger的信息
System.out.println(context.getTrigger().getJobDataMap().get("name"));
System.out.println(context.getJobDetail().getJobDataMap().get("age"));
String now = simpleDateFormat.format(new Date());
System.out.println("MyJob is running at " + now);
}
}
测试:
package all.demo.test;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.Date;
public class QuartzMapTest {
public static void main(String[] args) {
try {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
SimpleTrigger // 创建简单触发器,也可以写成Trigger
trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1", "group1") // 给定触发器唯一标识,设置触发器名称和组名
.startAt(new Date(System.currentTimeMillis() + 3000)) // 设置触发器开始时间
.withSchedule(
SimpleScheduleBuilder // 简单调度方式
.simpleSchedule() // 创建调度规则对象
.withIntervalInSeconds(3) // 设置间隔时间,单位为秒
.withRepeatCount(10) // 设置循环次数
)
.usingJobData("name", "zhangsan")
.build();
// JobDetail jobDetail = jobBuilder.build();
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("job1", "group1")
.usingJobData("age", "18")
.build();
scheduler.scheduleJob(job, trigger);
// 启动任务
scheduler.start();
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
}
3.5 监听器
简单使用:
1. 创建监听器
package all.demo.test.lisner;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
public class MyJobListener implements JobListener {
// 设置监听器的名称,要唯一标识
@Override
public String getName() {
return "MyJobListener";
}
// 任务之前触发执行
@Override
public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {
System.out.println("任务开始执行");
}
// 任务被否决触发执行时触发
@Override
public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {
System.out.println("任务被否决");
}
// 任务执行完毕触发执行
@Override
public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e) {
System.out.println("任务执行完毕");
}
}
2. 注册监听器
TriggerListener类似。
3.6 存储及持久化
3.6.1 JobStore
事实上,我们将JobDetail和Trigger组合加入到Scheduler中的时候,相关的信息(包括JobDataMap)就会存储在JobStore当中。即负责存储调度器中的工作数据:Job、Trigger、JobDataMap。存什么就执行什么。
而持久化的概念就是在数据库中存储下我们的工作数据。假如我们现在有个需要执行100次的一个任务,其中每执行一次,JobStore中的数据会随时更新。当我执行了40次的时候,系统崩溃了。然后整个项目重启,任务需要重头开始(默认又从0开始,基于内存RAM)。所以这会不崩溃的话,我们实际上会执行140次。
那么我们只要在崩溃的时候存储下JobStore中的内容,恢复的时候我们在数据库中拿工作数据就可以继续上一次的任务执行。
以上即RAMJobStore,而我们现在使用JDBCJobStore。
实现也不难,只要在配置中修改Quartz的配置属性JobStore修改为JDBCJobStore,并配上我们的数据源即可。支持集群。
spring:
quartz:
job-store-type: jdbc