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

本文最后更新于:9 个月前

风风火火,恍恍惚惚,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/
作者
无良芳
发布于
2021年11月22日
许可协议