Quartz 是一个功能全面、开源的企业作业调度服务器。
定时任务框架:
1)简单的有Java自带的Timer、 ScheduledExecutorService, Spring自带的Task。
2)相较复杂的分布式定时任务中间件有XXL-JOB、ElasticJob等。
选Quartz理由:
1)任务Tigger能够被持久化,这样即使在发布后,任务依然能够执行,不需要重新设定。
2)能够轻松暂停恢复触发器(即下次不会被调度)。
3)支持Calander,Cron表达式等复杂的触发器,可以灵活的编写复杂触发器。
Quartz中有三个基本”组件”,由它们共同来定义,运行一个定时任务:
1)JobDetail,定时任务中的“任务”;Job接口是真正需要执行的任务。JobDetail接口相当于将Job接口包装了一下,Trigger和Scheduler实际用到的都是JobDetail。
2)Trigger,定时任务中的“定时”;通过cron表达式或是SimpleScheduleBuilder等类,指定任务执行的周期。
3)Scheduler,定时任务的调度器(组装器);Quartz通过调度器来注册、暂停、删除Trigger和JobDetail。
具体使用:
- 引入springboot官方启动器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
- 创建任务
1)在Quartz中创建的所有定时任务都要实现Job接口,但是在SpringBoot中所有的定时任务只要继承QuartzJobBean类即可。
2)QuartzJobBean是一个抽象类,实现了Quartz的Job接口。
2)与Thread的run()方法类似,定时任务的具体实现写在executeInternal()方法中。
3)每创建一个新的定时任务,都需要新建一个Java类并继承QuartzJobBean、实现executeInternal()。
@Component
public class FlowRestartJob extends QuartzJobBean {
private final Logger logger = LoggerFactory.getLogger(FlowRestartJob.class);
@Autowired
private RestartFlowHandler restartFlowHandler;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
try {
restartFlowHandler.execute(); // 具体任务
} catch (Exception e) {
logger.error("任务失败 error:{}", e.getMessage());
}
}
}
- 提供 Quartz 相关配置 Bean,创建JobDetail和Trigger
使用建造者模式的JobBuilder来创建一个JobDetail对象。
JobDetail simpleJob = JobBuilder.newJob(SimpleJob.class) //传入一个Job类
.withIdentity("SimpleJob", "AnchorJobs") //(name, group)标识唯一一个JobDetail
.storeDurably() //在没有Trigger关联的情况下保存该任务到调度器
.build();
1)newJob()中传入的Job类必须是继承了QuartzJobBean的类。
2)withIdentity()中group可不传,不传时默认设为”DEFAULT”。
3)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();
具体实现:
@Configuration
public class QuartzConfig {
@Value("${quartz.restartCron}")
private String restartCron; // corn表达式
@Bean
public JobDetail restartJob() {
return JobBuilder.newJob(FlowRestartJob.class).withIdentity("FlowRestartJob").storeDurably().build();
}
@Bean
public Trigger restartTrigger() {
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(restartCron);
return TriggerBuilder.newTrigger().forJob(restartJob())
.withIdentity("FlowRestartJob").withSchedule(scheduleBuilder).build();
}
}
创建并启动一个定时任务的正常流程是:创建任务类 ——> 创建JobDetail ——> 创建Trigger
Quartz和Springboot一起使用时为什么不用创建Scheduler?
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 the
spring-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。
注册无周期性定时任务:
某些任务不是周期性的,只存在执行时间,必须完成的两点:
1)Job类需要获取到一些数据用于任务的执行。
2)任务执行完成后删除Job和Trigger。
具体代码实现如下:
1.创建任务
@Component
public class RestartJob extends QuartzJobBean {
private final Logger logger = LoggerFactory.getLogger(FlowRestartJob.class);
private Scheduler scheduler;
private SystemUserMapperPlus systemUserMapperPlus;
@Autowired
public RestartJob(Scheduler scheduler, SystemUserMapperPlus systemUserMapperPlus) {
this.scheduler = scheduler;
this.systemUserMapperPlus = systemUserMapperPlus;
}
@Autowired
private RestartFlowHandler restartFlowHandler;
@Override
protected 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());
}
}
}
2.service层逻辑
@Service
public class LeaveApplicationServiceImpl implements LeaveApplicationService {
@Autowired
private Scheduler scheduler;
// 添加job和trigger到scheduler
private void addJobAndTrigger(LeaveApplication leaveApplication) {
// 创建请假开始Job
Long 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("添加请假任务失败");
}
}
}
欢迎大家关注我的公众号!pavel随笔,分享工作时遇见的问题与解决方案,再就是一些学习笔记。