Quartz
之前在项目中用定时任务都是直接用的Spring的@Scheduled注解,因为遇到的业务都比较简单,比如定时更新榜单,支付功能中的定时查单,最近在写一个课堂助手的项目,要实现定时任务的业务对任务有增删改查的需求,@Scheduled已经无法满足了,so,我选择了Quartz
Quartz是OpenSymphony开源的一个项目,是一个由Java编写的开源作业调度框架。
- 支持分布式高可用
- 支持持久化,支持调度数据的多种存储方式
- 支持多任务调度和管理
存储方式
- RAMJobStore
- 不要外部数据库,配置容易,运行快
- 调度程序信息存在内存中,当应用程序停止运行时,所有调度信息将丢失
- 而且因为存在内存中,Job和Trigger的数量将会受到限制
- JDBCJobStore
- 支持集群
- 所有任务信息都会保存在数据库中,不会因为程序停止运行丢失
- 运行速度取决于数据库的速度
组件
Quartz的组成
JobDetail
Job相当于是线程池中的task,是定时任务真正业务逻辑的部分,Job需要封装成JobDetail,一个Job可以对应多JobDetail
通过JobBuilder创建
Trigger
触发器,定义触发时间,常用的有以下两种
- SimleTrigger:用于实现简单的定时,比如定频率的执行某个任务
- CronTrigger:配合Cron表达式使用,可以实现相对复杂的业务,比如到某个具体日期执行,每个月几号执行等
通过TriggerBuilder创建
Scheduler
调度器,帮我们把JobDetail和Trigger绑定在一起,按照Trigger中定义的触发时间去触发JobDetail中的Job
使用SchedulerFactory创建
DirectSchedulerFactory
:需要在代码中定义一些属性,不常用StdSchedulerFactory
:从配置文件中获取配置,推荐使用
java
//调度器
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); // 这里简单获取了一个默认的,可以在配置文件中配置属性并获取
scheduler.scheduleJob(jobDetail,trigger);
scheduler.start();
任务每次执行Scheduler都会根据JobDetail创建一个新的Job实例,让任务并发执行,如果我们不想要这种特性,想要前一个任务执行完了才会执行下一个,可以使用@DisallowConcurrentExecution
注解关闭
由于Scheduler每次执行都会根据JobDetail创建一个新的Job实例,jobDataMap属于JobDetail,那么每次也是一个新的,我们可以使用@PersistJobDataAfterExecution
来让JobDataMap持久化
JobDataMap
我们可以看到JobDetail和Trigger中都有JobDataMap,jobDataMap可以用于在启动这个定时器时,往任务中传递一些参数
任务类获取参数的方式有两种
- 通过获取jobDataMap再获取对应键值
- 在任务类中定义相关字段,设置好set方法,框架会自动往里面设置值,如果JobDetail和Trigger里设置相同名称的话,JobDetail的会被覆盖掉
go
JobDetail jobDetail = JobBuilder.newJob(MyJob.class)
.withIdentity("job1","group1")
.usingJobData("job","gwj's job") // 用来设置JobDetail中的JobDataMap中的值
.usingJobData("name","jobdetail")
.usingJobData("tong","jobde")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("trigger1","triggerGroup1")
.usingJobData("trigger","my trigger") // 用来设置Trigger中的JobDataMap中的值
.usingJobData("name","trigger")
.usingJobData("tong","triggde")
.startNow()
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(1)
.repeatForever())
.build();
任务类
java
@Data
public class MyJob implements Job {
private String name;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDataMap jobDetailMap = jobExecutionContext.getJobDetail().getJobDataMap(); // 获取JobDetail的JobDataMap
JobDataMap triggerMap = jobExecutionContext.getTrigger().getJobDataMap(); // 获取Trigger的JobDataMap
JobDataMap mergeMap = jobExecutionContext.getMergedJobDataMap(); // 获取混合JobDataMap
System.out.println(mergeMap.get("job")); // gwj's job
System.out.println(mergeMap.get("trigger")); // my trigger
System.out.println(mergeMap.get("tong")); // triggde
System.out.println(name); // trigger
// 具体任务流程......
}
}
整合SpringBoot
导入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
配置文件
yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test_db
username: root
password: 111111
type: com.alibaba.druid.pool.DruidDataSource
servlet:
multipart:
max-file-size: 2048MB
max-request-size: 2048MB
# 定时任务配置
quartz:
# 数据库方式
job-store-type: jdbc
jdbc:
initialize-schema: always # 数据库架构初始化模式
# never:从不 always:每次都清空数据库初始化 embedded:只初始化内存数据库(默认)
# quartz 相关属性配置
properties:
org:
quartz:
scheduler:
instanceName: demoScheduler
instanceId: AUTO
jobStore:
class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: QRTZ_
isClustered: true
clusterCheckinInterval: 10000
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool
threadCount: 10
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
创建表
Quartz支持对任务进行crud,可以使用mysql存储记录我们的任务
方式一
配置文件中配置initialize-schema: always
,项目启动后我们就可以看到数据库中自动生成了11张表,后面再删掉或者改成never
就行
方式二
可以在Quartz的jar包中找到对应数据库的SQL脚本,拷贝出来执行即可
表说明
表名 | 说明 |
---|---|
qrtz_blob_triggers | 以Blob 类型存储的触发器 |
qrtz calendars | 存放日历信息,quartz可配置一个日历来指定一个时间范围 |
qrtz_cron triggers | 存放cron类型的触发器 |
qrtz fired triggers | 存放已触发的触发器 |
qrtz job _details | 存放一个jobDetail信息 |
qrtz job listeners | job监听器 |
qrtz_locks | 存储程序的悲观锁的信息(假如使用了悲观锁) |
qrtz_paused trigger_graps | 存放暂停掉的触发器 |
qrtz scheduler state | 调度器状态 |
qrtz simple triggers | 简单触发器的信息 |
qrtz_trigger_listeners | 触发器监听器 |
编写业务类
示例
java
/**
* 任务业务类,用于动态处理任务信息
* @author gwj
* @date 2024/11/29 下午2:17
*/
public interface JobService {
/**
* 任务数据
*/
String TASK_DATA = "taskData";
/**
* 添加定时任务
* @param jobClass
* @param jobName
* @param cron
* @param data
*/
void addCronJob(Class jobClass, String jobName, String cron, String data);
/**
* 添加立即执行的任务
* @param jobClass
* @param jobName
* @param data
*/
void addCronJob(Class jobClass, String jobName, String data);
/**
* 暂停任务
* @param jobName
* @param jobGroup
*/
void pauseJob(String jobName, String jobGroup);
/**
* 恢复任务
* @param triggerName
* @param triggerGroup
*/
void resumeJob(String triggerName, String triggerGroup);
/**
* 删除job
* @param jobName
* @param jobGroup
*/
void deleteJob(String jobName, String jobGroup);
}
java
/**
* @author gwj
*/
@Slf4j
@Service
public class JobServiceImpl implements JobService {
/**
* Quartz定时任务核心的功能实现类
*/
@Autowired
private Scheduler scheduler;
@Override
public void addCronJob(Class jobClass, String jobName, String cron, String data) {
String jobGroup = JobGroup.SYSTEM;
// 自动命名
if(StringUtils.isEmpty(jobName)){
jobName = jobClass.getSimpleName().toUpperCase() + "_"+ IdWorker.getIdStr();
}
try {
JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
if (jobDetail != null) {
log.info("++++++++++任务:{} 已存在", jobName);
this.deleteJob(jobName, jobGroup);
}
log.info("++++++++++构建任务:{},{},{},{},{} ", jobClass.toString(), jobName, jobGroup, cron, data);
//构建job信息
jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroup).build();
//用JopDataMap来传递数据
jobDetail.getJobDataMap().put(TASK_DATA, data);
//按新的cronExpression表达式构建一个新的trigger
Trigger trigger = null;
// 有表达式的按表达式
if(!StringUtils.isEmpty(cron)){
log.info("+++++表达式执行:"+ JSON.toJSONString(jobDetail));
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup)
.withSchedule(scheduleBuilder).build();
}else{
// 无表达式则立即执行
log.info("+++++立即执行:"+ JSON.toJSONString(jobDetail));
trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).startNow().build();
}
scheduler.scheduleJob(jobDetail, trigger);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void addCronJob(Class jobClass, String jobName, String data) {
// 立即执行任务
this.addCronJob(jobClass, jobName, null, data);
}
@Override
public void pauseJob(String jobName, String jobGroup) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
scheduler.pauseTrigger(triggerKey);
log.info("++++++++++暂停任务:{}", jobName);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
@Override
public void resumeJob(String jobName, String jobGroup) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
scheduler.resumeTrigger(triggerKey);
log.info("++++++++++重启任务:{}", jobName);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
@Override
public void deleteJob(String jobName, String jobGroup) {
try {
JobKey jobKey = JobKey.jobKey(jobName,jobGroup);
scheduler.deleteJob(jobKey);
log.info("++++++++++删除任务:{}", jobKey);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
测试
java
/**
* @author gwj
* @date 2024/11/30 21:18
*/
@RestController
@RequestMapping("/test")
public class TestController {
@Resource
private JobService jobService;
/**
* 创建一个一分钟后执行的定时任务
*
* @param id
* @return
*/
@PostMapping
public String addTask(Long id) {
String jobName = JobPrefix.BREAK_EXAM + id;
Date date = new Date();
long l = date.getTime() + 1000 * 60;
Date until = new Date(l);
jobService.addCronJob(BreakExamJob.class,jobName, CronUtils.dateToCron(until),id+"");
return "定时任务创建成功";
}
}