动态任务调度
1、使用Quartz实现任务的管理
流程
- 首先需要创建我们的任务(Job),比如取消订单、定时发送短信邮件之类的,这是我们的任务主体,也是写业务逻辑的地方。
- 创建任务调度器(Scheduler),这是用来调度任务的,主要用于启动、停止、暂停、恢复等操作,也就是那几个api的用法。
- 创建任务明细(JobDetail),最开始我们编写好任务(Job)后,只是写好业务代码,并没有触发,这里需要用JobDetail来和之前创建的任务(Job)关联起来,便于执行。
- 创建触发器(Trigger),触发器是来定义任务的规则的,比如几点执行,几点结束,几分钟执行一次等等。这里触发器主要有两大类(SimpleTrigger和CronTrigger)。
- 根据Scheduler来启动JobDetail与Trigger
1.1 引入maven依赖
<!--quartz管理定时任务-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
1.2 创建定时任务类Job
@DisallowConcurrentExecution
//Job中的任务有可能并发执行,
// 例如任务的执行时间过长,而每次触发的时间间隔太短,
// 则会导致任务会被并发执行。如果是并发执行,
// 就需要一个数据库锁去避免一个数据被多次处理。
public class TestJob implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
//任务的业务逻辑写在这里
System.err.println(jobExecutionContext.getJobDetail().getJobDataMap().get("name"));
System.err.println(jobExecutionContext.getJobDetail().getJobDataMap().get("param"));
System.err.println(jobExecutionContext.getTrigger().getJobDataMap().get("orderNo"));
System.out.println("-------------------\n\n 定时任务执行\n\n-------------\n");
}
}
1.3 创建任务调度器(Scheduler)
@Autowired
private Scheduler scheduler;
1.4 创建任务明细(JobDetail)
/**通过JobBuilder.newJob()方法获取到当前Job的具体实现(以下均为链式调用)
* 这里是固定Job创建,所以代码写死XXX.class
* 如果是动态的,根据不同的类来创建Job,则 ((Job)Class.forName("com.zy.job.TestJob").newInstance()).getClass()
* 即是 JobBuilder.newJob(((Job)Class.forName("com.zy.job.TestJob").newInstance()).getClass())
* */
JobDetail jobDetail = JobBuilder.newJob(TestJob.class)
/**给当前JobDetail添加参数,K V形式*/
.usingJobData("name","zy")
/**给当前JobDetail添加参数,K V形式,链式调用,可以传入多个参数,在Job实现类中,可以通过jobExecutionContext.getJobDetail().getJobDataMap().get("age")获取值*/
.usingJobData("age",23)
/**添加认证信息,有3种重写的方法,我这里是其中一种,可以查看源码看其余2种*/
.withIdentity("我是name","我是group")
.build();//执行
1.5 创建触发器(Trigger)
这里主要分为两大类SimpleTrigger、CronTrigger。
SimpleTrigger:是根据它自带的api方法设置规则,比如每隔5秒执行一次、每隔1小时执行一次。
Trigger trigger = TriggerBuilder.newTrigger()
/**给当前JobDetail添加参数,K V形式,链式调用,可以传入多个参数,在Job实现类中,可以通过jobExecutionContext.getTrigger().getJobDataMap().get("orderNo")获取值*/
.usingJobData("orderNo", "123456")
/**添加认证信息,有3种重写的方法,我这里是其中一种,可以查看源码看其余2种*/
.withIdentity("我是name","我是group")
/**立即生效*/
// .startNow()
/**开始执行时间*/
.startAt(start)
/**结束执行时间,不写永久执行*/
.endAt(start)
/**添加执行规则,SimpleTrigger、CronTrigger的区别主要就在这里*/
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
/**每隔3s执行一次,api方法有好多规则自行查看*/
.withIntervalInSeconds(3)
/**一直执行,如果不写,定时任务就执行一次*/
.repeatForever()
)
.build();//执行
CronTrigger:这就比较常用了,是基于Cron表达式来实现的。
CronTrigger trigger = TriggerBuilder.newTrigger()
/**给当前JobDetail添加参数,K V形式,链式调用,可以传入多个参数,在Job实现类中,可以通过jobExecutionContext.getTrigger().getJobDataMap().get("orderNo")获取值*/
.usingJobData("orderNo", "123456")
/**添加认证信息,有3种重写的方法,我这里是其中一种,可以查看源码看其余2种*/
.withIdentity("orderNo","任务唯一信息")
/**立即生效*/
// .startNow()
/**开始执行时间*/
.startAt(start)
/**结束执行时间,不写永久执行*/
.endAt(start)
/**添加执行规则,SimpleTrigger、CronTrigger的区别主要就在这里,我这里是demo,写了个每2分钟执行一次*/
.withSchedule(CronScheduleBuilder.cronSchedule("0 0/2 * * * ?"))
.build();//执行
注意:.startNow( )和.startAt( )这里有个坑,这两个方法是对同一个成员变量进行修改的 也就是说startAt和startNow同时调用的时候任务开始的时间是按后面调用的方法为主的,谁写在后面用谁。
源码如下
public TriggerBuilder<T> startAt(Date triggerStartTime) {
this.startTime = triggerStartTime;
return this;
}
public TriggerBuilder<T> startNow() {
this.startTime = new Date();
return this;
}
1.6 启动任务
/**添加定时任务*/
scheduler.scheduleJob(jobDetail, trigger);
if (!scheduler.isShutdown()) {
/**启动*/
scheduler.start();
}
以上,任务的创建启动都完事了,后面就是任务的暂停、恢复、删除。比较简单,大致原理就是我们在创建任务明细(JobDetail)和创建触发器(Trigger)时,会调用.withIdentity(key,group)来传入认证信息,后续就是根据这些认证信息来管理任务(通过api方法)
1.7 任务的暂停pauseTrigger
scheduler.pauseTrigger(TriggerKey.triggerKey("orderNo","我是刚才写的group"));
1.8 任务的恢复resumeTrigger
scheduler.resumeTrigger(TriggerKey.triggerKey("orderNo","我是刚才写的group"));
1.9 任务的移除(暂停>移除触发器>删除Job)
scheduler.pauseTrigger(TriggerKey.triggerKey("orderNo","我是刚才写的group"));//暂停触发器
scheduler.unscheduleJob(TriggerKey.triggerKey("orderNo","我是刚才写的group"));//移除触发器
scheduler.deleteJob(JobKey.jobKey("orderNo","我是刚才写的group"));//删除Job
最后 附上动态调度封装好的方法
任务实体类
/**
* Created by IntelliJ IDEA.
* User: LvHaoIT (lvhao)
* Date: 2022/9/23
* Time: 11:51 定时任务信息
*/
@Data
@Slf4j
public class ScheduledTaskData {
// @TableId(value = "id", type = IdType.AUTO)
@ApiModelProperty(value = "id")
private String id;
@ApiModelProperty(value = "任务名称")
private String jobName;
@ApiModelProperty(value = "任务路径")
private String jobClass;
@ApiModelProperty(value = "运行时间表达式")
private String corn;
public String getParamJson() {
return JSON.toJSONString(this.param);
}
@ApiModelProperty(value = "是否禁用")
private String disabled;
@ApiModelProperty(value = "是否删除")
private Integer isDel;
@ApiModelProperty(value = "添加人")
private String addUser;
/**
* 添加时间
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@ApiModelProperty(value = "添加时间")
private Date addTime;
private Map<String, Object> param;
public static void printFun(ScheduledTaskData scheduledTaskData, boolean b) {
String info = "";
if (b) {
info = "启动成功";
} else info = "停止成功";
log.info("\n-------------------------------\n\n\t 定时任务:" + info + "\n" +
"\t 任务名称 :" + scheduledTaskData.getJobName() + "\n" +
"\t 任务ID :" + scheduledTaskData.getId() + "\n" +
"\t 任务对象 :" + scheduledTaskData.getJobClass() + "\n" +
"\t 运行时间 :" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + "\n" +
"\n\n-------------------------------");
}
}
封装操作Api
package com.lingxu.module.AutoJob.controller;
import cn.hutool.cron.CronUtil;
import com.lingxu.module.AutoJob.entity.ScheduledTaskData;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Date;
@Slf4j
@Api(tags = "Quartz任务调度")
@RestController
@RequestMapping("/quartz")
public class QuartzJob {
@Resource
private Scheduler scheduler;
@Resource
private CronUtil cronUtil;
@PostMapping("/create")
@ApiOperation(value = "定时任务_创建", notes = "创建")
public Object quartz(@RequestBody ScheduledTaskData scheduledTaskData) throws Exception {
Date start = new Date(System.currentTimeMillis() + 3 * 1000);
JobDetail jobDetail = JobBuilder.newJob(((Job) Class.forName(scheduledTaskData.getJobClass()).newInstance()).getClass())
.usingJobData("name", "aaa")
.usingJobData("param", scheduledTaskData.getParamJson())
.withIdentity(scheduledTaskData.getId())
.build();
CronTrigger trigger = TriggerBuilder.newTrigger()
.usingJobData("orderNo", scheduledTaskData.getId())
.withIdentity(scheduledTaskData.getId())
.startAt(start)
.withSchedule(CronScheduleBuilder.cronSchedule(scheduledTaskData.getCorn()))
.build();
scheduler.scheduleJob(jobDetail, trigger);
if (!scheduler.isShutdown()) {
scheduler.start();
ScheduledTaskData.printFun(scheduledTaskData, true);
} else {
scheduler.shutdown(true);
scheduler.pauseTrigger(TriggerKey.triggerKey(scheduledTaskData.getId()));
scheduler.unscheduleJob(TriggerKey.triggerKey(scheduledTaskData.getId()));
scheduler.deleteJob(JobKey.jobKey(scheduledTaskData.getId()));
scheduler.start();
ScheduledTaskData.printFun(scheduledTaskData, true);
}
return "ok";
}
@PostMapping("/shutdown")
@ApiOperation(value = "定时任务_停止", notes = "停止")
@ResponseBody
public Object shutdown(@RequestParam("orderNo") String orderNo) throws IOException, SchedulerException {
scheduler.pauseTrigger(TriggerKey.triggerKey(orderNo));
return "";
}
@PostMapping("/resume")
@ApiOperation(value = "定时任务_恢复", notes = "恢复")
@ResponseBody
public Object resume(@RequestParam("orderNo") String orderNo) throws IOException, SchedulerException {
scheduler.resumeTrigger(TriggerKey.triggerKey(orderNo));
return "ok";
}
@PostMapping("/del")
@ApiOperation(value = "定时任务_删除", notes = "删除")
@ResponseBody
public Object del(@RequestParam("orderNo") String orderNo) throws IOException, SchedulerException {
scheduler.pauseTrigger(TriggerKey.triggerKey(orderNo));
scheduler.unscheduleJob(TriggerKey.triggerKey(orderNo));
scheduler.deleteJob(JobKey.jobKey(orderNo));
return "ok";
}
}
如果需要让定时任务在启动项目后自动启动,则需要持久化任务,可以把基本信息保存在数据库中,项目启动时循环启动
错误解决
Unable to store Job : ‘DEFAULT.TASK_1‘, because one already exists with this identification.定时任务报错
原因: 是Quartz框架对应的数据库表格的问题(因为任务并未能正常结束,产生了脏数据),我们只需要删除以下三个好了。
DELETE from QRTZ_CRON_TRIGGERS;
DELETE from QRTZ_TRIGGERS;
DELETE from QRTZ_JOB_DETAILS;
2、使用hutool工具包中CronUtil实现(实现简单)
hutool的定时任务模块与Linux的Crontab使用上非常类似,通过一个cron.setting
配置文件,简单调用start()方法即可简单使用。
官方文档介绍:hutool官方文档
2.1 引入maven 依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-cron</artifactId>
<version>5.5.6</version>
</dependency>
2.2 创建单例CronUtil
@Component
public class CronUtiObject {
@Bean
public CronUtil getCronUtil() {
CronUtil cronUtil = new CronUtil();
return cronUtil;
}
}
2.3 编写定时任务类
需要实现Task接口
package com.lingxu.module;
import cn.hutool.cron.task.Task;
/**
* Created by IntelliJ IDEA.
* User: LvHaoIT (lvhao)
* Date: 2022/9/23
* Time: 16:29
*/
public class TestTask implements Task {
@Override
public void execute() {
System.out.println("!!!!! 2s定时任务开始");
}
}
2.4 编写任务管理代码
1. 新增任务 `CronUtil.schedule(任务编号, cron表达式 "*/2 * * * * *", 定时任务实例)`
2. 启动定时任务 `CronUtil.start();`
3. 移除定时任务 `CronUtil.remove(任务编号);`
4. 停止任务 `CronUtil.stop();`
@Test
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// CronUtil.schedule("abc", "*/2 * * * * *", new TestTask());
CronUtil.schedule("abc", "*/2 * * * * *", (Task) Class.forName("com.lingxu.module.TestTask").newInstance());
CronUtil.setMatchSecond(true);
CronUtil.start();
System.out.println("cronUtil已经启动");
CronUtil.schedule("aaa", "*/10 * * * * *", new Task() {
public void execute() {
System.out.println("===== 10 s定时任务开始");
}
});
ThreadUtil.sleep(12000);
CronUtil.remove("abc");
// CronUtil.stop();
}
以上两种都可以实现定时任务的动态调度,Quartz更灵活但实现也比较复杂,CronUtil实现起来较为简单,单管理功能偏少。