Springboot定时任务实现——Quartz

Springboot定时任务实现——Quartz

最近需要写一些周期性自动执行的定时任务,如果在实验室,可能会选择java自带的Timer类;但是对于公司中的项目,Timer类实现定时任务只能有一个后台线程执行任务,并且只能让程序按照某个频度执行,并不能在指定时间点执行。任务调度框架Quartz刚好满足这些需求,在使用的时候了解了这个quartz,现总结如下。

现有定时任务框架:

1)简单的有Java自带的Timer、 ScheduledExecutorService, Spring自带的Task。

2)相较复杂的分布式定时任务中间件有XXL-JOB、ElasticJob等。

选Quartz理由:

1)任务Tigger能够被持久化,这样即使在发布后,任务依然能够执行,不需要重新设定。

2)能够轻松暂停恢复触发器(即下次不会被调度)。

3)支持Calander,Cron表达式等复杂的触发器,可以灵活的编写复杂触发器。

Quartz框架三个基本”组件”和其运行原理:

Quartz中有三个基本”组件”,由它们共同来定义,运行一个定时任务:

  • JobDetail,定时任务中的“任务”;Job接口是真正需要执行的任务。JobDetail接口相当于将Job接口包装了一下,Trigger和Scheduler实际用到的都是JobDetail。
  • Trigger,定时任务中的“定时”;通过cron表达式或是SimpleScheduleBuilder等类,指定任务执行的周期。
  • Scheduler,定时任务的调度器(组装器);Quartz通过调度器来注册、暂停、删除Trigger和JobDetail。

Quartz通过Cron定义Quartz的调度时间Trigger(例如 0 0 12 ? * WED 表示“每周三上午12:00”)。此外,时间表也可以通过 SimpleTrigger ,由 Date 定义触发的开始时间、毫秒的时间间隔和重复计数(例如“在下周三12:00,然后每隔10秒、执行5次”)。框架具体代码逻辑如下图所示:

创建并启动一个定时任务的正常流程是:创建任务类 ——> 创建JobDetail ——> 创建Trigger,

具体代码实现:

  • 引入springboot官方启动器
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
  • 创建任务
  1. 在Quartz中创建的所有定时任务都要实现Job接口,但是在SpringBoot中所有的定时任务只要继承QuartzJobBean类即可。
  2. QuartzJobBean是一个抽象类,实现了Quartz的Job接口。
  3. 与Thread的run()方法类似,定时任务的具体实现写在executeInternal()方法中。
  4. 每创建一个新的定时任务,都需要新建一个Java类并继承QuartzJobBean、实现executeInternal()。
@Component
public class FlowRestartJob extends QuartzJobBean {
    private final Logger logger = LoggerFactory.getLogger(FlowRestartJob.class);
    @Autowired
    private RestartFlowHandler restartFlowHandler;
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        try {
            restartFlowHandler.execute();  // 具体任务
        } catch (Exception e) {
            logger.error("任务失败 error:{}", e.getMessage());
}
  • 提供 Quartz 相关配置 Bean,创建JobDetail和Trigger

使用建造者模式的JobBuilder来创建一个JobDetail对象。

JobDetail simpleJob = JobBuilder.newJob(SimpleJob.class)        //传入一个Job类
                                .withIdentity("SimpleJob", "AnchorJobs")    //(name, group)标识唯一一个JobDetail
                                .storeDurably()        //在没有Trigger关联的情况下保存该任务到调度器
                                .build();
  1. newJob()中传入的Job类必须是继承了QuartzJobBean的类。
  2. withIdentity()中group可不传,不传时默认设为”DEFAULT”。
  3. storeDurably()使JobDetail可在没有关联Trigger的情况下添加到调度器中,否则会抛异常。建议调用此方法。

常用Trigger有两种:SimpleTrigger和CronTrigger。二者最大的区别是CronTrigger支持Cron表达式。创建CronTrigger:

CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");    //Cron表达式,每5秒执行一次
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
                                    .withIdentity("CronJob", "AnchorTriggers")    //(name, group)唯一标识一个Trigger
                                    .startNow()                         //调用scheduler.scheduleJob()后立即开始执行定时任务
                                    .withSchedule(scheduleBuilder)      //不同的scheduleBuilder
                                    .build();

具体代码实现:

@Configuration
public class QuartzConfig {
    @Value("${quartz.restartCron}")
    private String restartCron; // corn表达式
    @Bean
    public JobDetail restartJob() {
        return JobBuilder.newJob(FlowRestartJob.class).withIdentity("FlowRestartJob").storeDurably().build();
    @Bean
    public Trigger restartTrigger() {
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(restartCron);
        return TriggerBuilder.newTrigger().forJob(restartJob())
                .withIdentity("FlowRestartJob").withSchedule(scheduleBuilder).build();
}

一些框架思考:

1. Quartz和Springboot一起使用时为什么不用创建Scheduler?

SpringBoot官方写了 spring-boot-starter-quartz,这是一个官方提供的启动器,有了这个启动器,集成的操作就会被大大简化。SpingBoot2.2.6官方文档,其中第4.20小节 Quartz Scheduler 就谈到了Quartz。

原文如下:

Spring Boot offers several conveniences for working with the Quartz scheduler, including the
spring-boot-starter-quartz “Starter”. If Quartz is available, a Scheduler is auto-configured (through the SchedulerFactoryBean abstraction).
Beans of the following types are automatically picked up and associated with the Scheduler:
• JobDetail: defines a particular Job. JobDetail instances can be built with the JobBuilder API.
• Calendar.
• Trigger: defines when a particular job is triggered.

即:SpringBoot提供了一些便捷的方法来和Quartz协同工作,这些方法里面包括`spring-boot-starter-quartz`这个启动器。

如果Quartz可用,Scheduler会通过SchedulerFactoryBean这个工厂bean自动配置到SpringBoot里。JobDetail、Calendar、Trigger这些类型的bean会被自动采集并关联到Scheduler上。 Job可以定义setter(也就是set方法)来注入配置信息。也可以用同样的方法注入普通的bean。

2. 如何注册无周期性定时任务?某些任务不是周期性的,只存在执行时间,但仍然是定时触发(例如某些游戏活动只在特定时刻发生一次,之后就不再产生)。想要完成这类任务必须完成以下两点:

  1. Job类需要获取到一些数据用于任务的执行。
  2. 任务执行完成后删除Job和Trigger。

具体代码实现:

  • 创建任务
@Component
public class RestartJob extends QuartzJobBean {
    private final Logger logger = LoggerFactory.getLogger(FlowRestartJob.class);
    private Scheduler scheduler;
    private SystemUserMapperPlus systemUserMapperPlus;
    @Autowired
    public RestartJob(Scheduler scheduler, SystemUserMapperPlus systemUserMapperPlus) {
        this.scheduler = scheduler;
        this.systemUserMapperPlus = systemUserMapperPlus;
    @Autowired
    private RestartFlowHandler restartFlowHandler;
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        Trigger trigger = jobExecutionContext.getTrigger();
        // 将添加任务的时候存进去的数据拿出来
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        JobDataMap jobDataMap = jobDetail.getJobDataMap();
        long username = jobDataMap.getLongValue("username");
        LocalDateTime time = LocalDateTime.parse(jobDataMap.getString("time"));
        try {
          // 具体任务逻辑
            restartFlowHandler.execute();  
      // 执行之后删除任务      
            scheduler.pauseTrigger(trigger.getKey()); // 暂停触发器的计时
            scheduler.unscheduleJob(trigger.getKey()); // 移除触发器中的任务
            scheduler.deleteJob(jobDetail.getKey()); // 删除任务
        } catch (Exception e) {
            logger.error("任务失败 error:{}", e.getMessage());
}
  • service层逻辑
@Service
public class LeaveApplicationServiceImpl implements LeaveApplicationService {
    @Autowired
    private Scheduler scheduler;
    // 添加job和trigger到scheduler
    private void addJobAndTrigger(LeaveApplication leaveApplication) {
        // 创建请假开始Job
        Long proposerUsername = leaveApplication.getProposerUsername();
        LocalDateTime startTime = leaveApplication.getStartTime();
        JobDetail startJobDetail = JobBuilder.newJob(RestartJob.class)
                // 指定任务组名和任务名
                .withIdentity(leaveApplication.getStartTime().toString(), proposerUsername + "_start")
                // 添加一些参数,执行的时候用于取出
                .usingJobData("username", proposerUsername)
                .usingJobData("time", startTime.toString())
                .build();
        // 创建请假开始任务的触发器
        // 创建cron表达式指定任务执行的时间,由于请假时间是确定的,所以年月日时分秒都是确定的,这也符合任务只执行一次的要求。
        String startCron = String.format("%d %d %d %d %d ? %d",
                startTime.getSecond(),
                startTime.getMinute(),
                startTime.getHour(),
                startTime.getDayOfMonth(),
                startTime.getMonth().getValue(),
                startTime.getYear());
        CronTrigger startCronTrigger = TriggerBuilder.newTrigger()
                // 指定触发器组名和触发器名
                .withIdentity(leaveApplication.getStartTime().toString(), proposerUsername + "_start")
                .withSchedule(CronScheduleBuilder.cronSchedule(startCron))
                .build();
        // 将job和trigger添加到scheduler里
        try {
            scheduler.scheduleJob(startJobDetail, startCronTrigger);