SpringBoot2.5.x整合Quartz并持久化

本文最后更新于:2 天前

风风火火,恍恍惚惚,springboot2.6.0正式版也发布了,同时也宣布了2.4x版本停止维护。2.5.x系列也是目前自己写Demo在用的,为什么要记录下来呢, 主要我把项目升级到2.6之后项目跑不起来了,原因我知道,但是我不知道怎么解决,看了看机翻的更新文档,有处可能是影响我项目跑不起来的原因,就是限制了依赖的循环导入,现在依赖循环导入,项目是会直接跑不起来。我的问题是项目里获取不到DataSource数据源,我用了各种姿势,都获取不到,靓仔在线疑惑。既然不能无缝升级,那我就记下来一个正常的版本。

定时任务是很常用的一个功能,比如简单的生日提醒,每天定点查询用户的生日信息。然后发出一系列的提示操作。不了解你就感觉很难,但是现在spring给我们带来了很多的便利,看一看,在头脑风暴一下,也不难。编程一途,殊途同归。

主要工作中好多同步代码感觉都可以使用定时任务来处理,然而实际上用的是一个一个的Main方法,吐槽吧,人家那代码又不是不能跑,不仅能跑,还能赚钱。没啥好吐槽的,就是维护时想骂娘,骂娘就骂娘,还十分容易出错。

核心配置类

用了这个配置类,腰不酸了,腿不痛了,一次能爬五层楼。觉也睡的香了。好吧,前面纯属扯淡,写了这个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
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
package com.wangijun.springbootdemo.config;

import ...;

/**
* quartz配置类
*/
@Configuration
public class QuartzConfig {

/**
* 此种注入方式,在springboot2.6中获取不到,
*/
@Resource
private DataSource dataSource;

/**
* Describe: 定时任务工厂
* Param: DataSource
* Return: ScheduleFactoryBean
* */
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
Properties prop = new Properties();
// QlScheduler这个值可以自定义
prop.put("org.quartz.scheduler.instanceName", "QlScheduler");
// 设置为AUTO时使用,默认的实现org.quartz.scheduler.SimpleInstanceGenerator是基于主机名称和时间戳生成。
prop.put("org.quartz.scheduler.instanceId", "AUTO");
// 线程池相关配置
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "20");
prop.put("org.quartz.threadPool.threadPriority", "5");
// JobStoreTX在每次执行任务后都使用commit或者rollback来提交更改。
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
// 集群配置:如果有多个调度器实体的话则必须设置为true,如果项目部署了多个,就设置为true
prop.put("org.quartz.jobStore.isClustered", "false");
// 集群配置:检查集群下的其他调度器实体的时间间隔
prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000");
// 设置一个频度(毫秒),用于实例报告给集群中的其他实例
prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "5000");
// 触发器触发失败后再次触犯的时间间隔
prop.put("org.quartz.jobStore.misfireThreshold", "12000");

// 数据库表前缀 重点这个和建表SQL是对应的关系,如果改了需要改动建表语句

prop.put("org.quartz.jobStore.tablePrefix", "schedule_");
// 从 LOCKS 表查询一行并对这行记录加锁的 SQL 语句
prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?");

SchedulerFactoryBean factory = new SchedulerFactoryBean();
// 数据源
factory.setDataSource(dataSource);
// 上面的配置
factory.setQuartzProperties(prop);
// 可自定义
factory.setSchedulerName("QlScheduler");
// 项目启动后30秒后启动开始执行定时任务
factory.setStartupDelay(30);
factory.setApplicationContextSchedulerContextKey("applicationContextKey");
factory.setOverwriteExistingJobs(true);
// 自动启动
factory.setAutoStartup(true);
return factory;
}
}

此种配置的Quartz是持久化到数据库的,就是项目重启定时任务的状态并不会丢失。

用于Web管理Bean

可以自定义,要点看代码注释。

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
package com.wangijun.springbootdemo.pojo;

import ...;

/**
* 定时任务
*/
@Data
@TableName(value = "schedule_job")
public class ScheduleJob implements Serializable { // 必须序列化

// 必须要有
public static final String JOB_PARAM_KEY = "JOB_PARAM_KEY";

/**
* 任务id
*/
@TableId(value = "id", type = IdType.ASSIGN_ID)
private String id;

/**
* spring bean名称
*/
@TableField(value = "bean_name")
private String beanName;

/**
* 参数
*/
@TableField(value = "params")
private String params;

/**
* cron表达式
*/
@TableField(value = "cron_expression")
private String cronExpression;

/**
* 任务状态 0:正常 1:暂停
*/
@TableField(value = "`status`")
private Byte status;

/**
* 备注
*/
@TableField(value = "remark")
private String remark;

/**
* 创建时间
*/
@TableField(value = "create_time")
private LocalDateTime createTime;

@TableField(value = "job_name")
private String jobName;

private static final long serialVersionUID = 1L; // 必须要有
}

定时任务执行日志Bean

主要就是记录一下日志,可根据自己需求,自行扩展。

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
package com.wangijun.qluiserver.pojo;

import ...;

/**
* 定时任务日志
*/
@Data
@TableName(value = "schedule_log")
public class ScheduleLog implements Serializable {
/**
* 任务日志id
*/
@TableId(value = "id", type = IdType.INPUT)
private String id;

/**
* 任务id
*/
@TableField(value = "job_id")
private String jobId;

@TableField(value = "job_name")
private String jobName;

/**
* 参数
*/
@TableField(value = "params")
private String params;

/**
* 任务状态 0:成功 1:失败
*/
@TableField(value = "`status`")
private String status;

/**
* 失败信息
*/
@TableField(value = "error")
private String error;

/**
* 耗时(单位:毫秒)
*/
@TableField(value = "times")
private Integer times;

/**
* 创建时间
*/
@TableField(value = "create_time")
private LocalDateTime createTime;

/**
* spring bean名称
*/
@TableField(value = "bean_name")
private String beanName;

private static final long serialVersionUID = 1L;
}

执行定时任务的上下文

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
package com.wangijun.springbootdemo.schedule;

import ...;

/**
* Describe: 定时任务执行上下文
* Author: 就免仪式
* CreateTime: 2019/10/23
* */
@Slf4j
public class ScheduleContext extends QuartzJobBean {

/**
* Describe: 执行任务并记录日志
* Param: JobExecutionContext
* Return: 无返回值
* */
@Override
protected void executeInternal(JobExecutionContext context) {
Object o = context.getMergedJobDataMap().get(ScheduleJob.JOB_PARAM_KEY);
ScheduleJob jobBean = (ScheduleJob) o;
ScheduleLogService scheduleJobLogService = (ScheduleLogService) SpringUtil.getBean("scheduleLogService");
ScheduleLog logBean = new ScheduleLog() ;
logBean.setId(IdWorker.getIdStr());
logBean.setJobId(jobBean.getId());
logBean.setJobName(jobBean.getJobName());
logBean.setBeanName(jobBean.getBeanName());
logBean.setParams(jobBean.getParams());
logBean.setCreateTime(LocalDateTime.now());
long beginTime = System.currentTimeMillis() ;
try {
Object target = SpringUtil.getBean(jobBean.getBeanName());
Method method = target.getClass().getDeclaredMethod("run", String.class);
method.invoke(target, jobBean.getParams());
long executeTime = System.currentTimeMillis() - beginTime;
logBean.setTimes((int) executeTime);
logBean.setStatus("0");
log.info("定时器 === >> " + jobBean.getJobName() + "执行成功,耗时 === >> " + executeTime);
} catch (Exception e){
long executeTime = System.currentTimeMillis() - beginTime;
logBean.setTimes((int)executeTime);
logBean.setStatus("1");
logBean.setError(e.getCause().getMessage());
e.getCause().printStackTrace();
} finally {
scheduleJobLogService.save(logBean);
}
}
}

定时任务处理器

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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package com.wangijun.springbootdemo.schedule;


import ...;

/**
* Describe: 定时任务处理器
* Author: 就免仪式
* CreateTime: 2019/10/23
* */
@NoArgsConstructor
public class ScheduleHandler {

/**
* 定时任务标识 Key,
* */
private static final String SCHEDULE_NAME = "Ql_" ;

/**
* 触发器 KEY
*/
public static TriggerKey getTriggerKey(Long jobId){
return TriggerKey.triggerKey(SCHEDULE_NAME + jobId) ;
}

/**
* 定时器 Key
*/
public static JobKey getJobKey (Long jobId){
return JobKey.jobKey(SCHEDULE_NAME+jobId) ;
}

/**
* 表达式触发器
*/
public static CronTrigger getCronTrigger (Scheduler scheduler, Long jobId){
try {
return (CronTrigger)scheduler.getTrigger(getTriggerKey(jobId)) ;
} catch (SchedulerException e){
return null;
}
}

/**
* Describe: 创建定时任务
* Param: Scheduler ScheduleJobBean
* Return: null
* */
public static void createJob (Scheduler scheduler, ScheduleJob scheduleJob){
try {
// 构建定时器
JobDetail jobDetail = JobBuilder.newJob(ScheduleContext.class).withIdentity(getJobKey(Long.parseLong(scheduleJob.getId()))).build() ;
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder
.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing() ;
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(getTriggerKey(Long.parseLong(scheduleJob.getId())))
.withSchedule(scheduleBuilder).build();
jobDetail.getJobDataMap().put(ScheduleJob.JOB_PARAM_KEY,scheduleJob);
scheduler.scheduleJob(jobDetail,trigger);
// 如果该定时器处于暂停状态
if ("1".equals(scheduleJob.getStatus())){
pauseJob(scheduler,Long.parseLong(scheduleJob.getId())) ;
}
} catch (SchedulerException e){
throw new RuntimeException("createJob Fail",e) ;
}
}

/**
* Describe: 更新定时任务
* Param: Scheduler ScheduleJobBean
* Return: null
* */
public static void updateJob(Scheduler scheduler, ScheduleJob scheduleJob) {
try {
TriggerKey triggerKey = getTriggerKey(Long.parseLong(scheduleJob.getId()));
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression()).withMisfireHandlingInstructionDoNothing();
CronTrigger trigger = getCronTrigger(scheduler, Long.parseLong(scheduleJob.getId()));
assert trigger != null;
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
trigger.getJobDataMap().put(ScheduleJob.JOB_PARAM_KEY, scheduleJob);
scheduler.rescheduleJob(triggerKey, trigger);
if("1".equals(scheduleJob.getStatus())){
pauseJob(scheduler, Long.parseLong(scheduleJob.getId()));
}
} catch (SchedulerException e) {
throw new RuntimeException("updateJob Fail",e);
}
}

/**
* Describe: 停止定时任务
* Param: Scheduler ScheduleJobBean
* Return: null
* */
public static void pauseJob (Scheduler scheduler, Long jobId){
try {
scheduler.pauseJob(getJobKey(jobId));
} catch (SchedulerException e){
throw new RuntimeException("pauseJob Fail",e);
}
}

/**
* Describe: 恢复定时任务
* Param: Scheduler ScheduleJobBean
* Return: null
* */
public static void resumeJob (Scheduler scheduler, Long jobId){
try {
scheduler.resumeJob(getJobKey(jobId));
} catch (SchedulerException e){
throw new RuntimeException("resumeJob Fail",e);
}
}

/**
* Describe: 删除定时任务
* Param: Scheduler ScheduleJobBean
* Return: null
* */
public static void deleteJob (Scheduler scheduler, Long jobId){
try {
scheduler.deleteJob(getJobKey(jobId));
} catch (SchedulerException e){
throw new RuntimeException("deleteJob Fail",e);
}
}

/**
* Describe: 执行定时任务
* Param: Scheduler ScheduleJobBean
* Return: null
* */
public static void run (Scheduler scheduler, ScheduleJob scheduleJob){
try {
JobDataMap dataMap = new JobDataMap() ;
dataMap.put(ScheduleJob.JOB_PARAM_KEY,scheduleJob);
scheduler.triggerJob(getJobKey(Long.parseLong(scheduleJob.getId())),dataMap);
} catch (SchedulerException e){
throw new RuntimeException("run Fail",e) ;
}
}

}

定时任务启动处理类

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
package com.wangijun.springbootdemo.schedule;

import ...;

/**
* Describe: 定时任务启动处理类
* Author: 就眠仪式
* CreateTime: 2019/10/23
* */
public class ScheduleStarted {

@Resource
private Scheduler scheduler ;

@Resource
private ScheduleJobService scheduleJobService;

@PostConstruct
public void init (){
List<ScheduleJob> scheduleJobList = scheduleJobService.list();
for (ScheduleJob scheduleJob : scheduleJobList) {
CronTrigger cronTrigger = com.wangijun.springbootdemo.schedule.ScheduleHandler.getCronTrigger(scheduler,Long.parseLong(scheduleJob.getId())) ;
if (cronTrigger == null){
com.wangijun.springbootdemo.schedule.ScheduleHandler.createJob(scheduler,scheduleJob);
} else {
com.wangijun.springbootdemo.schedule.ScheduleHandler.updateJob(scheduler,scheduleJob);
}
}
}
}

Web接口

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
92
93
94
95
96
97
98
99
package com.wangijun.springbootdemo;

import ...;

@RestController
public class ScheduleController {
@Resource
ScheduleJobService jobService;
@Resource
ScheduleLogService logService;
@Resource
Scheduler scheduler;


/**
* 根据任务ID运行一个任务
*
* @param id 任务ID
* @return 运行状态
*/
@GetMapping("task_run")
ResponseEntity<Object> taskRun(String id) {
ScheduleJob job = jobService.getById(jobId);
ScheduleHandler.run(scheduler, job);
return ResponseEntity.ok("成功");
}

/**
* 保存任务
*
* @param job 任务实体
* @return 保存结果
*/
@PostMapping("task_save")
ResponseEntity<Object> taskSave(ScheduleJob job) {
job.setId(IdWorker.getIdStr());
job.setCreateTime(LocalDateTime.now());
jobService.save(job);
ScheduleHandler.createJob(scheduler, job);
return ResponseEntity.ok("成功");
}

/**
* 更新任务
*
* @param job 任务数据
* @return 更新结果
*/
@PostMapping("task_update")
ResponseEntity<Object> taskUpdate(ScheduleJob job) {
jobService.updateById(job);
ScheduleHandler.updateJob(scheduler, job);
return ResponseEntity.ok("成功");
}

/**
* 停止定时任务
*
* @param id 任务Id
* @return 执行结果
*/
@GetMapping("task_pause")
ResponseEntity<Object> taskPause(String id) {
ScheduleJob scheduleJob = jobService.getById(jobId);
ScheduleHandler.pauseJob(scheduler, Long.parseLong(jobId));
scheduleJob.setStatus("1");
jobService.updateById(scheduleJob);
return ResponseEntity.ok("成功");
}

/**
* 恢复定时任务
*
* @param id 任务Id
* @return 执行结果
*/
@GetMapping("task_resume")
ResponseEntity<Object> taskResume(String id) {
ScheduleJob scheduleJob = jobService.getById(jobId);
ScheduleHandler.resumeJob(scheduler, Long.parseLong(jobId));
scheduleJob.setStatus("0");
jobService.updateById(scheduleJob);
return ResponseEntity.ok("成功");
}

/**
* 删除一个任务
*
* @param jobId 任务ID
* @return 删除结果
*/
@GetMapping("task_del")
ResponseEntity<Object> taskDel(String jobId) {
ScheduleHandler.deleteJob(scheduler, Long.parseLong(jobId));
jobService.removeById(jobId);
return ResponseEntity.ok("成功");
}
}

定时任务类

新增接口接口BaseTaskService.java

1
2
3
4
5
6
7
8
9
package com.wangijun.springbootdemo.task.base;

public interface BaseTaskService {
/**
* 任 务 实 现
* */
void run(String params) throws Exception;
}

通用定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.wangijun.springbootdemo.task;

import ...;

/**
* Describe: 示例任务
* Author: 就免仪式
* CreateTime: 2019/10/23
* */
@Slf4j
@Component("commonTask")
public class CommonTaskImpl implements BaseTaskService {
private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") ;
/**
* 任务实现
* */
@Override
public void run(String params) {
log.info("Params === >> " + params);
log.info("当前时间::::" + FORMAT.format(new Date()));
System.out.println("执行成功");
}
}

异常的定时任务

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
package com.wangijun.springbootdemo.task;


import ...;

/**
* Describe: 示例任务
* Author: 就免仪式
* CreateTime: 2019/10/23
* 异常任务
* */
@Slf4j
@Component("exceptionTask")
public class ExceptionTaskImpl implements BaseTaskService {


private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") ;
/**
* 任务实现
* */
@Override
public void run(String params) throws Exception{
log.info("当前时间::::" + FORMAT.format(new Date()));
throw new Exception("发生异常");
}
}


后话

建表语句以及测试项目源码,mock.http文件中里面有测试数据,IDEA可以很方便的测试。

源码

封面

新璃月空港


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!