在Springboot中整合Quartz实现简单定时任务


  在开发过程中有时候会遇到需要定期执行某项任务(如定期向某个外部服务请求回执)。
  在Springboot已经提供了简单的实现(@EnableScheduling@Scheduled),但是使用这种方式实现难以做到可控的任务暂停恢复,因此借助Quartz来实现支持更加复杂行为的定时任务。
  你可以直接引入Quartz,但如果你希望在Springboot的项目中引用Quartz,则可以引用如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    <version>${spring-boot-quartz-version}</version>
</dependency>

       本文中实现一个简单的Springboot应用,这个应用每分钟向控制台输出一段包含当前系统时间的字符串(充当真实项目中需要定时执行的业务逻辑)。在此之上,可以通过web接口控制该任务的暂停和恢复。

创建项目所使用的Spring boot:

  • Spring boot 2.4.9

一、创建空的Springboot项目(略)

       在这一步中,可以选择集成接下来要使用的spring-boot-starter-webspring-boot-starter-quartz

二、创建Job

       在Quartz中,创建一个Job时,需要定时执行的业务逻辑被包含在实现了Job的JobBean中,与此Job相关的一些信息则配置在一个JobDetail的Bean中。用户需要继承QuartzJobBean,并实现其中的executeInternal方法。Scheduler每次触发任务时,都会在一个新的线程实例化这个Bean并调用QuartzJobBeanexecute方法。executeInternal则在execute中被调用。

public class TimelyInvokedTask extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        System.out.println("TestQuartz----Timely:" + DateUtil.now());
    }
}

       JobDetail是一个配置Bean,需要被Spring管理起来。新建一个用于配置Quartz的Bean QuartzConfiguration,在其中编写一个方法,利用JobBuilder构造一个JobDetail

@Configuration
public class QuartzConfiguration {
    public static final String TIMELY_JOB_KEY = "timelyTask";

    @Bean("timelyJob")
    public JobDetail getTimelyJob(){
        return JobBuilder.newJob(TimelyInvokedTask.class)
                .withIdentity(TIMELY_JOB_KEY)
                .storeDurably().build();
    }
}

       withIdentity(String)配置这个Job的身份,将来需要对这个Job进行管理时,就可以通过这个名字找到它;storeDurably(boolean = true)表示当没有Trigger与之绑定时,该任务是否还被保存在上下文中,这个值默认为false,这个方法将其设置为true

三、Schedule

       顾名思义,Schedule表示一个任务被执行的计划。根据不同的需求,Quartz也提供不同种类的Schedule,对于一些间隔时间较短的任务,以下的两种便可满足绝大多数的需求。

3.1 SimpleSchedule

       这类计划支持每隔固定的系统时间触发有限或无限次数的任务。Schedule同样有相应的Builder来构造它,使用SimpleScheduleBuilder可以构造出SimpleSchedule
       在其中已经预置了数个模式,通过方法名称就可以辨识,例如repeatMinutelyForever即表示“每分钟重复触发,没有次数上限”,当给定参数时,可以指定多少分钟触发一次;repeatHourlyForTotalCount表示“每小时触发,最多触发count次”,同样也可以设置间隔。除了这些方法外,使用with...开头的方法可以单独指定其中的某一个参数,以及misfire时采取的行为(这个将在后文解释)。
       我们默认这些任务将会立即开始,现使用如下的方法构造一个简单计划:

ScheduleBuilder<SimpleTrigger> scheduleBuilder = SimpleScheduleBuilder.repeatMinutelyForever();

       先预览一下这样配置的任务将会以何种方式进行:

简单计划运行预览

       可以看到它确实在以分钟为单位执行,由于计划器(Scheduler)初始化时正好在40s的时刻上,因此之后的所有任务也都在每分钟的第40s时触发了。然而在真实业务逻辑中,往往需要在“整点”或每日0点等特别的时刻触发任务,我们当然不可能确保应用启动且Scheduler初始化完成时正好处于整点,你当然可以通过获得系统当前时间计算出下一次触发的时间并配置在扳机(Trigger)中,但这样过于复杂,还有更好的实现方法。

3.2 CronSchedule

      CronSchedule是一类通过cron表达式配置任务触发时机的计划。cron表达式非常灵活,甚至可以表达类似“在每个工作日的上午8点到下午5点的每个半点触发”这样非常复杂的逻辑。关于cron表达式,可以参考下面的文章:

【转】cron表达式详解

       使用cron表达式配置CronSchedule通过CronScheduleBuilder完成。

ScheduleBuilder<CronTrigger> scheduleBuilder = CronScheduleBuilder.cronSchedule("0 * * * * ? *");

       上述的cron表达式的含义是,对于任意的(*)年、月、日、小时、分钟,且不论(?)今天是周几,每逢秒为0(也就是在每分钟的00秒),就触发一次计划任务。通过这样配置出来的效果如下:

时间计划运行预览

四、Trigger

       在说Trigger之前,先小结一下目前使用到的Quartz的组件。Job是需要定时触发的任务,它包含的是业务逻辑;Schedule是计划,它表示任务应当以何种周期或规律执行。但是在配置计划时,并没有绑定任务,可以看到一个Schedule其实可以应用于多个Job;而一个Job应该也可以按照不同的Schedule混合触发,真正将这两者关系起来的就是Trigger
       Trigger也由一个Builder构造:

@Configuration
public class QuartzConfiguration {
    public static final String TIMELY_JOB_KEY = "timelyTask";
    public static final String TIMELY_TRIGGER_KEY = "timelyTrigger";

    //other code

    @Bean
    public Trigger getTimelyTrigger(){
        ScheduleBuilder<CronTrigger> scheduleBuilder = CronScheduleBuilder.cronSchedule("0 * * * * ? *");
        return TriggerBuilder.newTrigger().forJob(TIMELY_JOB_KEY)
                .withSchedule(scheduleBuilder)
                .withIdentity(TIMELY_TRIGGER_KEY)
                .startNow().build();
    }
}

       在构造Trigger时,通过配置在Job上的identity绑定一个任务,然后将ScheduleBuilder的对象绑定上。Trigger也有一个identity,在本例中并没有作用,只需要注意其区别于Job的identity即可。最后startNow()startAt(Date)表示这个Trigger最近一次生效的时间,当使用startAt(Date)时,在规定的时间之前不会触发定时任务。
       以上JobScheduleTrigger就是Quartz最基本的3个组成部分,只要完成这3者的配置,定时任务就已经可以执行了。效果如3.2中的图所示。

五、暂停/恢复/触发定时任务

5.1 Scheduler

       SchedulerQuartz中的“管理员”,能够调整和管理JobScheduleTrigger的状态和行为。当我们需要暂停、恢复或触发定时任务时,管理的对象主要是Job
  Scheduler是通过对象工厂创建的,运行在Spring boot中的Quartz在应用启动时就实例化了一个SchedulerFactoryBean,可以通过这个Bean来获得一个Scheduler。一般来说也无需多个Scheduler,因此也可以将其作为一个Bean交给Spring管理。

@Configuration
public class QuartzConfiguration {
    //other code

    @Bean("testScheduler")
    public Scheduler getScheduler(SchedulerFactoryBean schedulerFactoryBean){
        return schedulerFactoryBean.getScheduler();
    }
}

       编写一个Service,在其中注入SchedulerScheduler通过JobKey提供对任务的暂停(pause)、恢复(resume)和触发(trigger)方法,在Service中分别包装这个3个方法,然后在Controller中注入并调用即可。

@Service("quartzTestService")
@Setter(onMethod_={@Autowired})
class QuartzTestServiceImpl implements QuartzTestService {
    private Scheduler testScheduler;

    @Override
    public void pauseTimelyTask() {
        JobKey key = new JobKey(QuartzConfiguration.TIMELY_JOB_KEY);
        try {
            testScheduler.pauseJob(key);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void resumeTimelyTask() {
        JobKey key = new JobKey(QuartzConfiguration.TIMELY_JOB_KEY);
        try {
            testScheduler.resumeJob(key);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void forceRunTask() {
        JobKey key = new JobKey(QuartzConfiguration.TIMELY_JOB_KEY);
        try {
            testScheduler.triggerJob(key);
        } catch (SchedulerException e) {
            e.printStackTrace();
        }
    }
}

       下面是我们分别通过web请求一次调用pause、resume和trigger方法的运行效果:

  • 暂停:2021-08-10 15:58:00
  • 恢复:2021-08-10 15:59:59
  • 触发:2021-08-10 16:00:43

运行效果图

5.2 Misfire处理

       在5.1的图中,可以发现在暂停的任务恢复的瞬间,任务触发了一次,随后才按照正常的节奏继续执行。这就是Quartz的Misfire的处理机制之一。每当到了计划配置的时刻,任务触发,这个行为在Quartz中用“fire”(开火)表示,因此Trigger既可以理解为“触发器”,又可以理解为“扳机”。但是如果由于各种原因(例如暂停),而导致本应触发的任务没有触发,这就称为“misfire”。
       当本应执行的任务没有按时执行,在业务逻辑上来说往往需要进行补救(recovery),而针对CronScheduleQuartz也提供了3种策略来处理misfires。

5.2.1 Misfire_Handling_Instruction_Do_Nothing

       当发现有Misfires时不作任何补救,直到下一次触发任务时正常执行。采用这种方法也就是忽略了没有按期执行的任务。

    @Bean("timelyJobTrigger")
    public Trigger getTimelyTrigger(){
        ScheduleBuilder<CronTrigger> scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *")
                .withMisfireHandlingInstructionDoNothing();
        return TriggerBuilder.newTrigger().forJob(TIMELY_JOB_KEY)
                .withSchedule(scheduleBuilder)
                .withIdentity(TIMELY_TRIGGER_KEY)
                .startNow().build();
    }

       为了便于测试,将任务的运行间隔改为10s。

在这里插入图片描述

5.2.2 Misfire_Handling_Instruction_Fire_And_Proceed

       当发现有Misfire时,立即补发一次,然后按照预定的节奏正常执行以后的任务。这也是默认的策略。

    @Bean
    public Trigger getTimelyTrigger(){
        ScheduleBuilder<CronTrigger> scheduleBuilder = CronScheduleBuilder.cronSchedule("0/10 * * * * ? *")
                .withMisfireHandlingInstructionFireAndProceed();
        return TriggerBuilder.newTrigger().forJob(TIMELY_JOB_KEY)
                .withSchedule(scheduleBuilder)
                .withIdentity(TIMELY_TRIGGER_KEY)
                .startNow().build();
    }

       测试截图略(见5.1图)。

5.2.3 Misfire_Handling_Instruction_Ignore_Misfires

       从名称上看,似乎与DoNothing很像,但此处忽略的其实是Misfire Handling Instruction,也就是说Trigger将立即将手头堆积的任务(Misfires)立即发射出去,应发尽发。在说明文档中举了一个例子,如果你的任务每15秒触发一次,而任务暂停了5分钟的话,再次启动时,这个任务将会连续触发20次。

    @Bean("contextJobTrigger")
    public Trigger getContextTestTrigger(){
        ScheduleBuilder<CronTrigger> scheduleBuilder = CronScheduleBuilder.cronSchedule("1 * * * * ? *")
                .withMisfireHandlingInstructionDoNothing();
        return TriggerBuilder.newTrigger().forJob(CONTEXT_JOB_KEY)
                .withSchedule(scheduleBuilder)
                .withIdentity(CONTEXT_TRIGGER_KEY)
                .usingJobData(TRIGGER_TEST_KEY, "context aware!")
                .startNow().build();
    }

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值