quartz使用

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,是不是呢?各位大佬有不同想法可以留言指点下。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值