SpringBoot2.5.x整合Quartz并持久化
风风火火,恍恍惚惚,springboot2.6.0正式版也发布了,同时也宣布了2.4x版本停止维护。2.5.x系列也是目前自己写Demo在用的,为什么要记录下来呢, 主要我把项目升级到2.6之后项目跑不起来了,原因我知道,但是我不知道怎么解决,看了看机翻的更新文档,有处可能是影响我项目跑不起来的原因,就是限制了依赖的循环导入,现在依赖循环导入,项目是会直接跑不起来。我的问题是项目里获取不到DataSource数据源,我用了各种姿势,都获取不到,靓仔在线疑惑。既然不能无缝升级,那我就记下来一个正常的版本。
定时任务是很常用的一个功能,比如简单的生日提醒,每天定点查询用户的生日信息。然后发出一系列的提示操作。不了解你就感觉很难,但是现在spring给我们带来了很多的便利,看一看,在头脑风暴一下,也不难。编程一途,殊途同归。
主要工作中好多同步代码感觉都可以使用定时任务来处理,然而实际上用的是一个一个的Main方法,吐槽吧,人家那代码又不是不能跑,不仅能跑,还能赚钱。没啥好吐槽的,就是维护时想骂娘,骂娘就骂娘,还十分容易出错。
核心配置类
用了这个配置类,腰不酸了,腿不痛了,一次能爬五层楼。觉也睡的香了。好吧,前面纯属扯淡,写了这个yml中就不需要配置了,大致看一下每行的注释,测试学习中以先跑起来为主,然后再慢慢研究即可。
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
可以自定义,要点看代码注释。
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
主要就是记录一下日志,可根据自己需求,自行扩展。
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;
}
执行定时任务的上下文
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);
}
}
}
定时任务处理器
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) ;
}
}
}
定时任务启动处理类
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接口
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
package com.wangijun.springbootdemo.task.base;
public interface BaseTaskService {
/**
* 任 务 实 现
* */
void run(String params) throws Exception;
}
通用定时任务
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("执行成功");
}
}
异常的定时任务
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可以很方便的测试。
封面
SpringBoot2.5.x整合Quartz并持久化
https://wangijun.com/2021/11/22/java-05/