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}
