引入 Quartz 依赖

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

application.yml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
spring:
# 数据源配置
datasource:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:5432/public_opinion_db?useUnicode=true&characterEncoding=UTF-8
username: postgres
password: 123456
# Quartz 定时任务配置
quartz:
# 持久化模式,任务信息存储到数据库
job-store-type: jdbc
auto-startup: true # Quartz 是否自动启动
startup-delay: 0 # 延迟 N 秒启动
wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true
overwrite-existing-jobs: false # 应用启动时不自动覆盖已存在的任务(保留运行时修改的 cron)
scheduler-name: PublicOpinionScheduler
jdbc:
initialize-schema: never # 应用启动时自动执行建表 SQL
properties:
org:
quartz:
jobStore:
class: org.springframework.scheduling.quartz.LocalDataSourceJobStore # 使用mybatis的数据源
driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
tablePrefix: quartz.QRTZ_ # 表前缀,可以配置使用哪个scheme
isClustered: true
useProperties: false
scheduler:
instanceName: PublicOpinionScheduler
instanceId: AUTO
threadPool:
threadCount: 25 # 线程池大小。默认为 10 。
threadPriority: 5 # 线程优先级
class: org.quartz.simpl.SimpleThreadPool # 线程池类型
# MyBatis-Plus 配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
mapper-locations: classpath*:/mapper/**/*.xml

初始化 Quartz 数据库表

脚本在 ~/.m2/repository/org/quartz-scheduler/quartz/2.3.2/quartz-2.3.2.jar!/org/quartz/impl/jdbcjobstore

定时任务示例

可以实现 Job:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@Component
@RequiredArgsConstructor
public class FisrtlJob implements Job {

private final XxxService xxxService;

@Override
public void execute(JobExecutionContext context) {
...
}

}

也可以继承 QuartzJobBean:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class SecondJob extends QuartzJobBean {

private String name;

public void setName(String name) {
this.name = name;
}

@Override
protected void executeInternal(JobExecutionContext context) {
System.out.println(name);
}
}

Spring 的 QuartzJobBean 支持把 JobDataMap 中的属性注入到 Job。

例如:

1
2
3
4
JobDetail jobDetail = JobBuilder
.newJob(MyJob.class)
.usingJobData("name", "test")
.build();

创建 JobDetail 和 Trigger

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Configuration
public class fisrtQuartzConfig {

@Value("${quartz.jobs.fisrt-job.cron:0 0/5 * * * ?}")
private String cron;

@Value("${quartz.jobs.fisrt-job.description:0 0/5 * * * ?}")
private String description;

@Bean
public JobDetail fisrtJobDetail() {
return JobBuilder.newJob(FisrtlJob.class)
.withIdentity(JobConstants.FIRST_JOB, JobConstants.FIRST_GROUP)
.withDescription(description)
.storeDurably() // 表示这个 JobDetail 可以独立存在,即使暂时没有 Trigger。
.build();
}

@Bean
public Trigger fisrtTrigger(JobDetail fisrtJobDetail) {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cron);
return TriggerBuilder.newTrigger()
.forJob(fisrtJobDetail)
.withIdentity(JobConstants.FISRT_JOB, JobConstants.FISRT_GROUP)
.withSchedule(scheduleBuilder)
.build();
}

}

附赠一个 “把 Quartz 的管理封装成 Service”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface QuartzTaskService {

List<QuartzJobInfoVO> listJobs();

QuartzJobInfoVO getJob(String jobName, String groupName);

void resumeJob(String jobName, String groupName);

void pauseJob(String jobName, String groupName);

void updateCron(String jobName, String groupName, String cronExpression);

void runOnce(String jobName, String groupName);

void deleteJob(String jobName, String groupName);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
@Service
@RequiredArgsConstructor
public class QuartzTaskServiceImpl implements QuartzTaskService {

private final Scheduler scheduler;

@Override
@SneakyThrows
public List<QuartzJobInfoVO> listJobs() {
List<QuartzJobInfoVO> result = new ArrayList<>();
for (String group : scheduler.getJobGroupNames()) {
for (JobKey jobKey : scheduler.getJobKeys(GroupMatcher.jobGroupEquals(group))) {
result.add(buildJobInfo(jobKey));
}
}
return result;
}

@Override
@SneakyThrows
public QuartzJobInfoVO getJob(String jobName, String groupName) {
return buildJobInfo(new JobKey(jobName, groupName));
}

@Override
@SneakyThrows
public void resumeJob(String jobName, String groupName) {
scheduler.resumeJob(JobKey.jobKey(jobName, groupName));
}

@Override
@SneakyThrows
public void pauseJob(String jobName, String groupName) {
scheduler.pauseJob(JobKey.jobKey(jobName, groupName));
}

@Override
@SneakyThrows
public void updateCron(String jobName, String groupName, String cronExpression) {
TriggerKey triggerKey = new TriggerKey(jobName, groupName);

CronTrigger oldTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
if (oldTrigger == null)
throw new IllegalArgumentException("任务不存在");

CronTrigger newTrigger = oldTrigger.getTriggerBuilder()
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)
.withMisfireHandlingInstructionDoNothing())
.build();
scheduler.rescheduleJob(triggerKey, newTrigger);
}

@Override
@SneakyThrows
public void runOnce(String jobName, String groupName) {
scheduler.triggerJob(JobKey.jobKey(jobName, groupName));
}

@Override
@SneakyThrows
public void deleteJob(String jobName, String groupName) {
scheduler.deleteJob(JobKey.jobKey(jobName, groupName));
}

private QuartzJobInfoVO buildJobInfo(JobKey jobKey) throws SchedulerException {
JobDetail detail = scheduler.getJobDetail(jobKey);
List<? extends Trigger> triggers = scheduler.getTriggersOfJob(jobKey);

QuartzJobInfoVO info = new QuartzJobInfoVO();
info.setJobName(jobKey.getName());
info.setJobGroup(jobKey.getGroup());
info.setDescription(detail != null ? detail.getDescription() : "");
info.setJobClass(detail != null ? detail.getJobClass().getSimpleName() : "");

if (!triggers.isEmpty()) {
Trigger trigger = triggers.get(0);
Trigger.TriggerState state = scheduler.getTriggerState(trigger.getKey());
info.setTriggerState(state.name());
info.setNextFireTime(trigger.getNextFireTime());
info.setPreviousFireTime(trigger.getPreviousFireTime());
if (trigger instanceof CronTrigger cronTrigger) {
info.setCronExpression(cronTrigger.getCronExpression());
}
} else {
info.setTriggerState("NO_TRIGGER");
info.setCronExpression("");
}
return info;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Data
@Schema(description = "Quartz 任务信息视图对象")
public class QuartzJobInfoVO {

@Schema(description = "任务名称")
private String jobName;

@Schema(description = "任务分组")
private String jobGroup;

@Schema(description = "任务描述")
private String description;

@Schema(description = "任务类名")
private String jobClass;

@Schema(description = "触发器状态")
private String triggerState;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "下次触发时间")
private Date nextFireTime;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "上次触发时间")
private Date previousFireTime;

@Schema(description = "Cron 表达式")
private String cronExpression;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
@Slf4j
@Tag(name = "定时任务管理", description = "基于 Quartz 的定时任务管理接口")
@RestController
@RequestMapping("/quartz")
@RequiredArgsConstructor
public class QuartzAdminController {

private final QuartzTaskService quartzTaskService;

/**
* 查询所有 Job 列表(含触发器状态和下次执行时间)
*/
@Operation(summary = "查询 Job 列表")
@GetMapping("/jobs")
public R<List<QuartzJobInfoVO>> listJobs() {
List<QuartzJobInfoVO> vos = quartzTaskService.listJobs();
return R.ok(vos);
}

/**
* 查询单个 Job 详情
*/
@Operation(summary = "查询 Job 详情")
@GetMapping("/jobs/{group}/{name}")
public R<QuartzJobInfoVO> getJob(@PathVariable String group,
@PathVariable String name) {
QuartzJobInfoVO vo = quartzTaskService.getJob(name, group);
return R.ok(vo);
}

/**
* 暂停任务
*/
@Operation(summary = "暂停任务")
@PostMapping("/jobs/{group}/{name}/pause")
public R<Void> pauseJob(@PathVariable String group,
@PathVariable String name) {
quartzTaskService.pauseJob(name, group);
return R.ok();
}

/**
* 恢复任务
*/
@Operation(summary = "恢复任务")
@PostMapping("/jobs/{group}/{name}/resume")
public R<Void> resumeJob(@PathVariable String group,
@PathVariable String name) {
quartzTaskService.resumeJob(name, group);
return R.ok();
}

/**
* 立即触发一次任务
*/
@Operation(summary = "立即触发任务")
@PostMapping("/jobs/{group}/{name}/trigger")
public R<Void> triggerJob(@PathVariable String group,
@PathVariable String name) {
quartzTaskService.runOnce(name, group);
return R.ok();
}

/**
* 更新任务的 Cron 表达式
*/
@Operation(summary = "更新 Cron 表达式")
@PutMapping("/jobs/{group}/{name}/cron")
public R<Void> updateCron(@PathVariable String group,
@PathVariable String name,
@RequestParam String cron) {
if (!CronExpression.isValidExpression(cron))
throw new IllegalArgumentException("无效的 Cron 表达式: " + cron);

quartzTaskService.updateCron(name, group, cron);
return R.ok();
}

}

参考阅读

https://www.cnblogs.com/summerday152/p/14193968.html