一、Quartz的基本使用
Quartz 是一个开源的作业调度框架。在使用这个框架之前,我们需要知道几个基本的概念Job,Trigger以及Schedule:
Job和JobDetail
既然是作业调度,那么肯定要有作业呀,这个作业就是Job。在定义我们自己的Job的时候,只需要实现Job接口,然后在execute方法里编写具体的业务逻辑即可。也可以继承QuartzJobBean类并重写executeInternal方法,因为QuartzJobBean类也是实现了Job接口。
1 2 3 4 5 6 7 8
| public class QuartzTask implements Job {
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { } }
|
JobDetail为Job实例提供了许多设置属性,以及JobDataMap成员变量属性,它用来存储特定Job实例的状态信息,调度器需要借助Jobdetail对象来添加Job实例。
Trigger
有了任务之后,就该设置任务的触发事件了,在Quartz中使用Trigger来描述触发Job执行的时间触发规则。一个Job可以添加多个Trigger,但是一个Trigger只能绑定一个Job。
SimpleTrigger
:这种触发器可以在给定时刻触发作业,并且可选择以指定的时间间隔重复。
CronTrigger
:用过定时任务的小伙伴应该会猜到这个是干什么的吧。这个触发器可以设置一个Cron表达式,指定任务的执行周期。
CalendarIntervalTrigger
:用于根据重复的日历时间间隔触发。
DailyTimeIntervalTrigger
:用于根据每天重复的时间间隔触发任务的触发器。
还有MultableTrigger
和CoreTrigger
暂时不知道
Schedule
Schedule所扮演的是一个执行者的角色,将JobDetail和Trigger注册到Schedule中,它就会按照指定的规则去执行任务。
定义一个定时任务
如果是Springboot的项目,添加如下依赖即可:
1 2 3 4
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency>
|
定义任务分为三步:定义一个JobDetail、定义一个Trigger、使用Scheduler去执行任务。
1 2 3 4 5 6 7 8 9 10 11 12
| JobDetail jobDetail = JobBuilder .newJob(QuartzTask.class) .withIdentity("任务名", "任务组") .build(); SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity("触发器名", "触发器组名") .startNow() .build(); Scheduler scheduler = new StdSchedulerFactory().getScheduler(); scheduler.scheduleJob(jobDetail, trigger); scheduler.start();
|
上面这段代码中,将触发器设为startNow,也就是立即执行。也可以使用startAt以及endAt方法设置开始时间以及结束时间等。
在Job中获取自定义参数
在实际的使用过程中,我们可能需要在创建一个Job时指定一些参数用于具体的业务场景,就可以借助JobDataMap。比如指定一个时间给某个人发送奖品,那么在创建任务时就需要用户id,奖品名称,奖品id等信息。我们就可以在定义JobDetail时,使用usingJobData方法设置一些参数,或者使用setJobData方法将定义好的jobDetail填入进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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();
|
JobDataMap就可以将它理解为HashMap,用法都是类似的。然后在Job中就可以获取到JobDataMap,从而拿到相应的参数:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class QuartzTask implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) 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"); } }
|
二、Quartz工具类实现定时任务的动态CRUD
参考:blog.csdn.net/u010377605/…
上一节中提到定义一个定时任务要分为三步,但是在一个项目中必然会在多处都用到定时任务,如果每次定义都要写这样一段代码显然不够优雅。为了方便使用,可以写一个通用的工具类去实现定时任务的CRUD。
每个Job的jobName都必须是唯一的,建议使用和业务相关的主键id作为jobName。这样即可以保证唯一性,也可以通过jobName判断这个某个定时任务具体是做什么操作的,还可以在Job的execute方法中获取到这个主键id而执行相关操作,就不用额外传递参数了。比如订票系统中在发车前给用户发短信,就可以将订单的id作为jobName。
JobDetail和Trigger还需要指定一个分组,一个项目中使用到定时任务的分组应该是固定数量的,定义一个枚举类将需要用到的分组名称放在里面,在不同的业务场景下创建定时任务时选用不同的分组即可,需要额外的再往里面添加就行。
1 2 3 4 5 6 7 8 9 10
| less复制代码 @Getter @AllArgsConstructor public enum QuartzGroupEnum { T1( "测试分组1"), T2("测试分组2"); private final String value; }
|
添加一个定时任务
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
| typescript复制代码 @Component public class QuartzUtil { private static final SchedulerFactory SCHEDULER_FACTORY = new StdSchedulerFactory(); @Autowired private QuartzService quartzService;
public boolean addJob(String name, String group, Class<? extends Job> jobClass, LocalDateTime startTime, LocalDateTime endTime, String cron, JobDataMap jobDataMap) { try { 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)); 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) { return endDateTime != null ? DateUtil.date(endDateTime) : null; } private static Date toStartDate(LocalDateTime startDateTime) { return startDateTime != null ? DateUtil.date(startDateTime) : new Date(); } }
|
在需要添加定时任务的地方,我们只需要调用这个方法,将指定的几个参数传入进去,就可以定义并开始一个定时任务了。现在我们只需要关注如何在Job中编写自己的业务代码而不需要去关心如何创建一个定时任务了。
在定义触发器时,如果startTime参数传过来为null的话,就表示是立即执行,那么就在startAt中将现在的时间传入。不用判断是应该用startAt还是startNow,因为从源码中可以看到,startNow方法也是将时间设为现在。
这里有一个地方需要注意,在定义触发器时,写的是Trigger而不是SimpleTrigger或者CronTrigger。这是因为我想用这个方法去添加一个一次性任务或者周期性任务,这样写的话,由于Java多态的特点,如果不指定cron,在运行时就是自动转型为SimpleTrigger,指定了cron后,运行时就会自动转型为CronTrigger。这样我们就不用关心是该用SimpleTrigger还是CronTrigger了。这一点需要注意,因为在下面的小节中会用到这个知识点。
触发器设置时间用的是Date,但是我平时用LocalDateTime比较多,所以在传参时我选择使用LocalDateTime然后调用一个方法进行转换。
修改一个定时任务
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
| java复制代码
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; }
|
修改任务其实就是重新设置一个Trigger。先通过触发器名和触发器组名(也是任务名和任务组名)将旧的触发器 oldTrigger 查询出来,因为我们会用到其中的一些信息,然后定义一个新的触发器,对于不需要修改的参数就继续使用 oldTrigger 中的。
这里有段代码注意一下:
1 2 3 4 5 6
| scss复制代码if (StrUtil.isNotEmpty(cron)) { triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cron)); } else if (oldTrigger instanceof CronTrigger) { String oldCron = ((CronTrigger) oldTrigger).getCronExpression(); triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(oldCron)); }
|
前面提过,设置了Cron就是CronTrigger,未设置就是SimpleTrigger。所以这里就可以通过Trigger的类型来判断是哪一种,传过来的cron为null表示不需要修改,如果之前是一次性任务就不用管,如果之前就是周期性任务,那么肯定是CronTrigger,在不需要修改的情况下,就将cron设为之前的。
取消一个定时任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| typescript复制代码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 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| ini复制代码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 }
|
先获取所有的触发器组名,再遍历获取每个触发器组中的触发器Set集合,最后遍历触发器Set集合获取JobDetail信息,然后用一个QuartzEntity对象对数据进行封装返回,再将entity放入List中。
三、在Job中注入Bean
在执行具体的定时任务时,肯定会用到相应的Service,但是通过@Autowired或者构造器注入的方式都会注入失败。可以通过一个工具类去实现在Job中注入Bean。在Service的实现类上一定要添加@Service(“xxxxService”)注解,不然会注入失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| typescript复制代码@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); }
}
|
调用getBean方法就可以正常注入了。
1 2 3 4 5 6 7 8 9
| java复制代码public class TemplateJob implements Job {
@Override public void execute(JobExecutionContext context) throws JobExecutionException { QuartzService quartzService = (QuartzService) SpringContextJobUtil.getBean("quartzService"); }
}
|
四、持久化Job并实现程序启动时任务恢复
当遇到更新版本等情况时,肯定要将程序给停了,但是程序停止后那些还未开始或者没执行完的定时任务就没了。所以我们需要将任务持久化到数据库中,然后在程序启动时将这些任务进行恢复。
在数据库中添加一张表,用于存储Job的信息。
然后在QuartzUtil中定义一个 recoveryAllJob 方法用于恢复定时任务:
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
| scss复制代码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(); } } } }
|
首先从数据库中将需要恢复的任务查询出来,然后遍历任务将其挨个创建出来。
然后在前面的CRUD方法中添加对数据库的一些操作,也别忘了在一次性任务的Job中执行完成后调用quartzService.modifyTaskStatus(jobName, “1”) 方法将任务的状态修改为已完成,不然程序启动后任务又恢复过来了:
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
| typescript复制代码@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"); }
}
|
在保存和恢复任务时,将jobDataMap以Json的方式进行存储。
QuartzService中的代码就是一些基本的CRUD,没有什么好说的,就不在这里进行说明了,小伙伴们可以下载完整代码进行查看。(链接在文末)
那么有了恢复方法后,怎样在程序启动时调用这个方法呢?
很简单,只需要修改启动类,让其实现ApplicationRunner接口并实现run方法,在run方法中调用恢复方法即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| java复制代码@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(); } }
|
这样在程序启动时,就会自动地调用recoveryAllJob方法去恢复定时任务了。
五、小案例
现在通过一个具体的案例来简单模拟一下该怎么用。假设有这样一个场景:在火车票的订票系统中,在创建订单时设立一个定时任务,在发车前两个小时给乘客发送提醒乘车的短信,用户可能改签或者取消订单,那么也应该同样的对定时任务进行修改。
先来看一下Service中是如何使用QuartzUtil来操作定时任务的吧:
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
| typescript复制代码@Service("orderService") public class OrderServiceImpl implements OrderService {
@Autowired private QuartzUtil quartzUtil;
@Override public String bookTicket(String userId, String ticketId) { 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) { 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) { quartzUtil.cancelJob(String.valueOf(order.getId()), QuartzGroupEnum.DEPARTURE_NOTICE.getValue()); return ""; } }
|
首先将QuartzUtil给注入进来,然后调用其中相应的方法并将参数传入进去就可以操作定时任务了。这里的JobName设置为了订单的id,一方面是为了避免重复,另一方面是免去了额外传参的麻烦,因为在Job中只需要用到订单id。如果只有订单id这一个参数不够用,那么再使用JobDataMap 设置自定义的参数也是OK的,具体用法前面也有说明。GroupName则是在枚举类中定义的。
再来看一下DepartureNoticeJob
中都做了些什么:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| java复制代码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); quartzService.modifyTaskStatus(jobName, "1"); }
}
|
在这个Job中,由于Job的jobName被设为的订单的id,所以我们可以通过订单的id查询到订单以及用户的相关信息,然后封装短信的内容,进行发送短信操作。由于发短信是一次性任务,那么在结束后应该修改这条任务的状态为已结束,不然程序重启后这个任务又被恢复了,又会给用户发送重复的信息。
QuartzUtil的使用大概就是这样,用起来还是非常简单的。