Quartz的简单使用
通过调用接口,动态设置定时任务的执行策略。提供启动、暂停、唤起、重新指定执行策略、删除、立即触发的接口。目前只提供了通过cron指定执行策略的形式,其他触发形式可以按自己需求修改。
依赖
项目用的是spring boot,使用的是集成的jar包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
任务枚举
接口入参传入任务名称,通过枚举找到任务的具体实现类
public enum QuartzTaskEnum {
SYNCSTOCK("syncStock","com.liangsy.boot.quartzJob.SyncStocksJob")
;
//任务名称
private String taskName;
//Job实现类的全限定名
private String clazz;
QuartzTaskEnum(String taskName, String clazz) {
this.taskName = taskName;
this.clazz = clazz;
}
public String getTaskName(){
return taskName;
}
public String getClazz(){
return clazz;
}
//根据taskName获取对应Job实现类的全限定名
public static QuartzTaskEnum getClazzByTaskName(String taskName){
QuartzTaskEnum[] values = QuartzTaskEnum.values();
for (QuartzTaskEnum value : values) {
if (StringUtils.equals(taskName,value.taskName)){
return value;
}
}
return null;
}
}
任务实现类
继承QuartzJobBean抽象类,实现executeInternal方法。executeInternal方法里的是任务具体逻辑。
注意这里的SyncStocksJob类无需添加spring的@Component一类的组件注解,@Autowired也能生效,具体原因还没详细看。可以先参考文末给出的链接。
import org.springframework.scheduling.quartz.QuartzJobBean;
public class SyncStocksJob extends QuartzJobBean {
@Autowired
private SyncStocksService syncStocksService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
syncStocksService.syncStocks();
}
}
接口入参类
@Data
public class QuartzEntity {
//任务名,和枚举中的对应
private String taskName;
//指定执行cron表达式
private String cronExp;
//通过入参的taskName从枚举中获取对应的Job具体实现类的全限定名
public String getTaskClass(){
QuartzTaskEnum quartzTaskEnum = QuartzTaskEnum.getClazzByTaskName(taskName);
if (Objects.isNull(quartzTaskEnum)) {
return null;
}
return quartzTaskEnum.getClazz();
}
}
工具类
工具类中各个方法的入参Scheduler在Controller中通过@Autowired注入再传进来,框架会默认自动创建这个调度器。而QuartzEntity就是我们自定义的,这里是通过接口传进来Controller,再传进来的。
public class QuartzUtils {
private final static SimpleDateFormat SDF;
static{
SDF = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
/**
* 创建定时任务 定时任务创建之后默认启动状态
* @param scheduler 调度器
* @param quartzBean 定时任务信息类
* @throws Exception
*/
public static void createScheduleJob(Scheduler scheduler, QuartzEntity quartzBean) throws SchedulerException, ClassNotFoundException {
if (Objects.isNull(quartzBean.getTaskClass())) {
throw new ClassNotFoundException("请检查是否有配置{"+quartzBean.getTaskName()+"}对应任务");
}
if (hasTask(scheduler, quartzBean)) {
throw new SchedulerException("任务"+quartzBean.getTaskName()+"已存在");
}
//获取到定时任务的执行类 必须是类的绝对路径名称
//定时任务类需要是job类的具体实现 QuartzJobBean是job的抽象类。
Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(quartzBean.getTaskClass());
// 构建定时任务信息
JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(quartzBean.getTaskName()).build();
// 设置定时任务执行方式,withMisfireHandlingInstructionDoNothing()指定错过的执行不会触发
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzBean.getCronExp()).withMisfireHandlingInstructionDoNothing();
// 构建触发器trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(quartzBean.getTaskName()).withSchedule(scheduleBuilder).build();
scheduler.scheduleJob(jobDetail, trigger);
}
/**
* 根据任务名称暂停定时任务
* @param scheduler 调度器
* @param quartzBean 定时任务信息类
* @throws SchedulerException
*/
public static void pauseScheduleJob(Scheduler scheduler, QuartzEntity quartzBean) throws SchedulerException {
if (!hasTask(scheduler, quartzBean)) {
throw new SchedulerException("任务"+quartzBean.getTaskName()+"不存在");
}
JobKey jobKey = JobKey.jobKey(quartzBean.getTaskName());
scheduler.pauseJob(jobKey);
}
/**
* 根据任务名称恢复定时任务
* @param scheduler 调度器
* @param quartzBean 定时任务信息类
* @throws SchedulerException
*/
public static void resumeScheduleJob(Scheduler scheduler, QuartzEntity quartzBean) throws SchedulerException {
if (!hasTask(scheduler, quartzBean)) {
throw new SchedulerException("任务"+quartzBean.getTaskName()+"不存在");
}
JobKey jobKey = JobKey.jobKey(quartzBean.getTaskName());
scheduler.resumeJob(jobKey);
}
/**
* 根据任务名称立即运行一次定时任务
* @param scheduler 调度器
* @param quartzBean 定时任务信息类
* @throws SchedulerException
*/
public static void runOnce(Scheduler scheduler, QuartzEntity quartzBean) throws SchedulerException {
if (!hasTask(scheduler, quartzBean)) {
throw new SchedulerException("任务"+quartzBean.getTaskName()+"不存在");
}
JobKey jobKey = JobKey.jobKey(quartzBean.getTaskName());
scheduler.triggerJob(jobKey);
}
/**
* 更新定时任务
* @param scheduler 调度器
* @param quartzBean 定时任务信息类
* @throws SchedulerException
*/
public static void updateScheduleJob(Scheduler scheduler, QuartzEntity quartzBean) throws SchedulerException {
if (!hasTask(scheduler, quartzBean)) {
throw new SchedulerException("任务"+quartzBean.getTaskName()+"不存在");
}
//获取到对应任务的触发器
TriggerKey triggerKey = TriggerKey.triggerKey(quartzBean.getTaskName());
//设置定时任务执行方式,withMisfireHandlingInstructionDoNothing()指定错过的执行不会触发
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzBean.getCronExp()).withMisfireHandlingInstructionDoNothing();
//重新构建任务的触发器trigger
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
//重置对应的job
scheduler.rescheduleJob(triggerKey, trigger);
}
/**
* 根据定时任务信息类从调度器当中删除定时任务
* @param scheduler 调度器
* @param quartzBean 定时任务信息类
* @throws SchedulerException
*/
public static void deleteScheduleJob(Scheduler scheduler, QuartzEntity quartzBean) throws SchedulerException {
if (!hasTask(scheduler, quartzBean)) {
throw new SchedulerException("任务"+quartzBean.getTaskName()+"不存在");
}
JobKey jobKey = JobKey.jobKey(quartzBean.getTaskName());
scheduler.deleteJob(jobKey);
}
/**
* 获取当前已启动的任务名称
* @param scheduler 调度器
* @return
*/
public static List<ScheduledJob> getScheduleJobs(Scheduler scheduler) throws SchedulerException {
List<ScheduledJob> scheduledJobs = new ArrayList<>();
Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.anyGroup());
for (JobKey jobKey : jobKeys) {
List<? extends Trigger> triggersOfJob = scheduler.getTriggersOfJob(jobKey);
Trigger trigger;
//目前createScheduleJob方法中只允许一个Job添加一个Trigger,所以这里直接用get(0)了
if (CollectionUtils.isEmpty(triggersOfJob) || Objects.isNull(trigger = triggersOfJob.get(0))) {
continue;
}
Date finalFireTime = trigger.getFinalFireTime();
Date previousFireTime = trigger.getPreviousFireTime();
Date nextFireTime = trigger.getNextFireTime();
Date endTime = trigger.getEndTime();
Date startTime = trigger.getStartTime();
Trigger.TriggerState triggerState = scheduler.getTriggerState(trigger.getKey());
ScheduledJob scheduledJob = new ScheduledJob();
scheduledJob.setTaskName(jobKey.getName());
scheduledJob.setFinalFireTime(finalFireTime == null ? null : SDF.format(finalFireTime));
scheduledJob.setPreviousFireTime(previousFireTime == null ? null : SDF.format(previousFireTime));
scheduledJob.setNextFireTime(nextFireTime == null ? null : SDF.format(nextFireTime));
scheduledJob.setEndTime(endTime == null ? null : SDF.format(endTime));
scheduledJob.setStartTime(startTime == null ? null : SDF.format(startTime));
scheduledJob.setStatus(triggerState.name());
scheduledJobs.add(scheduledJob);
}
return scheduledJobs;
}
/**
* 检查任务是否已存在
* @param scheduler 调度器
* @param quartzBean 定时任务信息类
* @return
*/
public static Boolean hasTask(Scheduler scheduler,QuartzEntity quartzBean) throws SchedulerException {
JobKey jobKey = JobKey.jobKey(quartzBean.getTaskName());
return scheduler.checkExists(jobKey);
}
}
注意啦!!!
上方有个地方需要注意,updateScheduleJob方法中创建CronScheduleBuilder对象的最后有一个withMisfireHandlingInstructionDoNothing方法。
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzBean.getCronExp()).withMisfireHandlingInstructionDoNothing();
这是**可能是(你没看错,就是可能是;哈哈哈哈,因为我目前只是观察到这种情况,具体是否因为这样还没细看,已经了解的大佬可以提醒我下,我自己后面也看看)**由于Scheduler的rescheduleJob方法调用到以下逻辑,导致修改任务可能立即触发一次,我当前项目要避免这种情况,所以添加上withMisfireHandlingInstructionDoNothing方法;是否要添加此方法,看自己项目情况。
/**
* org.quartz.core.QuartzScheduler类中
*
* 通过
* org.quartz.impl.StdScheduler#rescheduleJob
* 调用到此方法
*/
public Date rescheduleJob(TriggerKey triggerKey,
Trigger newTrigger) throws SchedulerException {
validateState();
····(省略不重要的)
Calendar cal = null;
if (newTrigger.getCalendarName() != null) {
cal = resources.getJobStore().retrieveCalendar(
newTrigger.getCalendarName());
}
/**
* 看这里
* 计算第一次触发的时间
* 这里的第一次触发时间是从任务的startTime开始,以传入的cron策略开始算的第一次。
* 所以会存在返回早于当前的时间
* 如果返回了早于当前的时间,会导致任务会立马触发一次,即使当前时间不满足cron表达式
*
* org.quartz.impl.triggers.CronTriggerImpl#computeFirstFireTime
*/
Date ft = trig.computeFirstFireTime(cal);
····(省略不重要的)
//调试时当这个值返回了,才看到控制台输出任务触发的日志,可能是监听了这个,触发的任务执行
return ft;
}
CronTriggerImpl类的computeFirstFireTime
//org.quartz.impl.triggers.CronTriggerImpl
public Date computeFirstFireTime(org.quartz.Calendar calendar) {
//获取startTime前1秒后第一次触发时间
nextFireTime = getFireTimeAfter(new Date(getStartTime().getTime() - 1000l));
while (nextFireTime != null && calendar != null
&& !calendar.isTimeIncluded(nextFireTime.getTime())) {
nextFireTime = getFireTimeAfter(nextFireTime);
}
return nextFireTime;
}
//afterTime为new Date(getStartTime().getTime() - 1000l)
@Override
public Date getFireTimeAfter(Date afterTime) {
//不为空,跳过
if (afterTime == null) {
afterTime = new Date();
}
/**
* 这里没看懂
* 传进来的afterTime为new Date(getStartTime().getTime() - 1000l)
* 这里满足条件,又重新赋一个相同的值
* 是由于其他地方有需要这部分逻辑吗?
*/
if (getStartTime().after(afterTime)) {
afterTime = new Date(getStartTime().getTime() - 1000l);
}
//判断afterTime是否晚于endTime
if (getEndTime() != null && (afterTime.compareTo(getEndTime()) >= 0)) {
return null;
}
//进入下一个方法
Date pot = getTimeAfter(afterTime);
if (getEndTime() != null && pot != null && pot.after(getEndTime())) {
return null;
}
return pot;
}
protected Date getTimeAfter(Date afterTime) {
//getTimeAfter自己看了,就是算从afterTime开始,第一个满足cron表达式的时间
return (cronEx == null) ? null : cronEx.getTimeAfter(afterTime);
}
可以看到这里的第一次触发时间是从任务的startTime开始,以传入的cron策略开始算的第一次。所以会存在返回早于当前的时间。 如果返回了早于当前的时间,会导致任务会立马触发一次,即使当前时间不满足cron表达式。
例如:
任务的startTime为 : 2020-04-04 10:00:00
传入的cron表达式为 : “0 0/5 * * * ?” 从0分0秒开始(星期几、月、日、时任意),每5分钟执行一次
那么computeFirstFireTime计算得到的第一次触发时间为 2020-04-04 10:05:00
然后分两种情况
(1)当前时间早于这个返回的第一次触发时间 : 如2020-04-04 10:04:00,那么任务不会马上被触发
(2)当前时间晚于这个返回的第一次触发时间 : 如2020-04-04 10:29:00,那么任务会马上被触发一次,即使这个时间不满足"0 0/5 * * * ?"表达式指定的时间
所以已经过时的触发时间不需要重新触发的情况,可以通过添加withMisfireHandlingInstructionDoNothing方法避免
这个问题在github上也有人反映了,但好像没跟进?
另外角度
当然也有可能是不算是问题,因为假如当前时间为2020-04-04 10:29:00,如果想要每5分钟整执行一次,设置为"0 25/5 * * * ?"是否更合理???emmm,是不是呢?各位大佬有不同想法可以留言指点下。