Spring Boot 定时任务:@Scheduled 与 Quartz

Spring 项目中经常用到定时任务。简单的场景用 Spring 自带的 @Scheduled 就够了;需要持久化、动态管理任务(增删改查、错过触发补偿)时再上 Quartz。

@Scheduled 定时任务

 1@Component
 2public class ScheduledTest {
 3    /**
 4     * 定时任务
 5     */
 6    @Scheduled(cron = "")
 7    public void scheduledTest() {
 8        System.out.println("定时开始 " + new Date());
 9    }
10}

启用定时任务

在启动类上添加 @EnableScheduling 开启定时任务。

cron 表达式

cron 表达式是一个字符串,分为 6 或 7 个域,每两个域之间用空格分隔,其语法格式为:

秒域 分域 时域 日域 月域 周域 [年域]

其中年域可以省略,省略时表示每年。

常用 cron 表达式:

说明 表达式
每隔 5 秒执行一次 */5 * * * * ?
每隔 1 分钟执行一次 0 */1 * * * ?
每天 23 点执行一次 0 0 23 * * ?
每天凌晨 1 点执行一次 0 0 1 * * ?
每月 1 号凌晨 1 点执行一次 0 0 1 1 * ?
每月最后一天 23 点执行一次 0 0 23 L * ?
每周星期天凌晨 1 点执行一次 0 0 1 ? * L
在 26 分、29 分、33 分执行一次 0 26,29,33 * * * ?
每天的 0 点、13 点、18 点、21 点各执行一次 0 0 0,13,18,21 * * ?

cron 表达式在线生成:https://www.bejson.com/othertools/cron/

Quartz

介绍

Quartz 是一个完全由 Java 编写的开源作业调度框架,支持把任务信息持久化到数据库、动态增删改任务、错过触发后的补偿策略(misfire)等,比 @Scheduled 更重量级也更灵活。

基本组成部分

  • 调度器:Scheduler
  • 任务:JobDetail
  • 触发器:Trigger,包括 SimpleTrigger 和 CronTrigger

Spring Boot 集成

添加依赖:

1<dependency>
2    <groupId>org.springframework.boot</groupId>
3    <artifactId>spring-boot-starter-quartz</artifactId>
4</dependency>

创建 Job

 1import org.quartz.Job;
 2import org.quartz.JobExecutionContext;
 3import org.quartz.JobExecutionException;
 4
 5public class HelloJob implements Job {
 6    @Override
 7    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
 8        System.out.println("hello job");
 9    }
10}

创建 Scheduler

 1import org.quartz.*;
 2import org.quartz.impl.StdSchedulerFactory;
 3
 4import java.util.concurrent.TimeUnit;
 5
 6public class MyQuartz {
 7    public static void main(String[] args) throws SchedulerException, InterruptedException {
 8        // 1、创建调度器 Scheduler
 9        SchedulerFactory schedulerFactory = new StdSchedulerFactory();
10        Scheduler scheduler = schedulerFactory.getScheduler();
11        // 2、创建 JobDetail 实例
12        JobDetail jobDetail = JobBuilder.newJob(HelloJob.class).withIdentity("job1", "group1").build();
13
14        // 3、构建 Trigger 实例,每隔 1s 执行一次
15        Trigger trigger = TriggerBuilder.newTrigger().withIdentity("trigger1", "triggerGroup1")
16                .startNow()
17                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
18                        // 每隔 1s 执行一次
19                        .withIntervalInSeconds(1)
20                        // 一直执行
21                        .repeatForever()).build();
22        // 4、执行
23        scheduler.scheduleJob(jobDetail, trigger);
24        System.out.println("--------scheduler start ! ------------");
25        scheduler.start();
26        // 睡眠
27        TimeUnit.MINUTES.sleep(1);
28        scheduler.shutdown();
29        System.out.println("--------scheduler shutdown ! ------------");
30    }
31}

持久化到数据库的配置

 1server:
 2  port: 8080
 3spring:
 4  datasource:
 5    url: jdbc:mysql://127.0.0.1:3306/quartz?useUnicode=true&characterEncoding=UTF-8&useSSL=false&autoReconnect=true&failOverReadOnly=false&serverTimezone=GMT%2B8
 6    username: root
 7    password: 123456
 8    driver-class-name: com.mysql.cj.jdbc.Driver
 9    type: com.zaxxer.hikari.HikariDataSource
10    hikari:
11      minimum-idle: 5
12      connection-test-query: SELECT 1 FROM DUAL
13      maximum-pool-size: 20
14      auto-commit: true
15      idle-timeout: 30000
16      pool-name: SpringBootDemoHikariCP
17      max-lifetime: 60000
18      connection-timeout: 30000
19  quartz:
20    # 参见 org.springframework.boot.autoconfigure.quartz.QuartzProperties
21    job-store-type: jdbc
22    wait-for-jobs-to-complete-on-shutdown: true
23    scheduler-name: SpringBootDemoScheduler
24    properties:
25      org.quartz.threadPool.threadCount: 5
26      org.quartz.threadPool.threadPriority: 5
27      org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
28      org.quartz.jobStore.misfireThreshold: 5000
29      org.quartz.jobStore.class: org.quartz.impl.jdbcjobstore.JobStoreTX
30      org.quartz.jobStore.driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
31      # 在调度流程的第一步(拉取即将触发的 triggers)时上锁,不会出现多个线程拉取到相同 trigger 的情况,避免重复调度。参考:https://segmentfault.com/a/1190000015492260
32      org.quartz.jobStore.acquireTriggersWithinLock: true
33logging:
34  level:
35    com.example.demo: debug
36    org.quartz: debug

job-store-type: jdbc 后,任务和触发器信息都会存到 quartz 系列表里,应用重启任务不丢。

动态管理任务

通过 Scheduler API 可以在运行期增删改查任务:

  1@Service
  2@Slf4j
  3public class JobServiceImpl implements JobService {
  4    private final Scheduler scheduler;
  5    private final JobMapper jobMapper;
  6
  7    @Autowired
  8    public JobServiceImpl(Scheduler scheduler, JobMapper jobMapper) {
  9        this.scheduler = scheduler;
 10        this.jobMapper = jobMapper;
 11    }
 12
 13    /**
 14     * 添加并启动定时任务
 15     *
 16     * @param form 表单参数 {@link JobForm}
 17     * @throws Exception 异常
 18     */
 19    @Override
 20    public void addJob(JobForm form) throws Exception {
 21        // 启动调度器
 22        scheduler.start();
 23
 24        // 构建 Job 信息
 25        JobDetail jobDetail = JobBuilder.newJob(JobUtil.getClass(form.getJobClassName()).getClass()).withIdentity(form.getJobClassName(), form.getJobGroupName()).build();
 26
 27        // Cron 表达式调度构建器(即任务执行的时间)
 28        CronScheduleBuilder cron = CronScheduleBuilder.cronSchedule(form.getCronExpression());
 29
 30        // 根据 Cron 表达式构建一个 Trigger
 31        CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(form.getJobClassName(), form.getJobGroupName()).withSchedule(cron).build();
 32
 33        try {
 34            scheduler.scheduleJob(jobDetail, trigger);
 35        } catch (SchedulerException e) {
 36            log.error("【定时任务】创建失败!", e);
 37            throw new Exception("【定时任务】创建失败!");
 38        }
 39    }
 40
 41    /**
 42     * 删除定时任务
 43     *
 44     * @param form 表单参数 {@link JobForm}
 45     * @throws SchedulerException 异常
 46     */
 47    @Override
 48    public void deleteJob(JobForm form) throws SchedulerException {
 49        scheduler.pauseTrigger(TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName()));
 50        scheduler.unscheduleJob(TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName()));
 51        scheduler.deleteJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));
 52    }
 53
 54    /**
 55     * 暂停定时任务
 56     *
 57     * @param form 表单参数 {@link JobForm}
 58     * @throws SchedulerException 异常
 59     */
 60    @Override
 61    public void pauseJob(JobForm form) throws SchedulerException {
 62        scheduler.pauseJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));
 63    }
 64
 65    /**
 66     * 恢复定时任务
 67     *
 68     * @param form 表单参数 {@link JobForm}
 69     * @throws SchedulerException 异常
 70     */
 71    @Override
 72    public void resumeJob(JobForm form) throws SchedulerException {
 73        scheduler.resumeJob(JobKey.jobKey(form.getJobClassName(), form.getJobGroupName()));
 74    }
 75
 76    /**
 77     * 重新配置定时任务
 78     *
 79     * @param form 表单参数 {@link JobForm}
 80     * @throws Exception 异常
 81     */
 82    @Override
 83    public void cronJob(JobForm form) throws Exception {
 84        try {
 85            TriggerKey triggerKey = TriggerKey.triggerKey(form.getJobClassName(), form.getJobGroupName());
 86            // 表达式调度构建器
 87            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(form.getCronExpression());
 88
 89            CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
 90
 91            // 根据 Cron 表达式重新构建 Trigger
 92            trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
 93
 94            // 按新的 trigger 重新设置 job 执行
 95            scheduler.rescheduleJob(triggerKey, trigger);
 96        } catch (SchedulerException e) {
 97            log.error("【定时任务】更新失败!", e);
 98            throw new Exception("【定时任务】创建失败!");
 99        }
100    }
101
102    /**
103     * 查询定时任务列表
104     *
105     * @param currentPage 当前页
106     * @param pageSize    每页条数
107     * @return 定时任务列表
108     */
109    @Override
110    public PageInfo<JobAndTrigger> list(Integer currentPage, Integer pageSize) {
111        PageHelper.startPage(currentPage, pageSize);
112        List<JobAndTrigger> list = jobMapper.list();
113        return new PageInfo<>(list);
114    }
115}