1. 适用场景
当业务涉及到动态定时任务时,我首先能想到的就是Quartz。目前有很多种实现定时任务的方式,比如:
- ScheduledThreadPoolExecutor
- timer
- spring自带的@Scheduled
- quartz
使用方式不进行过多介绍,但是前三者在使用的过程中都不是那么灵活,往往服务启动后,定时任务便固定,想要进行不停机修改很不容易。而quartz完美解决动态定时任务的问题,它提供了多样的api,可以进行定时任务的暂停、恢复、定时时间的修改等,同时还支持分布式定时任务。
2. quartz为什么这么强?
quartz这么灵活的原因与他本身的设计理念是分不开的。
核心概念:
- Job表示一个工作,要执行具体的内容。此接口只有一个方法,就是用来执行业务逻辑的。
void execute(JobExecutionContext context)
- JobDetail表示一个具体的可执行调度程序,job是这个可执行程序所需要执行的内容。同时JobDetail还包含了一些任务调度的吧方案和策略。
- Trigger调度参数,能设置什么时候取执行定时任务。用来设置定时周期的。
- Scheduler调度容器,JobDetail和trigger在scheduler中注册就可以被Scheduler容器调度了。
从quartz的核心组件我们可以看到, 任务,定时周期,任务调度这三者是独立分割开来了,只有当三种通过某种方式组合在一起的时候,定时任务才会被最终执行,这就意味着拥有无限可能。我是否可以卸载某个scheduler中的trigger重新注册一个新的trigger上进行调度?我是否可以将任务暂时停止不去执行??
3. quartz整合springboot
关于quartz的基本使用和api我这边就不多介绍了,可以自行查看官方文档都很详细。网上整合quartz的文章也有很多,这边主要是说一下在使用过程中的一些坑。
- quartz第三方包版本问题导致RAMJobStore总是加载失败,报错是 “no setter method of 'useProperties'。
使用springboot1.5.9 quartz 2.2.3报错。 将quartz版本升级到2.3.0正常启动。
具体整合方案:
gradle:
compile('org.quartz-scheduler:quartz:2.3.0')
compile('org.quartz-scheduler:quartz-jobs:2.3.0')
相关配置:
@Configuration
public class SchedulerConfig {
@Autowired
private CustomJobFactory customJobFactory;
@Bean(name = "properties")
public Properties quartzProperties() throws IOException {
//加载配置信息
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
//新建quartz.properties 放在resources目录下
propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
@Bean(name = "schedulerFactory")
public SchedulerFactory schedulerFactory(Properties properties) throws IOException, SchedulerException {
//采用功能强大的StdScheduleFactory
SchedulerFactory schedulerFactory = new StdSchedulerFactory(properties);
return schedulerFactory;
}
/**
* 通过SchedulerFactory获取Scheduler的实例
*/
@Bean(name = "scheduler")
public Scheduler scheduler(SchedulerFactory schedulerFactory) throws IOException, SchedulerException {
Scheduler scheduler = schedulerFactory.getScheduler();
//设置自定义的jobFactory
scheduler.setJobFactory(customJobFactory);
scheduler.start();
return scheduler;
}
}
自定义JobFactory
@Component
public class CustomJobFactory extends AdaptableJobFactory {
/**
* 通过spring AutowireCapableBeanFactory将job对象加入到spring容器,提供给spring管理
*/
@Autowired
private AutowireCapableBeanFactory capableBeanFactory;
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
//调用父类方法创建job对象, 这里创建的对象是通过反射创建,思考一下会引发什么?
Object jobInstance = super.createJobInstance(bundle);
//注入到spring 采用这种注入方式,再job类中无法使用构造函数注入,只能通过变量注入
capableBeanFactory.autowireBean(jobInstance);
//autowireMode=4表示先查找构造函数,如果有就使用构造函数注入,否则就根据类型注入
//capableBeanFactory.autowire(jobInstance.getClass(), 4, true);
return jobInstance;
}
}
需要执行的job任务
//坑: 注意,这里一定不要多此一举加上@Component, 因为之前已经通过capableBeanFactory进行注入,如果加上这里会创建实例报错
@DisallowConcurrentExecution
public class Task implements Job {
private final static Logger logger = LogUtils.getLogger(RuleUpdateTask.class);
@Autowired
private Service service;
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
logger.info("定时任务开始执行”);
//.......
service.save(obj)
}
}
初始化时使用调度器执行定时任务:
@Component
public class InitStartSchedule implements CommandLineRunner {
private final Logger logger = LogUtils.getLogger(InitStartSchedule.class);
@Autowired
private Scheduler scheduler;
@Override
public void run(String... args) throws Exception {
//增加job以及trigger
JobDetail jobDetail = JobBuilder.newJob(RuleUpdateTask.class).withIdentity("jobName", "jobGroupName").build();
//采用表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(ruleUpdate.getCron());
//trigger的key名称要保持和对应的jobDetail一致
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("jobName", "jobGroupName").withSchedule(scheduleBuilder).startNow().build();
//任务不存在时添加
if (!scheduler.checkExists(jobDetail.getKey())) {
try {
scheduler.scheduleJob(jobDetail, cronTrigger);
} catch (SchedulerException e) {
logger.info("创建定时任务失败", e);
throw new Exception("创建定时任务失败");
}
}
}
}
工具类实现定时任务的动态修改:
/**
* @author jiandan
* @Create 2020-12-23
* 任务调度工具类, 更新, 增加, 暂停, 开启
*/
@Component
public class SchedulerUtil {
private static final Logger logger = LogUtils.getLogger(SchedulerUtil.class);
@Autowired
private Scheduler scheduler;
/**
* 添加定时任务
* @param cronExpression
* @throws Exception
*/
public void addJob(String cronExpression, String jobName, String jobGroupName, Class jobClass) throws Exception{
//构建Job信息
JobDetail jobDetail = JobBuilder.newJob(jobClass)
.withIdentity(jobName, jobGroupName)
.build();
//cron表达式构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity(jobName, jobGroupName)
.withSchedule(scheduleBuilder)
.startNow().build();
if (!scheduler.checkExists(jobDetail.getKey())){
try {
scheduler.scheduleJob(jobDetail, cronTrigger);
} catch (SchedulerException e) {
logger.info("创建定时任务失败" + e);
throw new Exception("创建定时任务失败");
}
}
}
/**
* 暂定job
* @throws Exception
*/
public void jobPause(String jobName, String jobGroupName) throws Exception {
scheduler.pauseJob(JobKey.jobKey(jobName, jobGroupName));
}
/**
* 启动job
*/
public void jobResume(String jobName, String jobGroupName) throws Exception {
scheduler.resumeJob(JobKey.jobKey(jobName, jobGroupName));
}
/**
* 更新定时任务的cron表达式
*/
public void jobReschedule(String jobName, String jobGroupName, String cronExpression) throws Exception {
try{
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//使用新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).startNow().build();
// 按照新的trigger重新设置job执行
scheduler.rescheduleJob(triggerKey, trigger);
} catch (SchedulerException e) {
logger.error("更新定时任务失败", e);
throw new Exception("更新定时任务失败");
}
}
}
quartz.properties, 配置文件根据自己所需进行填写,官网上有许多对于配置文件属性的讲解
#SimpleThreadPool维护一个固定的线程集,不会增长也不会缩小,功能和性能比较强大
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
#5个线程已经比较充足了
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
org.quartz.jobStore.misfireThreshold = 60000
#RAMJobStore性能最高,但是所有的数据保存在RAM,程序重启不会保留之前的调度数据
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
关于job中带一些业务数据并持久化保存可以参考quartz的持久化配置以及JobDataMap的api