最近需要写一些周期性自动执行的定时任务,如果在实验室,可能会选择java自带的Timer类;但是对于公司中的项目,Timer类实现定时任务只能有一个后台线程执行任务,并且只能让程序按照某个频度执行,并不能在指定时间点执行。任务调度框架Quartz刚好满足这些需求,在使用的时候了解了这个quartz,现总结如下。
现有定时任务框架:
选Quartz理由:

Quartz中有三个基本”组件”,由它们共同来定义,运行一个定时任务:
JobDetail,定时任务中的“任务”;Job接口是真正需要执行的任务。JobDetail接口相当于将Job接口包装了一下,Trigger和Scheduler实际用到的都是JobDetail。
Trigger,定时任务中的“定时”;通过cron表达式或是SimpleScheduleBuilder等类,指定任务执行的周期。
Scheduler,定时任务的调度器(组装器);Quartz通过调度器来注册、暂停、删除Trigger和JobDetail。
Quartz通过Cron定义Quartz的调度时间Trigger(例如0 0 12 ? * WED
表示“每周三上午12:00”)。此外,时间表也可以通过SimpleTrigger
,由Date
定义触发的开始时间、毫秒的时间间隔和重复计数(例如“在下周三12:00,然后每隔10秒、执行5次”)。框架具体代码逻辑如下图所示:

创建并启动一个定时任务的正常流程是:创建任务类 ——> 创建JobDetail ——> 创建Trigger,具体代码实现如下:
引入springboot官方启动器
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId></dependency>
创建任务
在Quartz中创建的所有定时任务都要实现Job接口,但是在SpringBoot中所有的定时任务只要继承QuartzJobBean类即可。
QuartzJobBean是一个抽象类,实现了Quartz的Job接口。
与Thread的run()方法类似,定时任务的具体实现写在executeInternal()方法中。
每创建一个新的定时任务,都需要新建一个Java类并继承QuartzJobBean、实现executeInternal()。
@Componentpublic class FlowRestartJob extends QuartzJobBean {private final Logger logger = LoggerFactory.getLogger(FlowRestartJob.class);@Autowiredprivate RestartFlowHandler restartFlowHandler;@Overrideprotected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {try {restartFlowHandler.execute(); // 具体任务} catch (Exception e) {logger.error("任务失败 error:{}", e.getMessage());}}}
提供 Quartz 相关配置 Bean,创建JobDetail和Trigger
JobDetail simpleJob = JobBuilder.newJob(SimpleJob.class) //传入一个Job类.withIdentity("SimpleJob", "AnchorJobs") //(name, group)标识唯一一个JobDetail.storeDurably() //在没有Trigger关联的情况下保存该任务到调度器.build();
newJob()中传入的Job类必须是继承了QuartzJobBean的类。
withIdentity()中group可不传,不传时默认设为”DEFAULT”。
storeDurably()使JobDetail可在没有关联Trigger的情况下添加到调度器中,否则会抛异常。建议调用此方法。
常用Trigger有两种:SimpleTrigger和CronTrigger。二者最大的区别是CronTrigger支持Cron表达式。创建CronTrigger:
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?"); //Cron表达式,每5秒执行一次CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("CronJob", "AnchorTriggers") //(name, group)唯一标识一个Trigger.startNow() //调用scheduler.scheduleJob()后立即开始执行定时任务.withSchedule(scheduleBuilder) //不同的scheduleBuilder.build();
具体代码实现:
@Configurationpublic class QuartzConfig {@Value("${quartz.restartCron}")private String restartCron; // corn表达式@Beanpublic JobDetail restartJob() {return JobBuilder.newJob(FlowRestartJob.class).withIdentity("FlowRestartJob").storeDurably().build();}@Beanpublic Trigger restartTrigger() {CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(restartCron);return TriggerBuilder.newTrigger().forJob(restartJob()).withIdentity("FlowRestartJob").withSchedule(scheduleBuilder).build();}}
SpringBoot官方写了 spring-boot-starter-quartz,这是一个官方提供的启动器,有了这个启动器,集成的操作就会被大大简化。SpingBoot2.2.6官方文档,其中第4.20小节 Quartz Scheduler 就谈到了Quartz。
原文如下:
Spring Boot offers several conveniences for working with the Quartz scheduler, including thespring-boot-starter-quartz “Starter”. If Quartz is available, a Scheduler is auto-configured (through the SchedulerFactoryBean abstraction).Beans of the following types are automatically picked up and associated with the Scheduler:• JobDetail: defines a particular Job. JobDetail instances can be built with the JobBuilder API.• Calendar.• Trigger: defines when a particular job is triggered.
即:SpringBoot提供了一些便捷的方法来和Quartz协同工作,这些方法里面包括`spring-boot-starter-quartz`这个启动器。
如果Quartz可用,Scheduler会通过SchedulerFactoryBean这个工厂bean自动配置到SpringBoot里。JobDetail、Calendar、Trigger这些类型的bean会被自动采集并关联到Scheduler上。
Job可以定义setter(也就是set方法)来注入配置信息。也可以用同样的方法注入普通的bean。
Job类需要获取到一些数据用于任务的执行。
任务执行完成后删除Job和Trigger。
创建任务
@Componentpublic class RestartJob extends QuartzJobBean {private final Logger logger = LoggerFactory.getLogger(FlowRestartJob.class);private Scheduler scheduler;private SystemUserMapperPlus systemUserMapperPlus;@Autowiredpublic RestartJob(Scheduler scheduler, SystemUserMapperPlus systemUserMapperPlus) {this.scheduler = scheduler;this.systemUserMapperPlus = systemUserMapperPlus;}@Autowiredprivate RestartFlowHandler restartFlowHandler;@Overrideprotected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {Trigger trigger = jobExecutionContext.getTrigger();// 将添加任务的时候存进去的数据拿出来JobDetail jobDetail = jobExecutionContext.getJobDetail();JobDataMap jobDataMap = jobDetail.getJobDataMap();long username = jobDataMap.getLongValue("username");LocalDateTime time = LocalDateTime.parse(jobDataMap.getString("time"));try {// 具体任务逻辑restartFlowHandler.execute();// 执行之后删除任务scheduler.pauseTrigger(trigger.getKey()); // 暂停触发器的计时scheduler.unscheduleJob(trigger.getKey()); // 移除触发器中的任务scheduler.deleteJob(jobDetail.getKey()); // 删除任务} catch (Exception e) {logger.error("任务失败 error:{}", e.getMessage());}}}
service层逻辑
@Servicepublic class LeaveApplicationServiceImpl implements LeaveApplicationService {@Autowiredprivate Scheduler scheduler;// 添加job和trigger到schedulerprivate void addJobAndTrigger(LeaveApplication leaveApplication) {// 创建请假开始JobLong proposerUsername = leaveApplication.getProposerUsername();LocalDateTime startTime = leaveApplication.getStartTime();JobDetail startJobDetail = JobBuilder.newJob(RestartJob.class)// 指定任务组名和任务名.withIdentity(leaveApplication.getStartTime().toString(), proposerUsername + "_start")// 添加一些参数,执行的时候用于取出.usingJobData("username", proposerUsername).usingJobData("time", startTime.toString()).build();// 创建请假开始任务的触发器// 创建cron表达式指定任务执行的时间,由于请假时间是确定的,所以年月日时分秒都是确定的,这也符合任务只执行一次的要求。String startCron = String.format("%d %d %d %d %d ? %d",startTime.getSecond(),startTime.getMinute(),startTime.getHour(),startTime.getDayOfMonth(),startTime.getMonth().getValue(),startTime.getYear());CronTrigger startCronTrigger = TriggerBuilder.newTrigger()// 指定触发器组名和触发器名.withIdentity(leaveApplication.getStartTime().toString(), proposerUsername + "_start").withSchedule(CronScheduleBuilder.cronSchedule(startCron)).build();// 将job和trigger添加到scheduler里try {scheduler.scheduleJob(startJobDetail, startCronTrigger);} catch (SchedulerException e) {e.printStackTrace();throw new CustomizedException("添加请假任务失败");}}}




