简介
java后端入门新手,对知识内容理解较浅,如文章内容有误,请各位大佬多多指点。本篇文章适用于对quartz有一定了解的入门新手,且并没有采用quartz官方的持久化方式,是在结合工作需求的基础上完成的quartz任务调度的任务添加与修改,以及对时间表达式的修改等功能
任务调度框架Quartz基本概念如下:
Job/JobDetail: 任务
任务指的是你要做的事,也是任务调度的核心,比如我想要每隔一个小时喝一杯水,那么任务就是“喝水”
Trigger: 触发器
触发器指的是任务触发的时间,也就是任务在什么时间开始执行。例如上一句喝水的例子,“每隔一个小时”,这就是一个触发器,触发器具有两种定时类型,一种简单定时,一种使用Cron时间表达式进行复杂定时。
简单使用
一.传统SpringBoot配置使用
这个方法是使用了SpringBoot与Quartz的整合方式,并将任务和触发器用Bean组件的方式进行自动配置。
1.导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2.新建任务类
在SpringBoot2.x之前,使用Quartz建立任务类需要实现Job接口,重写excute()方法,而在SpringBoot中,一般是继承QuartzJobBean类,重写executeInternal() 方法,方法内就是任务具体执行的内容
@Slf4j
public class MyJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("MyJob开始运行啦!");
}
}
3.新建配置类
需要创建一个Job,关联对应的实体类,每个Job和Trigger都可以设置分组,Job/Trigger有组group和名字name的联合唯一标识,在一个触发器中不允许有两个相同标识的Job/Trigger
- Job和trigger都可以进行分组,相对应的任务和触发器可以分到同一组
- 可以使用Trigger的group和name来获取对应的触发器
@Configuration
public class QuartzConfig {
@Bean
public JobDetail jobDetail(){
return JobBuilder.newJob(MyJob.class)//加载任务实体(必须)
.withIdentity("myJob","group1")//设置分组
.storeDurably(true)//true会使任务在没有对应触发器的情况下一直存在
.build();
}
@Bean
public Trigger trigger(){
//设置Cron时间表达式,此处也可用SimpleScheduleBuilder
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0/30 0/1 * * * ?");
//建立触发器
return TriggerBuilder.newTrigger()
.forJob(jobDetail())//与Job进行绑定
.withIdentity("trigger","group1")//分组
.withSchedule(cronScheduleBuilder)//绑定触发时间
.build();//构建
}
}
程序启动时会自动加载@Configration配置文件,以及被@Bean标记的方法,至此,任务调度启动。
程序启动时会实现以下内容,这是代表着Quartz的内容存储在内存中,没有进行持久化,并显示了线程数等信息
Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED.
Currently in standby mode.
Number of jobs executed: 0
Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.
二.复杂多任务调度
Gitee源码地址:quartz任务调度
1.简介
多任务调度自然不可能是并发,而是采用了多线程异步并行的方法来进行多个任务的调度。Quartz有自带的线程池,默认的核心线程数是10个,也就是说,最多支持10个任务的同时调度;当十个任务要进行切换时,线程池会释放空余的线程,并从第一个线程开始,循环往复。不过不建议在没有配置线程池的情况下运行10个任务,可能会造成线程堵塞的情况。
2.非默认持久化
本篇文章的多任务方法并没有使用默认的持久化方式,而是采用了自建表的方式来存储任务信息。如需要使用默认的持久化方式,请参考Quartz官方文档建表说明;如想简单使用自建表,主要需要如下内容:
3.创建数据表
每个任务类都继承于QuartzJobBean类,并重写对应的方法
总体需求:完成多个任务的定时调度,并且能够在数据表中添加任务和修改任务执行周期
任务类的创建需要与数据表中的数据互相对应,并自行配置任务类的结果
数据表内容如下:
其中实体类路径一定要存在对应的实体类,在表中添加任务之前一定要确保你想要添加的Job存在于你的程序中。
4.程序结构
程序结构如下:
其中,核心代码为控制层的两个类
(1)实体类:Job.java
import lombok.Data;
@Data
public class Job {
private int id; //id自增
private String group; //分组
private String name; //任务名
private String className; //任务类路径
private String cron; //时间表达式
}
2.任务类
MyJob/MyJob2
注意分开写。在数据库添加任务之前,记得建立对应的任务类。任务类中的executeInternal()方法中就是你的任务要执行的操作,这里以日志代替,进行了省略。
@Slf4j
public class MyJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("MyJob开始运行啦!");
}
}
@Slf4j
public class MyJob2 extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
log.info("MyJob2开始运行啦!");
}
}
3.核心类
QuartzJob.java
这个类是任务调度的核心类,主要包括四个方法,代码中会详细的介绍方法的作用
@Slf4j
@Component
public class QuartzJob {
@Resource
Scheduler scheduler;
//这里是获取数据库表的数据Service
@Resource
JobService jobService;
public void start() throws SchedulerException {
scheduler.start();
}
@Scheduled(cron = "0 0/1 * * * ?")
public void loadJob() throws Exception {
//数据库遍历数据
List<Job> list = jobService.findAllJob();
for (Job job : list){
//获取一个要修改的触发器的资料,身份,key
TriggerKey triggerKey = new TriggerKey(job.getName(),job.getGroup());
//根据key获取要更改的具体的CronTrigger触发器
CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//判定,如果没有对应的触发器,就建立任务和触发器
if (null == cronTrigger){
createJob(scheduler,job);
}else {
//如果已经有对应的触发器,那么就已存在任务,程序转入触发器Cron监测方法
updateJob(job,job.getCron());
}
}
}
public void createJob(Scheduler scheduler,Job job) throws SchedulerException {
Class<org.quartz.Job> clazz;
try {
//这就是调度器要执行的类
clazz = (Class<org.quartz.Job>) Class.forName(job.getClassName());
} catch (ClassNotFoundException e1) {
throw new RuntimeException(e1);
}
//批量创建任务
JobDetail jobDetail = JobBuilder.newJob(clazz)
.withIdentity(job.getName(),job.getGroup())
.build();
//获取当前Cron时间
String cron = job.getCron();
//创建表达式,构建触发器
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cron).withMisfireHandlingInstructionDoNothing();
//创建触发器
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withSchedule(cronScheduleBuilder)
.withIdentity(job.getName(),job.getGroup())
.build();
//调度器整合任务与对应的触发器
scheduler.scheduleJob(jobDetail,cronTrigger);
log.info("当前job创建成功:{}",job.getName());
}
/**
* 1.此方法会对触发器进行更新,主要更新Cron表达式
* 2.此方法会判定当前触发器的时间较上一分钟是否存在修改
* 3.只有判定存在修改时,才会对表达式进行修改
*
* @param job 任务实体类
* @param time 修改后的Cron表达式
* @throws Exception
*/
public void updateJob(Job job,String time) throws Exception{
//创建一个要修改的触发器的资料,身份
TriggerKey triggerKey = new TriggerKey(job.getName(),job.getGroup());
//获取要更改的具体的CronTrigger触发器
CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//获取当前时间
String oldTime = cronTrigger.getCronExpression();
if (!oldTime.equals(time)){
//用修改后的时间更新触发器
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(time).withMisfireHandlingInstructionDoNothing();
CronTrigger cronTrigger1 = TriggerBuilder.newTrigger()
.withIdentity(job.getName(),job.getGroup())
.withSchedule(cronScheduleBuilder)
.build();
//调度器整合新的触发器
scheduler.rescheduleJob(triggerKey,cronTrigger1);
log.info("监听到修改,任务“{}”发生修改,修改前执行时间为:{} ,修改后执行时间为: {}",job.getName(),oldTime,time);
}
}
}
这里按照从底到外的顺序来说明,createJob/updateJob -> loadJob ->start
createJob(): 这里遍历任务,创建任务JobDetail以及对应的触发器Trigger
updateJob(): 这个方法用于更新触发器,当对时间表达式进行了修改时,用来更新对应的任务类的触发器。
loadJob (): 这个方法用于执行任务的创建和触发器的修改,方法执行时会开始检查对应的name和group,来查找是否建立了这个任务及其触发器。所以在数据表中添加任务时会发现队友对应的触发器,就会去新建任务以及触发器。如果已经存在,就会转入触发器的监听方法,只有当修改了时间表达式后,才会修改触发器。这个方法加入@Scheduled注解,让它能够在固定的时间运行一次,这里设置的是一分钟。
start(): 是任务调度的开启方法,里面启动了调度器,之所以单独分开写,是因为要确保调度器只启动一次,并且一直保持稳定运行
QuartzStart.java
@Slf4j
@Component
public class QuartzStart implements ApplicationListener<ApplicationEvent> {
private static boolean loaded = false;
@Resource
QuartzJob quartzJob;
//这个方法会在容器加载时执行一次
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
if(applicationEvent instanceof ContextRefreshedEvent){
if(!loaded){//避免多次执行
loaded = true;
//定时任务启动
try {
//第一遍加载全部任务
quartzJob.loadJob();
//全部任务都开始执行
quartzJob.start();
System.out.println("任务已经启动...啦啦啦啦啦啦啦");
} catch (SchedulerException se) {
se.printStackTrace();
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
}
}
这个类的方法只会在服务器启动时加载执行一次,目的是加载第一遍任务以及启动任务调度
5.执行结果
刚开始会创建所有任务,并按照任务表中的Cron表达式执行,初始都为10s执行一次
在修改表达式后,任务二改为15秒执行一次
这里要进一步说明的是,在时间修改监听,也就是@Scheduled注解的方法,时间如果设为整点(每分钟执行),并且与任务的执行重合(每十秒,第六次为一个整分钟),由于修改触发器与任务类的执行是分开的两个线程,所以当时间重合时,即使已经修改了时间,但是任务类还是会按照修改前的时间执行一次,第二次会按照新的时间执行,为了避免这样的情况,建议合理安排时间。
本篇文章中对任务与时间的修改在数据库中完成,也可结合前端,封装为接口,动态修改数据库中的数据,从而实现对定时任务的控制
本文完。