SpringBoot快速整合Quartz动态管理定时任务

SpringBoot快速整合Quartz动态管理定时任务

背景

​ 如题,上个版本产品提出了一个报告需求的整体大功能,其中有一个小的子功能就是开发一个在页面==动态添加、删除、修改、启停==的定时任务来执行一系列的操作(生成周期报告、发送邮件等);关于SpringBoot中的定时任务,大家应该都比较熟悉,在主启动类上添加***@EnableScheduling***,然后直接在需要定时执行的方法上直接添加***@Scheduled(cron = “”)并且添加上定时任务的cron表达式即可;但是这种方式有一个局限性就是,在编码阶段就已经将任务的个数,执行周期全部的确定好了,没办法在系统运行时进行***动态的增删改查,所以这里不就符合我们的需求,这时***Quartz***就该上场了。

​ ***注:***我这里又没有使用到整个Quartz的功能,我只是使用了它的任务调度及触发功能;关于任务的存储和持久化,我直接使用了数据库创建一个自己的表来存储;这样有一个好处,任务的信息可以直接拿给前端去使用,免去了复杂的转换数据,并且在后期如果有拓展信息的需求也可以更方便和快捷的添加;这样做的需要在项目启动时,将数据库已经保存的数据根据需要依次加载到内存中,这样就可以不用创建Quartz大量的数据库表,并且, 我这边的项目已经有其他的模块在使用那些任务和表了,我想区分开,让各个不同模块的定时任务都分开,独立出来。

​ **附:**开发后页面的功能如下:

SpringBoot快速整合Quartz动态管理定时任务_第1张图片

什么是quartz

​ Quartz是一个完全由java编写的开源作业调度框架。不要让作业调度这个术语吓着你。尽管Quartz框架整合了许多额外功能, 但就其简易形式看,你会发现它易用得简直让人受不了! ------ 来自百度词条

​ 其实关于Quartz现在在网上已经有很多的文章和帖子介绍了,大家也可以自己去了解了解,这里的重点是SpringBoot项目中,怎么快速的整合它;好消息是,现在的SpringBoot中已经继承了Quartz的starer了,所以大伙可以更方便的使用它了,甚至使用的它的简单功能时,都不需要不用自己主动去注入了,直接@Autowire 一键注入!太香了!

相关概念

​ 在了解Quartz时,有几个比较重要的名词和概念需要了解一下,这几个概念了解透彻了,整合起来会更加的顺畅、丝滑!他们的关系如下

SpringBoot快速整合Quartz动态管理定时任务_第2张图片

  • Scheduler:任务调度器,用来管理和调度任务;

  • Trigger:触发器,存放Job执行的时间策略,用于定义任务调度时间规则。

    Trigger触发器有:SimpleTrigger、CalendarIntervalTrigger、DailyTimeIntervalTrigger、CronTrigger; 其中常用的有两种***SimpleTrigger*** 和 CronTrigger

    • SimpleTrigger:指定从某一个时间开始,以一定的时间间隔(单位是毫秒)执行的任务。
      • 它适合的任务类似于:9:00 开始,每隔1小时,执行一次。
    • CronTrigger:适合于更复杂的任务,它支持类型于Linux Cron的语法(并且更强大)。基本上它覆盖了其他Trigger的绝大部分能力(但不是全部)—— 当然,也更难理解。 所以 CronTrigger比SimpleTrigger更常用,当你需要一个基于日历般概念的作业调度器,而不是像SimpleTrigger那样精确指定间隔时间。所以我们选择了CronTrigger。
  • Job &obDetail:JobDetail是任务的定义,而Job是任务的执行逻辑。在JobDetail里会引用一个Job Class定义,可以理解为JobDetail是Job的进一步信息的封装。

  • Cron: Cron表达式相信大家都比较熟悉了,这里的Cron写法基本和Linux 中的Crontab表达式一致,但是有一些细节还是有区别的,建议大家生成时,去网站进行校验一下。

本文主要讲解
  • SpringBoot快速整合Quartz(主要是快速、简单)

  • 动态任务的基于内存,但是项目重启了定时任务信息就没了,所以需要将任务信息持久化到数据库

  • 动态添加的任务数据的持久化是自己创建的数据库表,而不是Quartz框架中的表

步骤
1. 引入POM依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
    <version>2.2.10.RELEASE</version>
</dependency>
2. 编写job实现类
// 这里我直接实现了QuartzJobBean
@Component
@Slf4j
public class ReportJob extends QuartzJobBean {
    @Override
    protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        try {
            // 需要执行任务详细业务代码 。。。
          
            // 此处catch一下,为了在执行失败后,不让quartz再执行一次业务逻辑,导致业务数据错误,
            // 正常应该是不应该将异常抛到此处的,但是由于我的项目需要,只能在这里进行捕捉了,这里是由于Quartz的失败重试机制,容易导致数据异常。
        } catch (Exception e) {
            log.error("{}:执行失败,跳过此次任务执行。",jobExecutionContext.getJobDetail().getKey().getName());
        }
    }
}
3. 业务数据PO代码

​ 由于我没有使用Quartz JobStores中的数据库表,这里就是我对前端页面传过来的数据进行保存进自定义数据库表对应的PO。

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName(value = "t_report_subscribe", autoResultMap = true)
@NoArgsConstructor
public class ReportSubscribePO implements Serializable {

    private static final long serialVersionUID = 1L;
    /**
     * 主键
     */
    @ApiModelProperty(value = "主键id")
    @TableId(value = "id")
    @JsonFormat(shape = JsonFormat.Shape.STRING)
    private Long id;
    /**
     * 报告名称
     */
    @ApiModelProperty(value = "报告名称")

    private String reportName;

    /**
     * 模板id
     */
    @ApiModelProperty(value = "模板id")
    private String  templateId;

    /**
     * 报告创建时间
     */
    @ApiModelProperty(value = "创建时间")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    /**
     * 更新时间,拓展字段
     */
    @ApiModelProperty(value = "更新时间")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    /**
     * cron表达式
     */
    @ApiModelProperty(value = "cron表达式")
    private String cronExpression;
    /**
     * 发送周期:day-每天,week-每周,month-每月
     */
    @ApiModelProperty(value = "发送周期:day-每天,week-每周,month-每月")
    private String sendPeriod;
    /**
     * 日:月末用"end"
     */
    @ApiModelProperty(value = "日:月末用:end")
    private String sendDay;
    /**
     * 时
     */
    @ApiModelProperty(value = "时")
    private Integer sendHour;

    /**
     * 上一次生成时间
     */
    @ApiModelProperty(value = "上一次生成时间")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private LocalDateTime lastTime;
    /**
     * 下一次生成时间
     */
    @ApiModelProperty(value = "下一次生成时间")
    @JsonSerialize(using = LocalDateTimeSerializer.class)
    @JsonFormat(pattern="yyyy-MM-dd HH:mm:ss")
    private LocalDateTime nextTime;
    /**
     * 发送邮箱设备id
     */
    @ApiModelProperty(value = "发送邮箱设备id")
    private String  deviceId;


    /**
     * 接收人邮箱
     */
    @ApiModelProperty(value = "接收人邮箱")
    private String receiveEmails;
    /**
     * 状态:true-启用,false-禁用
     */
    @ApiModelProperty(value = "状态:true-启用,false-禁用")
    private Boolean status;
    /**
     * 报告类型
     */
    @ApiModelProperty(value = "报告类型")
    private String  type;
    /**
     * 报告格式
     */
    @ApiModelProperty(value = "报告格式")
    private String format;
    /**
     * 统计周期 :0-前1天,1-前1周,2-前1月
     */
    @ApiModelProperty(value = "统计周期 :0-前1天,1-前1周,2-前1月")
    private Integer statisticalPeriod;

    /**
     * 订阅人
     */
    @ApiModelProperty(value = "订阅人")
    private String subscribeOwner;

}
4. *Job管理类

​ 这个类,是本文最关键的类之一,主要用来实现定时任务的动态的增删改查的业务逻辑

@Slf4j
@Component
public class ReportQuartzManage {
    /**
     * 创建定时任务 定时任务创建之后默认启动状态
     *
     * @param scheduler:       调度器
     * @param reportSubscribe: 报告订阅对象
     * @return: void
     **/
    public void createScheduleJob(Scheduler scheduler, ReportSubscribePO reportSubscribe) throws SchedulerException {

        //获取到定时任务的执行类  必须是类的绝对路径名称
        //定时任务类需要是job类的具体实现 QuartzJobBean是job的抽象类。
        Class<? extends Job> jobClass = ReportJob.class;
        // 构建定时任务信息
        JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(reportSubscribe.getId().toString(), ReportConstant.REPORT_JOB_GROUP_NAME).build();
        // 设置定时任务执行方式
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(reportSubscribe.getCronExpression());
        // 构建触发器trigger
        // 如果已经有下一次时间,就设置为下一次时间为触发时间
        CronTrigger trigger;
        if (!Objects.isNull(reportSubscribe.getNextTime())) {
            Date date = DateUtil.localDateTimeToDate(reportSubscribe.getNextTime());
            trigger = TriggerBuilder.newTrigger().startAt(date)
                    .withIdentity(reportSubscribe.getId().toString(), ReportConstant.REPORT_JOB_GROUP_NAME).withSchedule(scheduleBuilder).build();
        } else {
            trigger = TriggerBuilder.newTrigger().startNow()
                    .withIdentity(reportSubscribe.getId().toString(), ReportConstant.REPORT_JOB_GROUP_NAME).withSchedule(scheduleBuilder).build();
        }

        scheduler.scheduleJob(jobDetail, trigger);

        // 设置下次执行时间
      /*  LocalDateTime nextTime = DateUtil.dateToLocalDate(trigger.getNextFireTime());
        log.info("下次一执行时间:{}",nextTime);
        reportSubscribe.setNextTime(nextTime);*/


    }

    /**
     * 根据任务名称暂停定时任务
     *
     * @param scheduler 调度器
     * @param jobName   定时任务名称(这里直接用ReportSubscribePO的Id)
     * @throws SchedulerException
     */
    public void pauseScheduleJob(Scheduler scheduler, String jobName) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName, ReportConstant.REPORT_JOB_GROUP_NAME);
        scheduler.pauseJob(jobKey);

    }

    /**
     * 根据任务名称恢复定时任务
     *
     * @param scheduler 调度器
     * @param reportSubscribe   定时任务名称(这里直接用ReportSubscribePO的Id)
     * @throws SchedulerException
     */
    public void resumeScheduleJob(Scheduler scheduler,  ReportSubscribePO reportSubscribe) throws SchedulerException {

        // 判断当前任务是否在调度中
        Set<JobKey> jobKeys = scheduler.getJobKeys(GroupMatcher.groupEquals(ReportConstant.REPORT_JOB_GROUP_NAME));
        List<JobKey> thisNameJobs = jobKeys.stream().filter(jobKey -> Objects.equals(reportSubscribe.getId().toString(), jobKey.getName())).collect(Collectors.toList());

        if (CollectionUtils.isNotEmpty(thisNameJobs)){
            JobKey jobKey = JobKey.jobKey(reportSubscribe.getId().toString(), ReportConstant.REPORT_JOB_GROUP_NAME);
            scheduler.resumeJob(jobKey);
            // 下一次执行时间设置回去
            Trigger trigger = scheduler.getTrigger(TriggerKey.triggerKey(reportSubscribe.getId().toString(), ReportConstant.REPORT_JOB_GROUP_NAME));
            LocalDateTime nextTime = DateUtil.dateToLocalDate(trigger.getNextFireTime());
            reportSubscribe.setNextTime(nextTime);
        }else {
            createScheduleJob(scheduler, reportSubscribe);
        }

    }

    /**
     * 更新定时任务
     *
     * @param scheduler       调度器
     * @param reportSubscribe 报告订阅对象
     * @throws SchedulerException
     */
    public void updateScheduleJob(Scheduler scheduler, ReportSubscribePO reportSubscribe) throws SchedulerException {

        //获取到对应任务的触发器
        TriggerKey triggerKey = TriggerKey.triggerKey(reportSubscribe.getId().toString(),ReportConstant.REPORT_JOB_GROUP_NAME);
        //设置定时任务执行方式
        CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(reportSubscribe.getCronExpression());
        //重新构建任务的触发器trigger
        CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
        if (trigger == null){
            return;
        }
      //  trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
          trigger = TriggerBuilder.newTrigger().startNow()
                .withIdentity(reportSubscribe.getId().toString(), ReportConstant.REPORT_JOB_GROUP_NAME).withSchedule(scheduleBuilder).build();

        //重置对应的job
        scheduler.rescheduleJob(triggerKey, trigger);

        reportSubscribe.setNextTime(DateUtil.dateToLocalDate(trigger.getNextFireTime()));
    }

    /**
     * 根据定时任务名称从调度器当中删除定时任务
     *
     * @param scheduler 调度器
     * @param jobName   定时任务名称 (这里直接用ReportSubscribePO的Id)
     * @throws SchedulerException
     */
    public void deleteScheduleJob(Scheduler scheduler, String jobName) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName,ReportConstant.REPORT_JOB_GROUP_NAME);
        scheduler.deleteJob(jobKey);

    }

}
5. *项目启动时任务初始化

​ 这里我直接让service实现了CommandLineRunner,然后在run()方法中,将初始化逻辑写入进来,让数据库中的持久化的任务全部添加进内存中。

@Service
@Slf4j
public class ReportSubscribeServiceImpl extends ServiceImpl<ReportSubscribeMapper, ReportSubscribePO> implements ReportSubscribeService, CommandLineRunner {
	@Override
    public void run(String... args) throws Exception {
        init();
    }
    
     private void init() {
        // 初始化所有的已经启用的订阅
        LambdaQueryWrapper<ReportSubscribePO> query = Wrappers.<ReportSubscribePO>lambdaQuery().eq(ReportSubscribePO::getStatus, true);
        List<ReportSubscribePO> enableSubs = this.list(query);
        log.info("需要初始化的任务个数:{}", enableSubs.size());
        for (ReportSubscribePO sub : enableSubs) {
            try {
                log.info("开始初始化订阅任务,任务name:{}", sub.getId());
                reportQuartzManage.createScheduleJob(scheduler, sub);
            } catch (Exception e) {
                log.error("启动时初始化订阅任务失败:{}", e.getMessage());
            }
        }
    }
}
6. Service层

​ 这里就是一些业务代码,涉及公司内容,就不在放进来了。

7. Controller层

​ 同上

总结

至此SpringBoot快速整合Quartz基本就写完了,其中有几点需要注意的是:

  1. 基于内存的定时任务,在项目重启时怎么重新初始化进内存。 ----- 3、5 小节
  2. 任务的动态管理。 ----- 4小节
  3. Quartz的在发生异常时会重试一次,注意异常处理。 -----2小节

你可能感兴趣的:(spring,boot,后端,java,quartz)