• Quartz入门——定时任务动态添加修改及持久化与自动恢复


    本文已收录至我的Github仓库DayDayUP:github.com/RobodLee/DayDayUP,欢迎Star

    ⭐⭐⭐⭐⭐`转载请注明出处:https://blog.csdn.net/weixin_43461520/article/details/126362784

    前言

    最近学习了定时任务相关的内容,了解了下Quartz框架,但是原生的一些API用起来不太方便,所以按照我自己的使用场景做了一些封装。这篇文章就带小伙伴们了解一下Quartz的基本使用。包括基本概念以及如何动态地对定时任务进行CRUD,并且如何实现定时任务的持久化以及任务恢复。

    一、Quartz的基本使用

    Quartz 是一个开源的作业调度框架。在使用这个框架之前,我们需要知道几个基本的概念JobTrigger以及Schedule

    Job和JobDetail

    既然是作业调度,那么肯定要有作业呀,这个作业就是Job。在定义我们自己的Job的时候,只需要实现Job接口,然后在execute方法里编写具体的业务逻辑即可。也可以继承QuartzJobBean类并重写executeInternal方法,因为QuartzJobBean类也是实现了Job接口。

    public class TemplateJob implements Job {
    
        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            // 欢迎关注我的微信公众号:Robod
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    JobDetail为Job实例提供了许多设置属性,以及JobDataMap成员变量属性,它用来存储特定Job实例的状态信息,调度器需要借助Jobdetail对象来添加Job实例。

    Trigger

    有了任务之后,就该设置任务的触发事件了,在Quartz中使用Trigger来描述触发Job执行的时间触发规则。一个Job可以添加多个Trigger,但是一个Trigger只能绑定一个Job。

    image-20220812142614950

    Quartz中一共有四种Trigger:

    • SimpleTrigger:这种触发器可以在给定时刻触发作业,并且可选择以指定的时间间隔重复。
    • CronTrigger:用过定时任务的小伙伴应该会猜到这个是干什么的吧。这个触发器可以设置一个Cron表达式,指定任务的执行周期。
    • CalendarIntervalTrigger:用于根据重复的日历时间间隔触发。
    • DailyTimeIntervalTrigger:用于根据每天重复的时间间隔触发任务的触发器。

    后面两种我并没有用过,所以下文的介绍主要基于前两种。后两种感兴趣的小伙伴可以自行研究。

    Schedule

    Schedule所扮演的是一个执行者的角色,将JobDetail和Trigger注册到Schedule中,它就会按照指定的规则去执行任务。

    定义一个定时任务

    如果是Springboot的项目,添加如下依赖即可:

    <dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-quartzartifactId>
    dependency>
    
    • 1
    • 2
    • 3
    • 4

    定义任务分为三步:定义一个JobDetail定义一个Trigger使用Scheduler去执行任务

    JobDetail jobDetail = JobBuilder.newJob(TemplateJob.class).withIdentity("任务名", "任务组名").build();
    SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger()
            .withIdentity("触发器名", "触发器组名")
            .startNow()
            .build();
    Scheduler scheduler = new StdSchedulerFactory().getScheduler();
    scheduler.scheduleJob(jobDetail, trigger);
    scheduler.start();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    上面这段代码中,将触发器设为startNow,也就是立即执行。也可以使用startAt以及endAt方法设置开始时间以及结束时间等。

    关于SimpleTrigger的详细用法可以参考这个链接:https://xuzongbao.gitbooks.io/quartz/content/SimpleTrigger.html

    在Job中获取自定义参数

    在实际的使用过程中,我们可能需要在创建一个Job时指定一些参数用于具体的业务场景,就可以借助JobDataMap。比如指定一个时间给某个人发送奖品,那么在创建任务时就需要用户id,奖品名称,奖品id等信息。我们就可以在定义JobDetail时,使用usingJobData方法设置一些参数,或者使用setJobData方法将定义好的jobDetail填入进去。

    JobDetail jobDetail = JobBuilder.newJob(TemplateJob.class)
            .usingJobData("userId", "VIP2345678")
            .usingJobData("awardName", "100元优惠券")
            .usingJobData("awardId", "YHQ675567687765")
      		.usingJobData("awardValue",6.6)
            .withIdentity("任务名", "任务组名")
            .build();
    //-----------或者---------------
    JobDataMap jobDataMap = new JobDataMap();
    jobDataMap.put("userId", "VIP5465756453");
    jobDataMap.put("awardName", "100元优惠券");
    jobDataMap.put("awardId", "YHQ67354747443");
    jobDataMap.put("awardValue",6.6);
    JobDetail jobDetail2 = JobBuilder.newJob(TemplateJob.class)
            .setJobData(jobDataMap)
            .withIdentity("任务名", "任务组名")
            .build();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    JobDataMap就可以将它理解为HashMap,用法都是类似的。然后在Job中就可以获取到JobDataMap,从而拿到相应的参数:

    public class AwardJob implements Job {
    
        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            JobDataMap jobDataMap = context.getJobDetail().getJobDataMap();
            String userId = jobDataMap.getString("userId");
            String awardName = jobDataMap.getString("awardName");
            String awardId = jobDataMap.getString("awardId");
            double awardValue = jobDataMap.getDouble("awardValue");
            //…………发奖品…………
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    二、Quartz工具类实现定时任务的动态CRUD

    参考:https://blog.csdn.net/u010377605/article/details/77801896

    上一节中提到定义一个定时任务要分为三步,但是在一个项目中必然会在多处都用到定时任务,如果每次定义都要写这样一段代码显然不够优雅。为了方便使用,可以写一个通用的工具类去实现定时任务的CRUD。

    每个Job的jobName都必须是唯一的,建议使用和业务相关的主键id作为jobName。这样即可以保证唯一性,也可以通过jobName判断这个某个定时任务具体是做什么操作的,还可以在Job的execute方法中获取到这个主键id而执行相关操作,就不用额外传递参数了。比如订票系统中在发车前给用户发短信,就可以将订单的id作为jobName。

    JobDetail和Trigger还需要指定一个分组,一个项目中使用到定时任务的分组应该是固定数量的,定义一个枚举类将需要用到的分组名称放在里面,在不同的业务场景下创建定时任务时选用不同的分组即可,需要额外的再往里面添加就行。

    @Getter
    @AllArgsConstructor
    public enum QuartzGroupEnum {
    
        T1( "测试分组1"),
        T2("测试分组2");
    
        private final String value;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    添加一个定时任务

    @Component
    public class QuartzUtil {
    
        private static final SchedulerFactory SCHEDULER_FACTORY = new StdSchedulerFactory();
    
        @Autowired
        private QuartzService quartzService;
    
        /**
         * 添加一个定时任务
         *
         * @param name      任务名。每个任务唯一,不能重复。方便起见,触发器名也设为这个
         * @param group     任务分组。方便起见,触发器分组也设为这个
         * @param jobClass  任务的类类型  eg:TemplateJob.class
         * @param startTime 任务开始时间。传null就是立即开始
         * @param endTime   任务结束时间。如果是一次性任务或永久执行的任务就传null
         * @param cron      时间设置表达式。传null就是一次性任务
         */
        public boolean addJob(String name, String group, Class<? extends Job> jobClass,
                           LocalDateTime startTime, LocalDateTime endTime, String cron, JobDataMap jobDataMap) {
            try {
                // 第一步: 定义一个JobDetail
                JobDetail jobDetail = JobBuilder.newJob(jobClass).
                        withIdentity(name, group).setJobData(jobDataMap).build();
                // 第二步: 设置触发器
                TriggerBuilder<Trigger> triggerBuilder = newTrigger();
                triggerBuilder.withIdentity(name, group);
                triggerBuilder.startAt(toStartDate(startTime));
                triggerBuilder.endAt(toEndDate(endTime)); //设为null则表示不会停止
                if (StrUtil.isNotEmpty(cron)) {
                    triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
                }
                Trigger trigger = triggerBuilder.build();
                //第三步:调度器设置
                Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
                scheduler.scheduleJob(jobDetail, trigger);
                if (!scheduler.isShutdown()) {
                    scheduler.start();
                }
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
            return true;
        }
    
        private static Date toEndDate(LocalDateTime endDateTime) {
            // 结束时间可以为null,所以endDateTime为null,直接返回null即可
            return endDateTime != null ?
                    DateUtil.date(endDateTime) : null;
        }
    
        private static Date toStartDate(LocalDateTime startDateTime) {
            // startDateTime为空时返回当前时间,表示立即开始
            return startDateTime != null ?
                    DateUtil.date(startDateTime) : new Date();
        }
    
    }
    
    • 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

    在需要添加定时任务的地方,我们只需要调用这个方法,将指定的几个参数传入进去,就可以定义并开始一个定时任务了。现在我们只需要关注如何在Job中编写自己的业务代码而不需要去关心如何创建一个定时任务了。

    在定义触发器时,如果startTime参数传过来为null的话,就表示是立即执行,那么就在startAt中将现在的时间传入。不用判断是应该用startAt还是startNow,因为从源码中可以看到,startNow方法也是将时间设为现在。

    image-20220813232823357

    这里有一个地方需要注意,在定义触发器时,写的是Trigger而不是SimpleTrigger或者CronTrigger。这是因为我想用这个方法去添加一个一次性任务或者周期性任务,这样写的话,由于Java多态的特点,如果不指定cron,在运行时就是自动转型为SimpleTrigger,指定了cron后,运行时就会自动转型为CronTrigger。这样我们就不用关心是该用SimpleTrigger还是CronTrigger了。这一点需要注意,因为在下面的小节中会用到这个知识点。

    触发器设置时间用的是Date,但是我平时用LocalDateTime比较多,所以在传参时我选择使用LocalDateTime然后调用一个方法进行转换。

    修改一个定时任务

    /**
     * 修改一个任务的开始时间、结束时间、cron。不改的就传null
     *
     * @param name         任务名。每个任务唯一,不能重复。方便起见,触发器名也设为这个
     * @param group        任务分组。方便起见,触发器分组也设为这个
     * @param newStartTime 新的开始时间
     * @param newEndTime   新的结束时间
     * @param cron         新的时间表达式
     */
    public boolean modifyJobTime(String name, String group, LocalDateTime newStartTime,
                             LocalDateTime newEndTime, String cron) {
        try {
            Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
            TriggerKey triggerKey = TriggerKey.triggerKey(name, group);
            Trigger oldTrigger = scheduler.getTrigger(triggerKey);
            if (oldTrigger == null) {
                return false;
            }
            TriggerBuilder<Trigger> triggerBuilder = newTrigger();
            triggerBuilder.withIdentity(name, group);
            if (newStartTime != null) {
                triggerBuilder.startAt(toStartDate(newStartTime));   // 任务开始时间设定
            } else if (oldTrigger.getStartTime() != null) {
                triggerBuilder.startAt(oldTrigger.getStartTime()); //没有传入新的开始时间就不变
            }
            if (newEndTime != null) {
                triggerBuilder.endAt(toEndDate(newEndTime));   // 任务结束时间设定
            } else if (oldTrigger.getEndTime() != null) {
                triggerBuilder.endAt(oldTrigger.getEndTime()); //没有传入新的结束时间就不变
            }
            if (StrUtil.isNotEmpty(cron)) {
                triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
            } else if (oldTrigger instanceof CronTrigger) {
                String oldCron = ((CronTrigger) oldTrigger).getCronExpression();
                triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(oldCron));
            }
            Trigger newTrigger = triggerBuilder.build();
            scheduler.rescheduleJob(triggerKey, newTrigger);    // 修改触发时间
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    	return true;
    }
    
    • 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

    修改任务其实就是重新设置一个Trigger。先通过触发器名和触发器组名(也是任务名和任务组名)将旧的触发器 oldTrigger 查询出来,因为我们会用到其中的一些信息,然后定义一个新的触发器,对于不需要修改的参数就继续使用 oldTrigger 中的。

    这里有段代码注意一下:

    if (StrUtil.isNotEmpty(cron)) {
    		triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron));
    } else if (oldTrigger instanceof CronTrigger) {
    		String oldCron = ((CronTrigger) oldTrigger).getCronExpression();
        triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(oldCron));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    前面提过,设置了Cron就是CronTrigger,未设置就是SimpleTrigger。所以这里就可以通过Trigger的类型来判断是哪一种,传过来的cron为null表示不需要修改,如果之前是一次性任务就不用管,如果之前就是周期性任务,那么肯定是CronTrigger,在不需要修改的情况下,就将cron设为之前的。

    取消一个定时任务

    public boolean cancelJob(String jobName, String groupName) {
        try {
            Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
            TriggerKey triggerKey = TriggerKey.triggerKey(jobName, groupName);
            scheduler.pauseTrigger(triggerKey); // 停止触发器
            scheduler.unscheduleJob(triggerKey);    // 移除触发器
            scheduler.deleteJob(JobKey.jobKey(jobName, groupName)); // 删除任务
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        //将数据库中的任务状态设为 取消
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    取消就比较简单了,直接将触发器停止并移除,最后删除任务即可。

    查询所有的定时任务

    public List<QuartzEntity> getAllJobs() throws SchedulerException {
        Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
    
        List<QuartzEntity> quartzJobs = new ArrayList<>();
        try {
            List<String> triggerGroupNames = scheduler.getTriggerGroupNames();
            for (String groupName : triggerGroupNames) {
                GroupMatcher<TriggerKey> groupMatcher = GroupMatcher.groupEquals(groupName);
                Set<TriggerKey> triggerKeySet = scheduler.getTriggerKeys(groupMatcher);
                for (TriggerKey triggerKey : triggerKeySet) {
                    Trigger trigger = scheduler.getTrigger(triggerKey);
                    JobKey jobKey = trigger.getJobKey();
                    JobDetail jobDetail = scheduler.getJobDetail(jobKey);
                    //组装数据
                    QuartzEntity entity = new QuartzEntity();
                    entity.setJobName(jobDetail.getKey().getName());
                    entity.setGroupName(jobDetail.getKey().getGroup());
                    entity.setStartTime(LocalDateTimeUtil.of(trigger.getStartTime()));
                    entity.setEndTime(LocalDateTimeUtil.of(trigger.getStartTime()));
                    entity.setJobClass(jobDetail.getJobClass().getName());
                    if (trigger instanceof CronTrigger) {
                        entity.setCron(((CronTrigger) trigger).getCronExpression());
                    }
                    entity.setJobDataMapJson(JSONUtil.toJsonStr(jobDetail.getJobDataMap()));
                    quartzJobs.add(entity);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return quartzJobs;
    }
    
    • 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

    先获取所有的触发器组名,再遍历获取每个触发器组中的触发器Set集合,最后遍历触发器Set集合获取JobDetail信息,然后用一个QuartzEntity对象对数据进行封装返回,再将entity放入List中。

    三、在Job中注入Bean

    在执行具体的定时任务时,肯定会用到相应的Service,但是通过@Autowired或者构造器注入的方式都会注入失败。可以通过一个工具类去实现在Job中注入Bean。在Service的实现类上一定要添加@Service(“xxxxService”)注解,不然会注入失败。

    @Component
    public class SpringContextJobUtil implements ApplicationContextAware {
    
        private static ApplicationContext context;
    
        @Override
        @SuppressWarnings("static-access")
        public void setApplicationContext(ApplicationContext context)
                throws BeansException {
            this.context = context;
        }
    
        public static Object getBean(String beanName) {
            return context.getBean(beanName);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    调用getBean方法就可以正常注入了。

    public class TemplateJob implements Job {
    
        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            QuartzService quartzService = (QuartzService) SpringContextJobUtil.getBean("quartzService");
          	//…………欢迎关注我的微信公众号:Robod
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    参考这篇文章:https://www.jianshu.com/p/aff9199b4416

    四、持久化Job并实现程序启动时任务恢复

    当遇到更新版本等情况时,肯定要将程序给停了,但是程序停止后那些还未开始或者没执行完的定时任务就没了。所以我们需要将任务持久化到数据库中,然后在程序启动时将这些任务进行恢复。

    image-20220813222316954

    在数据库中添加一张表,用于存储Job的信息。

    然后在QuartzUtil中定义一个 recoveryAllJob 方法用于恢复定时任务:

    public void recoveryAllJob() {
        List<QuartzEntity> tasks = quartzService.notStartOrNotEndJobs();
        if (tasks != null && tasks.size() > 0) {
            for (QuartzEntity task : tasks) {
                try {
                    JobDataMap jobDataMap = JSONUtil.toBean(task.getJobDataMapJson(), JobDataMap.class);
                    JobDetail jobDetail = JobBuilder.newJob((Class<? extends Job>) Class.forName(task.getJobClass()))
                            .withIdentity(task.getJobName(), task.getGroupName())
                            .setJobData(jobDataMap).build();
                    TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger();
                    triggerBuilder.withIdentity(task.getJobName(), task.getGroupName());
                    triggerBuilder.startAt(toStartDate(task.getStartTime()));
                    triggerBuilder.endAt(toEndDate(task.getEndTime()));
                    if (StrUtil.isNotEmpty(task.getCron())) {
                        triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(task.getCron()));
                    }
                    Trigger trigger = triggerBuilder.build();
                    Scheduler scheduler = SCHEDULER_FACTORY.getScheduler();
                    scheduler.scheduleJob(jobDetail, trigger);
                    if (!scheduler.isShutdown()) {
                        scheduler.start();
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    • 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

    首先从数据库中将需要恢复的任务查询出来,然后遍历任务将其挨个创建出来。

    然后在前面的CRUD方法中添加对数据库的一些操作,也别忘了在一次性任务的Job中执行完成后调用**quartzService.modifyTaskStatus(jobName, “1”)**方法将任务的状态修改为已完成,不然程序启动后任务又恢复过来了:

    @Component
    public class QuartzUtil {
    
        public boolean addJob(String name, String group, Class<? extends Job> jobClass,
                           LocalDateTime startTime, LocalDateTime endTime, String cron, JobDataMap jobDataMap) {
           	//…………
            //存储到数据库中
            QuartzEntity entity = new QuartzEntity();
            entity.setJobName(name);
            entity.setGroupName(group);
            entity.setStartTime(startTime != null ? startTime : LocalDateTime.now());
            entity.setEndTime(endTime);
            entity.setJobClass(jobClass.getName());
            entity.setCron(cron);
            entity.setJobDataMapJson(JSONUtil.toJsonStr(jobDataMap));
            entity.setStatus("0");
            quartzService.save(entity);
            return true;
        }
    
        public boolean modifyJobTime(String name, String group, LocalDateTime newStartTime,
                                 LocalDateTime newEndTime, String cron) {
            //…………
            // 修改数据库中的记录
            QuartzEntity entity = new QuartzEntity();
            entity.setJobName(name);
            entity.setGroupName(group);
            if (newStartTime != null) {
                entity.setStartTime(newStartTime);
            }
            if (newEndTime != null) {
                entity.setEndTime(newEndTime);
            }
            if (StrUtil.isNotEmpty(cron)) {
                entity.setCron(cron);
            }
            return quartzService.modifyJob(entity);
        }
    
        public boolean cancelJob(String jobName, String groupName) {
            //…………
            //将数据库中的任务状态设为 取消
            return quartzService.modifyTaskStatus(jobName, "2");
        }
    
    }
    
    • 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

    在保存和恢复任务时,将jobDataMap以Json的方式进行存储

    QuartzService中的代码就是一些基本的CRUD,没有什么好说的,就不在这里进行说明了,小伙伴们可以下载完整代码进行查看。(链接在文末)

    那么有了恢复方法后,怎样在程序启动时调用这个方法呢?

    很简单,只需要修改启动类,让其实现ApplicationRunner接口并实现run方法,在run方法中调用恢复方法即可。

    @SpringBootApplication
    @MapperScan("com.robod.quartzdemo.mapper")
    public class QuartzDemoApplication implements ApplicationRunner {
    
        @Autowired
        private QuartzUtil quartzUtil;
    
        public static void main(String[] args) {
            SpringApplication.run(QuartzDemoApplication.class, args);
        }
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            quartzUtil.recoveryAllJob();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    这样在程序启动时,就会自动地调用recoveryAllJob方法去恢复定时任务了。

    五、小案例

    现在通过一个具体的案例来简单模拟一下该怎么用。假设有这样一个场景:在火车票的订票系统中,在创建订单时设立一个定时任务,在发车前两个小时给乘客发送提醒乘车的短信,用户可能改签或者取消订单,那么也应该同样的对定时任务进行修改。

    先来看一下Service中是如何使用QuartzUtil来操作定时任务的吧:

    @Service("orderService")
    public class OrderServiceImpl implements OrderService {
    
        @Autowired
        private QuartzUtil quartzUtil;
    
        @Override
        public String bookTicket(String userId, String ticketId) {
            //TODO 查询余票下订单等一些列操作
            Order order = new Order();
            //…………
            //创建一个定时任务
            LocalDateTime noticeTime = order.getDepartureTime().minusHours(2); //通知时间为发车前两小时
            quartzUtil.addJob(String.valueOf(order.getId()), QuartzGroupEnum.DEPARTURE_NOTICE.getValue(),
                    DepartureNoticeJob.class, noticeTime, null, null, null);
            return "";
        }
    
        @Override
        public String rebook(Order order) {
            //TODO 修改订单等一系列操作
            //修改定时任务
            LocalDateTime noticeTime = order.getDepartureTime().minusHours(2); //通知时间为发车前两小时
            quartzUtil.modifyJob(String.valueOf(order.getId()), QuartzGroupEnum.DEPARTURE_NOTICE.getValue(),
                    noticeTime, null, null);
            return "";
        }
    
        @Override
        public String cancelOrder(Order order) {
            //TODO 取消订单等一系列操作
            //取消定时任务
            quartzUtil.cancelJob(String.valueOf(order.getId()), QuartzGroupEnum.DEPARTURE_NOTICE.getValue());
            return "";
        }
    }
    
    • 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

    首先将QuartzUtil给注入进来,然后调用其中相应的方法并将参数传入进去就可以操作定时任务了。这里的JobName设置为了订单的id,一方面是为了避免重复,另一方面是免去了额外传参的麻烦,因为在Job中只需要用到订单id。如果只有订单id这一个参数不够用,那么再使用JobDataMap 设置自定义的参数也是OK的,具体用法前面也有说明。GroupName则是在枚举类中定义的。

    再来看一下DepartureNoticeJob中都做了些什么:

    public class DepartureNoticeJob implements Job {
    
        @Override
        public void execute(JobExecutionContext context) throws JobExecutionException {
            QuartzService quartzService = (QuartzService) SpringContextJobUtil.getBean("quartzService");
            OrderService orderService = (OrderService) SpringContextJobUtil.getBean("orderService");
    
            String jobName = context.getJobDetail().getKey().getName();
            long orderId = Long.parseLong(jobName);
            // TODO 获取订单及用户信息,封装短信内容,调用短信发送模块发送短信
            quartzService.modifyTaskStatus(jobName, "1");
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    在这个Job中,由于Job的jobName被设为的订单的id,所以我们可以通过订单的id查询到订单以及用户的相关信息,然后封装短信的内容,进行发送短信操作。由于发短信是一次性任务,那么在结束后应该修改这条任务的状态为已结束,不然程序重启后这个任务又被恢复了,又会给用户发送重复的信息。

    QuartzUtil的使用大概就是这样,用起来还是非常简单的。

    总结

    介绍到这里就结束了,我感觉这应该足够满足一些基本的业务场景了。小伙伴们可以将代码下载下来进行学习,然后根据自己的需求去做一些更改就可以用在自己的项目中了。代码我没有进行过多的测试,所以可能会有一些瑕疵,不过整体的思路应该是没问题的。

    我也在Controller中添加了几个测试接口,并在ApiFox中添加了一些测试用例,将测试用例导入到ApiFox中就可以测试了。

    image-20220816111930619

    完整的代码以及相关文件:https://github.com/RobodLee/QuartzDemo

    ⭐⭐⭐⭐⭐转载请注明出处:https://blog.csdn.net/weixin_43461520/article/details/126362784

    本文已收录至我的Github仓库DayDayUPgithub.com/RobodLee/DayDayUP,欢迎Star

    如果您觉得文章还不错,请给我来个点赞收藏关注

    学习更多编程知识,点击下方小卡片,微信扫描二维码关注公众号『 R o b o d 』:

  • 相关阅读:
    Python如何获取 Jenkins构建日志
    秋招入职阿里腾讯大厂,阿里淘系内传322页“Java并发编程核心讲义”学习笔记,轻松拿下大厂offer
    git使用
    排序算法——希尔排序
    力扣 704. 二分查找
    Win11 Dev 预览版 25211 发布,任务栏右键菜单“任务管理器”回归
    WinDbg 远程调试遇到IP为:169.254.xx.xx 的处理
    思腾云计算
    K8S部署Dashboard
    EDU挖掘
  • 原文地址:https://blog.csdn.net/weixin_43461520/article/details/126362784