quartz框架详解及单机版quartz持久化方案改造
目录
quartz介绍:
日常生活中,我们经常会有定时执行某个任务的需求,但仅仅是spring提供的@Schedule远远达不到我们的目的,例如我们可能需要监控任务执行的情况,对定时任务进行持久化操作,需要通过图形化界面操纵定时任务。这些复杂的定时任务需求都无法只用一个注解来解决,quartz框架实现了定时任务创建、修改、删除、执行、以及监控等操作,完美的帮我们解决了这一系列问题。
quartz的优点
现在网上常见的定时任务框架有
Spring Task
Quartz
Elastic-job
那么quartz的优点是什么呢?
- 强大的调度功能,例如支持丰富多样的调度方法,可以满足各种常规及特殊需求;
- 灵活的应用方式,例如支持任务和调度的多种组合方式,支持调度数据的多种存储方式;
- 分布式和集群能力,Terracotta 收购后在原来功能基础上作了进一步提升。(Spring task就不支持集群)
最重要的一点,quartz是spring默认的调度框架,Quartz很容易跟Spring集合
首先,我们先来了解下quartz的四个核心元素
1、Scheduler
它的名字叫任务调度器,主要职责是总体控制任务调度。比如说任务的暂停,开始,创建和删除等操作。
Scheduler主要有三种:RemoteMBeanScheduler、 RemoteScheduler、StdScheduler 最常用的是StdScheduler
,本文主要以StdScheduler进行讲述
关于Scheduler的具体介绍我们可以看该博客 https://www.cnblogs.com/laosunlaiye/p/9406784.html
2、Trigger
它的名字是触发器,用于定义任务调度的时间规则。
quartz总共有四种触发器,分别是:
SimpleTrigger
主要是针对一些相对简单的时间触发进行配置使用,比如在指定的时间开始然后在一定的时间间隔之内重复执行一个Job
CronTrigger
用cron表达式来控制定时任务执行时间,可以配置更复杂的触发时刻表
DateIntervalTrigger
类似于SimpleTrigger 适合调度类似每 N(1, 2, 3…)小时,每 N 天,每 N 周等的任务
NthIncludedDayTrigger
它设计的目标是提供不同时间间隔的第n天执行时刻表。比如,你想在每个月的第15日处理财务发票记帐,那么可以使用
NthIncludedDayTrigger
来完成这项工作。
当然我们用的最多的还是CronTrigger
,它主要是通过cron表达式进行控制时间规则。
这里我简单介绍一下,cron表达式主要用来表达各种时间需求,总共有7位,最后一位可选,至少六位,从左到右各位置分别是:
位置 | 意义 | 取值 | 支持的符号 |
---|---|---|---|
1 | 秒 | 0-59 | , - * / |
2 | 分 | 0-59 | , - * / |
3 | 时 | 0-23 | , - * / |
4 | 日 | 1-31 | , - * ? / L W C |
5 | 月 | 1-12 或 JAN - DEC | , - * / |
6 | 周 | 1-7 或 MON - SAT | , - * ? / L C # |
7 | 年 | 空或 1970-2099 | , - * / |
注:Cron 表达式对日期英文缩写、特殊字符大小写不敏感。
这是一个常见的cron表达式:“*/5 * * * * ?” 表示的意义是每五秒执行一次,而CronTrigger的主要作用就是解析弄明白他们,然后控制任务时间。
关于cron表达式具体使用我们可以参考该文章https://www.gairuo.com/p/cron-expression-sheet。
3. Job
这个单词相信大家都认识,就是工作任务的意思,也就是你具体要执行的任务。
Job仅仅只是一个接口。
package org.quartz;
public interface Job {
void execute(JobExecutionContext var1) throws JobExecutionException;
}
可以通过实现该接口来定义需要执行的任务
使用示例:
/**
*@Description: 打印helloword
*/
public class printHelloWorld implements Job{
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("hello world");
}
}
注意一个job可以对应多个触发器Trigger,但一个Trigger只能对应一个job ,他们之间是多对一的关系。(这个我想应该很好理解,同一个工作可以由多个触发器触发,但一个触发器只能触发一个工作。)
4.JobDetail
Quartz在执行job的时候,都需要一个job实例,会接受一个job实现类,然后运行的时候通过反射去实例化这个类,所以jobDetail的意义就是用来描述job实现类以及相关静态资源的类。
使用示例:
JobDetail jobDetail = JobBuilder.newJob(PrintHelloWorld.class).withIdentity("job1").build();
这里的newJob的作用就是跟PrintHelloWorld这个job实现类绑定。
- 最后提一点,trigger和job是怎么绑定的呢,这其实就是我们的调度器的作用。
example:
scheduler.scheduleJob(jobDetail,cronTrigger);
附上核心元素关系图
小案例
好的,现在我们已经基本了解了quartz的四个重要元素,这时候我们可以尝试敲一个案例来试验一下。
1.首先,我们需要先引入jar包,这里用的是maven引入
<!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
2.我们创建一个简单的job
/**
*@Description: 打印helloword
*/
public class PrintHelloWorld implements Job{
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
System.out.println("hello world");
}
}
3.创建触发器Trigger和调度器Scheduler来定时执行job
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import java.util.concurrent.TimeUnit;
import static org.quartz.TriggerBuilder.newTrigger;
/**
* @author lixiangxiang
* @description 测试定时任务
* @date 2021/1/25 8:58
*/
public class TestScheduler {
public static void main(String[] args) {
try {
//1.创建调度器Scheduler实例(SchedulerFactory就是造schduler的工厂,一会还会提)
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
//2.创建JobDetail实例,并与printHelloWorld绑定 withIdentity是设置该job的唯一标识 bulid就是创建jobDetail对象
JobDetail jobDetail = JobBuilder.newJob(PrintHelloWorld.class).withIdentity("job1").build();
//3.构建触发器Trigger,设置每五秒执行一次
Trigger cronTrigger = newTrigger()
//设置触发器的名字 作为任务标识
.withIdentity("Trigger1")
//立即生效
.startNow()
//解析cron表达式 创建cronTrigger
.withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
.build();
//4.注册job和触发器Trigger
scheduler.scheduleJob(jobDetail,cronTrigger);
//5.启动调度器
scheduler.start();
System.out.println("--------scheduler start ! ------------");
//睡眠20s
TimeUnit.SECONDS.sleep(20);
scheduler.shutdown();
System.out.println("--------scheduler shutdown ! ------------");
} catch (Exception e) {
e.printStackTrace();
}
}
}
- JobBuilder 无构造函数,只能通过JobBuilder 的静态方法 newJob(Class jobClass) 生成 JobBuilder 实例。
这里需要提一下SchedulerFactory,我们一般不直接创建Scheduler对象,而是SchedulerFactory来创建,
SchedulerFactory是一个接口,它有两个实现类分别是**StdSchedulerFactory和DirectSchedulerFactory**,
我们最常用的还是StdSchedulerFactory,只需要调用getScheduler() 就能获取一个scheduler实例。
DirectSchedulerFactory 使用起来不够方便,需要作许多详细的手工编码设置,所以我们一般不用。
执行结果
--------scheduler start ! ------------
hello world
hello world
hello world
hello world
--------scheduler shutdown ! ------------
SpringBoot项目集成quartz框架
很好,我们先在已经了解到一个定时任务的执行流程了,但我们现在做的项目都是用的Spring框架,所以我们还需要让quartz能够集成到Spring框架中。
幸好springboot早已经帮我们做了这件事情。我们需要引入jar包
<!--spring boot集成quartz-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependncy>
SpringBoot整合quartz框架流程
1.创建job
spring为quartz提供了一个抽象类QuartzJobBean,QuartzJobBean实现了job,我们只需要继承它,并实现他的抽象方法executeInternal
。
这里我们执行一个Spring bean PrintHelloService的 printHello方法 来模拟真实开发中的场景
@Service
public class PrintHelloService {
public void printHello() {
System.out.println("hello");
}
}
/**
* @author lixiangxiang
* @description 执行bean的方法
* @date 2021/1/25 8:57
*/
public class PrintHelloJob extends QuartzJobBean {
@Autowired
PrintHelloService printHelloService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
printHelloService.printHello();
}
}
2.将job注入到bean容器里
之前我们在创建scheduler的时候是通过SchedulerFactory创建,这样有一个缺点,遇到需要注入的bean就会报空指针异常
出现这个问题的原因是定时任务的 Job 对象实例化的过程是通过 Quartz 内部自己完成的,但是我们通过 Spring 进行注入的 Bean 却是由 Spring 容器管理的,Quartz 内部无法感知到 Spring 容器管理的 Bean,所以没有办法在创建 Job 的时候就给装配进去。
所以我们要做的是将Job也装配到 Bean 容器中。
我们知道Job在Spring中是由SchedulerFactoryBean创建的,要想将 Job 放入 bean 容器,那么必定与SchedulerFactoryBean有关。
我们通过查看SchedulerFactoryBean 源码发现如果 jobFactory 不存在的话,默认会使用 AdaptableJobFactory 实现对 job 的创建。
private Scheduler prepareScheduler(SchedulerFactory schedulerFactory) throws SchedulerException {
// 省略部分代码...
// Get Scheduler instance from SchedulerFactory.
try {
Scheduler scheduler = createScheduler(schedulerFactory, this.schedulerName);
populateSchedulerContext(scheduler);
if (!this.jobFactorySet && !(scheduler instanceof RemoteScheduler)) {
// Use AdaptableJobFactory as default for a local Scheduler, unless when
// explicitly given a null value through the "jobFactory" bean property.
//如果 jobFactory 不存在 则使用 AdaptableJobFactory创建
this.jobFactory = new AdaptableJobFactory();
}
if (this.jobFactory != null) {
if (this.applicationContext != null && this.jobFactory instanceof ApplicationContextAware) {
((ApplicationContextAware) this.jobFactory).setApplicationContext(this.applicationContext);
}
if (this.jobFactory instanceof SchedulerContextAware) {
((SchedulerContextAware) this.jobFactory).setSchedulerContext(scheduler.getContext());
}
scheduler.setJobFactory(this.jobFactory);
}
return scheduler;
}
- 所以我们就可以继承AdaptableJobFactory,自己创建一个JobFactory,在创建job实例的同时,通过
AutowireCapableBeanFactory
将创建好的job对象交给Spring管理,然后再将JobFactory传到SchedulerFactoryBean对象中,就ok了。
上代码
@Configuration
public class QuartzConfig {
/**
* 解决Job中注入Spring Bean为null的问题
*/
@Component("quartzJobFactory")
public static class QuartzJobFactory extends AdaptableJobFactory {
private final AutowireCapableBeanFactory capableBeanFactory;
//通过构造器实现bean注入
public QuartzJobFactory(AutowireCapableBeanFactory capableBeanFactory) {
this.capableBeanFactory = capableBeanFactory;
}
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
//调用父类的方法创建job实例
Object jobInstance = super.createJobInstance(bundle);
//把job实例交个Spring管理
capableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
/**
* 注入scheduler到spring
* @param quartzJobFactory /
* @return Scheduler
* @throws Exception /
*/
@Bean(name = "scheduler")
public Scheduler scheduler(QuartzJobFactory quartzJobFactory) throws Exception {
SchedulerFactoryBean factoryBean=new SchedulerFactoryBean();
// 自定义 JobFactory 使得在 Quartz Job 中可以使用自动注入
factoryBean.setJobFactory(quartzJobFactory);
factoryBean.afterPropertiesSet();
Scheduler scheduler=factoryBean.getScheduler();
scheduler.start();
return scheduler;
}
}
3.创建JobDetai CronTrigger 用Scheduler开启任务
/**
* @author lixiangxiang
* @description scheduler示例
* @date 2021/1/25 15:41
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SimpleScheduler {
@Autowired
private Scheduler scheduler;
@Test
public void runJob () {
try {
//1.创建JobDetail实例,并与PrintHelloJob绑定
JobDetail jobDetail = JobBuilder.newJob(PrintHelloJob.class).withIdentity("printHelloJob").build();
//2.构建触发器Trigger,设置每五秒执行一次
Trigger cronTrigger = newTrigger()
.withIdentity("Trigger1")
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))
.build();
//3.注册job和触发器Trigger
scheduler.scheduleJob(jobDetail,cronTrigger);
//4.启动调度器
scheduler.start();
System.out.println("--------scheduler start ! ------------");
//睡眠20s
TimeUnit.SECONDS.sleep(20);
scheduler.shutdown();
System.out.println("--------scheduler shutdown ! ------------");
} catch (Exception e) {
e.printStackTrace();
}
}
}
我们用单元测试启动该定时任务
可以看到定时任务还跟以前一样成功执行。
那么到这里为止,springboot项目整合quartz框架的基本流程已经结束。
但是仅仅只是这些当然无法我们项目的需求,我们之前说过quartz框架是能够帮我们实现定时任务的添加,执行,删除,暂停等操作,也就是要实现quartz的动态化操作。
具体如何实现,让我们接着往下看^^
定时任务的动态化(以下代码参考el-admin框架)
关于定时任务的管理,我们的需求其实也就几个。
- 添加定时任务
- 删除一个任务
- 暂停、恢复 一个任务
- 立即执行一个任务
- 更新任务的cron表达式(我们需要能够改变定时任务的执行时间,这里我们都用的是CronTrigger,所以我们要能更新Cron表达式。)
首先我们需要建一个Job类,用于储存job的一些信息。(这里用了lombook的@Data注解)
@Data
public class QuartzJob implements Serializable {
public static final String JOB_KEY = "JOB_KEY";
@ApiModelProperty(value = "ID")
@TableId(value = "id",type = IdType.AUTO)
//任务的id
private Long id;
//用于子任务唯一标识
@TableField(exist = false)
@ApiModelProperty(value = "用于子任务唯一标识", hidden = true)
private String uuid;
//job名称
private String jobName;
//cron表达式
private String cronExpression;
//定时任务的状态 暂停或启动
private Boolean isPause = false;
//子任务
private String subTask;
//失败后是否暂停
private Boolean pauseAfterFailure;
}
我们还用之前的PrintHelloJob作为job实现类
建一个任务管理类,对任务进行动态管理
添加定时任务
/**
* description: 添加一个任务
*
* @author: lixiangxiang
* @param quartzJob job的信息
* @return void
*/
public void addJob(QuartzJob quartzJob){
try {
// 构建jobDetail,并与PrintHelloJob类绑定(Job执行内容)
JobDetail jobDetail = JobBuilder.newJob(PrintHelloJob.class).
withIdentity(quartzJob.getId()).build();
//通过触发器名和cron表达式创建Trigger
Trigger cronTrigger = newTrigger()
.withIdentity(quartzJob.getId())
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(quartzJob.getCronExpression()))
.build();
//把job信息放入jobDataMap中 job_key为标识
cronTrigger.getJobDataMap().put(QuartzJob.JOB_KEY,quartzJob);
//重置启动时间
((CronTriggerImpl)cronTrigger).setStartTime(new Date());
//执行定时任务
scheduler.scheduleJob(jobDetail,cronTrigger);
} catch (Exception e) {
//strUtil是hutool的工具类
log.error(StrUtil.format("【创建定时任务失败】 定时任务id:{}",quartzJob.getId()),e);
}
}
我们根据quartzJob
中job的信息对定时任务进行设置
增加一个定时任务的原理就是将定时任务job与 Trigger根据quartzJob的信息创建好,然后用sheduler将他们绑定并执行。注意我们都用quartzJob的id对job和Trigger进行了唯一标识。这便于我们后面再拿到改job或Trigger
但是我们需要考虑一个问题,增加定时任务时,定时任务默认是开启的,但是quartzJob中的有个属性是isPause是否暂停,所以我们还需要对其进行判断,如果定改属性为true则暂停。我们需要先实现暂停定时任务的方法。
关于jobDataMap 我需要提一下
JobDataMap用来保存任务实例的状态信息。
当一个Job被添加到调度程序(任务执行计划表)scheduler
的时候,JobDataMap实例就会存储一次关于该任务的状态信息数据。也可以使用 @PersistJobDataAfterExecution 注解标明在一个任务执行完毕之后就存储一次。
也可以自定义添加内容,他与java的map结构相同。我们可以通过==put(key,value)==向其中储存内容,达到向job传值得目的。
如果需要取出,我们可以通过JobExecutionContext (创建job的时候见过)来获取到JobDataMap.
后面会具体讲到如何从JobExecutionContext中获取JobDataMap
中储存的数据。
暂停定时任务
* description: 暂停一个job
*
* @author: lixiangxiang
* @param quartzJob /
* @return void
*/
public void pauseJob(QuartzJob quartzJob) {
try {
//根据之前设的id获取到jobkey
JobKey jobKey = JobKey.jobKey(quartzJob.getId());
//根据jobkey暂停任务
scheduler.pauseJob(jobKey);
} catch (Exception e) {
log.error(StrUtil.format("【暂停定时任务失败】定时任务id:{}",quartzJob.getId()),e);
}
}
对addJob进行改进
/**
* description: 添加一个任务
*
* @author: lixiangxiang
* @param quartzJob job的信息
* @return void
*/
public void addJob(QuartzJob quartzJob){
try {
// 构建jobDetail,并与ExecutionJob类绑定(Job执行内容)
JobDetail jobDetail = JobBuilder.newJob(PrintHelloJob.class).
withIdentity(quartzJob.getId()).build();
//通过触发器名和cron表达式创建Trigger
Trigger cronTrigger = newTrigger()
.withIdentity(quartzJob.getId())
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(quartzJob.getCronExpression()))
.build();
//把job信息放入jobDataMap中 job_key为标识
cronTrigger.getJobDataMap().put(QuartzJob.JOB_KEY,quartzJob);
//重置启动时间
((CronTriggerImpl)cronTrigger).setStartTime(new Date());
//执行定时任务
scheduler.scheduleJob(jobDetail,cronTrigger);
//如果设置暂停,暂停任务
if (quartzJob.getIsPause()){
pauseJob(quartzJob);
}
} catch (Exception e) {
//strUtil是hutool的工具类
log.error(StrUtil.format("【创建定时任务失败】 定时任务id:{}",quartzJob.getId()),e);
}
}
恢复定时任务
/**
* description: 恢复一个job
*
* @author: lixiangxiang
* @param quartzJob /
* @return void
*/
public void resumeJob(QuartzJob quartzJob) {
try {
//根据job的id生成TriggerKey 从而获取到 trigger
TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId());
CronTrigger trigger =(CronTrigger) scheduler.getTrigger(triggerKey);
//如果trigger不存在创建一个新的定时任务
if(ObjectUtil.isNull(trigger)) {
addJob(quartzJob);
}
JobKey jobKey = JobKey.jobKey(quartzJob.getId());
scheduler.resumeJob(jobKey);
} catch (Exception e) {
log.error(StrUtil.format("【恢复定时任务失败】定时任务id:{}",quartzJob.getId()),e);
}
}
为什么我要判断trigger
是否为空呢?
因为当项目重新启动时该job
将不会在保存在quartz的内存中,你直接用id找是找不到的,所以我们要做的是重新添加。前面的一部分代码就是为了判断该id对应的任务是否还能找到。
这是el-admin的一种独特解决思路,他并没有存到quartz本身设计的数据库中,只是将所有定时任务信息存入数据库,在项目重启时重新添加所有定时任务。这点后面会再次提到。
更新cron表达式
/**
* description: 更新cron表达式
*
* @author: lixiangxiang
* @param quartzJob /
* @return void
*/
public void updateJobCron(QuartzJob quartzJob) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey( quartzJob.getId());
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//如果不存在创建一个定时任务
if(ObjectUtil.isNull(trigger)) {
addJob(quartzJob);
trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
}
//按新的cronExpression表达式重新构建trigger
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(quartzJob.getCronExpression());
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
//重置启动时间
((CronTriggerImpl)trigger).setStartTime(new Date());
//重新将quartzJob放入map
trigger.getJobDataMap().put(QuartzJob.JOB_KEY,quartzJob);
//按新的trigger重新设置scheduler
scheduler.rescheduleJob(triggerKey,trigger);
//判断任务是否暂停
if (quartzJob.getIsPause()) {
pauseJob(quartzJob);
}
} catch (Exception e) {
log.error(StrUtil.format("【更新定时任务失败】定时任务id:{}",quartzJob.getId()));
}
}
前面一部分代码也是为了确定该job是存在的,保证获得trigger不为空。
中间有详细的注释就不再提了。
重置scheduler时调用的rescheduleJob会导致job重新开始执行,这时候我们需要判断quartzJob的isPause是否为true,如果为true则暂停任务。
立即执行一个job
有时候我们需要立即执行一个任务,但不触发他的trigger,就是只执行一次。
/**
* description: 立即执行任务
*
* @author: lixiangxiang
* @param quartzJob /
* @return void
*/
public void runJobNow(QuartzJob quartzJob) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(JOB_NAME + quartzJob.getId());
CronTrigger trigger = (CronTrigger)scheduler.getTrigger(triggerKey);
//如果不存在创建一个定时任务
if(ObjectUtil.isNull(trigger)) {
addJob(quartzJob);
}
//将quartzjob的信息再放入JobDataMap中
JobDataMap dataMap = new JobDataMap();
dataMap.put(QuartzJob.JOB_KEY,quartzJob);
JobKey jobKey = JobKey.jobKey(quartzJob.getId());
//只执行一次job
scheduler.triggerJob(jobKey,dataMap);
} catch (Exception e) {
log.error(StrUtil.format("【定时任务执行失败】定时任务id:{}",quartzJob.getId()),e);
}
}
删除一个job
/**
* 删除一个job
* @param quartzJob /
*/
public void deleteJob(QuartzJob quartzJob){
try {
JobKey jobKey = JobKey.jobKey(quartzJob.getId());
//暂停任务
scheduler.pauseJob(jobKey);
//删除任务
scheduler.deleteJob(jobKey);
} catch (Exception e){
log.error(StrUtil.format("【删除定时任务失败】定时任务id:{}",quartzJob.getId()));
}
}
到此为止quartz的动态化操作已经基本结束。
现在我们还需要解决一个问题,如何持久化储存我们的定时任务。
定时任务的持久化操作(以下方案参考el-admin)
关于定时任务的持久化操作,其实quartz框架提供了jobStore。
但本篇文章的持久化操作并不是通过该方法实现的。
如果需要用quartz默认的持久化方法可以看https://blog.csdn.net/hxnlyw/article/details/88181226
quartz的jobStore使用需要导入很多张表,这对项目开发不太友好,我们能不能只用一个表就完成持久化操作呢。
其实前面的动态化操作已经帮我们解决了这个问题。
我们只需要对quartzJob进行持久化储存即可。
接下来我会演示如何实现定时任务持久化 增删改 操作(使用技术mybatis-plus)
增加一个定时任务
public void create(QuartzJob resources) {
//验证cron表达式是否正确,如果不正确抛出异常
if (!CronExpression.isValidExpression(resources.getCronExpression())){
throw new BadRequestException("cron表达式格式错误");
}
//数据库保存数据
quartzJobMapper.insert(resources);
//quartz增加任务
quartzManage.addJob(resources);
}
修改定时任务
public void update(QuartzJob resources) {
if(!CronExpression.isValidExpression(resources.getCronExpression())) {
throw new BadRequestException("cron表达式错误");
}
//通过id修改
quartzJobMapper.updateById(resources);
//更新定时任务cron表达式
quartzManage.updateJobCron(resources);
}
删除定时任务(可以删除多个)
public void delete(Set<Long> ids) {
for(Long id : ids) {
QuartzJob quartzJob = findById(id);
//quartz删除定时任务
quartzManage.deleteJob(quartzJob);
//数据库删除该job信息·
quartzJobMapper.deleteById(id);
}
}
立即执行定时任务
public void execution(QuartzJob quartzJob) {
quartzManage.runJobNow(quartzJob);
}
到此为止定时任务的持久化 操作就结束了
你以为这就没有了。。。当然不是,还记得我们之前创建的job吗·
/**
* @author lixiangxiang
* @description 执行bean的方法
* @date 2021/1/25 8:57
*/
public class PrintHelloJob extends QuartzJobBean {
@Autowired
PrintHelloService printHelloService;
@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
printHelloService.printHello();
}
}
我们这个job的目的是打印hello。但是未来我们的job肯定不会如此简单,可能我们需要同时执行两个或多个定时任务。
那么这个时候就出现了新的问题,
- 我们必须每次都创建一个新的job类,很麻烦。
- 我们可能需要做一个后台页面来控制定时任务,但是我们绑定定时任务时需要传入job的实现类,前端不可能为我们传一个类过来。
- 我们如何让多个定时任务同时执行。
// 构建jobDetail,并与PrintHelloJob类绑定(Job执行内容)
JobDetail jobDetail = JobBuilder.newJob(PrintHelloJob.class).
withIdentity(quartzJob.getId()).build();
我们能不能把所有定时任务放在一个类中,执行某一个方法即可。或者我们只用绑定一个job类,我们一直用该类作为模板创建JobDetail实例,通过传入的job信息来确定执行的方法。
改进方案(参考el-admin)
- JobDetail只绑定一个Job实现类
- 在Job实现类中,我们通过bean对象,方法名,参数 反射执行需要执行的方法
- 使用异步线程池实现多个定时任务同时进行
首先我们将定时任务的所有信息封装到quartzJob对象中(@ApiModelProperty swagger-ui的注解)
@Data
public class QuartzJob implements Serializable {
public static final String JOB_KEY = "JOB_KEY";
@ApiModelProperty(value = "ID")
private Long id;
@ApiModelProperty(value = "用于子任务唯一标识", hidden = true)
private String uuid;
@ApiModelProperty(value = "定时器名称")
private String jobName;
@ApiModelProperty(value = "Bean名称")
private String beanName;
@ApiModelProperty(value = "方法名称")
private String methodName;
@ApiModelProperty(value = "参数")
private String params;
@ApiModelProperty(value = "cron表达式")
private String cronExpression;
@ApiModelProperty(value = "状态,暂时或启动")
private Boolean isPause = false;
@ApiModelProperty(value = "子任务")
private String subTask;
@ApiModelProperty(value = "失败后暂停")
private Boolean pauseAfterFailure;
}
创建一个job的实现类
public class ExecutionJob extends QuartzJobBean {
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
//通过JobExecutionContext对象得到QuartzJob实例。
QuartzJob quartzJob =(QuartzJob) context.getMergedJobDataMap().get(QuartzJob.JOB_KEY);
//反射获取到方法,并执行。
runMethod(quartzJob.getBeanName(),quartzJob.getMethodName(),quartzJob.getParams());v
}
/***
* description:反射执行方法
*
* @author: lixiangxiang
*/
public void runMethod(String beanName,String methodName,String params) {
Object target = SpringContextHolder.getBean(beanName);
Method method = null;
try{
//执行的方法只能有两种,有String参数或者无参数,毕竟前端只能传字符串参数给后端。
if(StringUtils.isNotBlank(params)) {
//反射获取到方法 两个参数 分别是方法名和参数类型
method = target.getClass().getDeclaredMethod(methodName,String.class);
}else {
method = target.getClass().getDeclaredMethod(methodName);
}
//反射执行方法
ReflectionUtils.makeAccessible(method);
if(StringUtils.isNotBlank(params)) {
method.invoke(target,params);
}else {
method.invoke(target);
}
} catch (Exception e){
throw new BadRequestException("定时任务执行失败");
}
}
上文提过我们可以使用JobExecutionContext获取到jobDataMap
获得jobDataMap的方式有三种
//这个是获取到Trigger中的jobDataMap
context.getTrigger().getJobDataMap();
//这个是获取到JobDetail中的jobDataMap
context.getJobDetail().getJobDataMap()
//这个是将上面两者合并后获得的DataMap,如果有相同的key,则有一个会被覆盖
context.getMergedJobDataMap()
但是这样写并不能同时执行多个定时任务
创建线程类
我们可以把反射执行方法的过程写到一个线程类,然后放入线程池中。
/**
* @author lixiangxiang
* @description 执行定时任务
* @date 2021/1/15 9:57
*/
@Slf4j
public class QuartzRunnable implements Callable<Object> {
private final Object target;
private final Method method;
private final String params;
QuartzRunnable(String beanName,String methodName,String params) throws NoSuchMethodException, ClassNotFoundException {
//获取到bean对象
this.target = SpringContextHolder.getBean(beanName);
//获取到参数
this.params = params;
//如果参数不为空
if(StringUtils.isNotBlank(params)) {
//反射获取到方法 两个参数 分别是方法名和参数类型
this.method = target.getClass().getDeclaredMethod(methodName,String.class);
}else {
this.method = target.getClass().getDeclaredMethod(methodName);
}
}
/***
* description: 线程回调函数 反射执行方法
*
* @author: lixiangxiang
*/
@Override
public Object call() throws Exception {
ReflectionUtils.makeAccessible(method);
if(StringUtils.isNotBlank(params)) {
method.invoke(target,params);
}else {
method.invoke(target);
}
return null;
}
}
改进ExecutionJob 增加异步注解
@Async
public class ExecutionJob extends QuartzJobBean {
//获取线程池
private final static ThreadPoolExecutor EXECUTOR = ThreadPoolExecutorUtil.getPoll();
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
//通过JobExecutionContext对象得到QuartzJob实例。
QuartzJob quartzJob =(QuartzJob) context.getMergedJobDataMap().get(QuartzJob.JOB_KEY);
//执行任务
try{
//创建定时任务线程
QuartzRunnable task = new QuartzRunnable(quartzJob.getBeanName(),quartzJob.getMethodName(),quartzJob.getParams());
//执行任务并返回future,可以通过future获取线程状态
Future<?> future = EXECUTOR.submit(task);
}catch {
throw new BadRequestException("定时任务执行失败");
}
}
}
我们可以再建一个日志信息类
把任务执行的具体日志信息存入数据库中
@Data
public class QuartzLog implements Serializable {
@ApiModelProperty(value = "ID", hidden = true)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ApiModelProperty(value = "任务名称", hidden = true)
private String jobName;
@ApiModelProperty(value = "bean名称", hidden = true)
private String beanName;
@ApiModelProperty(value = "方法名称", hidden = true)
private String methodName;
@ApiModelProperty(value = "参数", hidden = true)
private String params;
@ApiModelProperty(value = "cron表达式", hidden = true)
private String cronExpression;
@ApiModelProperty(value = "状态", hidden = true)
private Boolean isSuccess;
@ApiModelProperty(value = "异常详情", hidden = true)
private String exceptionDetail;
@ApiModelProperty(value = "执行耗时", hidden = true)
private Long time;
@ApiModelProperty(value = "创建时间", hidden = true)
private Timestamp createTime;
}
ExecutionJob加入日志
@Async
public class ExecutionJob extends QuartzJobBean {
//获取线程池
private final static ThreadPoolExecutor EXECUTOR = ThreadPoolExecutorUtil.getPoll();
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
//通过JobExecutionContext对象得到QuartzJob实例。
QuartzJob quartzJob =(QuartzJob) context.getMergedJobDataMap().get(QuartzJob.JOB_KEY);
//使用SpringContextHolder获取bean实例
QuartzLogMapper quartzLogMapper = SpringContextHolder.getBean(QuartzLogMapper.class);
QuartzJobService quartzJobService = SpringContextHolder.getBean(QuartzJobService.class);
String uuid = quartzJob.getUuid();
QuartzLog quartzLog = new QuartzLog();
quartzLog.setJobName(quartzJob.getJobName());
quartzLog.setBeanName(quartzJob.getBeanName());
quartzLog.setMethodName(quartzJob.getMethodName());
quartzLog.setParams(quartzJob.getParams());
long startTime = System.currentTimeMillis();
quartzLog.setCronExpression(quartzJob.getCronExpression());
//执行任务
try {
log.info("-----------------------------------");
log.info(StrUtil.format("【定时任务开始执行】 任务名称: {} ", quartzJob.getJobName()));
//创建定时任务线程
QuartzRunnable task = new QuartzRunnable(quartzJob.getBeanName(),quartzJob.getMethodName(),quartzJob.getParams());
//用future管理task
Future<?> future = EXECUTOR.submit(task);
future.get();
//计算运行时间
long times = System.currentTimeMillis() - startTime;
quartzLog.setTime(times);
if(StringUtils.isNotBlank(uuid)){
//将执行结果存入redis,以uuid为唯一标识
redisUtils.set(uuid,true);
}
//任务状态
quartzLog.setIsSuccess(true);
log.info(StrUtil.format("【定时任务执行完毕】 任务名称: {}, 执行时间: {} ms", quartzJob.getJobName(),times));
log.info("------------------------------------------------------")
} catch (Exception e) {
//保存执行状态
if (StringUtils.isNotBlank(uuid)) {
redisUtils.set(uuid, false);
}
log.info(StrUtil.format("【任务执行失败】任务名称: {} ", quartzJob.getJobName()));
log.info("-------------------------------------------");
long times = System.currentTimeMillis() - startTime;
quartzLog.setTime(times);
//任务状态 0; 成功 1 ;失败 0
quartzLog.setIsSuccess(false);
//存入异常信息
quartzLog.setExceptionDetail(ThrowableUtil.getStackTrace(e));
//如果任务失败则暂停
if (quartzJob.getPauseAfterFailure() != null && quartzJob.getPauseAfterFailure()) {
quartzJob.setIsPause(false);
//更新状态,让任务暂停
quartzJobService.updateIsPause(quartzJob);
}
e.printStackTrace();
throw new BadRequestException("定时任务执行失败");
}finally {
//日志信息存入数据库
quartzLogMapper.insert(quartzLog);
}
}
}
smpe-admin任务调度模块的使用方法。
最后再介绍下smpe-admin任务调度模块的使用方法。
点击新增增加一个定时任务,下面有具体参数介绍
点击执行,立即执行一个任务
你还可以进行定时任务修改、删除、暂停等操作。
新增定时任务的部分参数介绍
- bean名称: 定时任务通过bean名称来获取具体执行的bean对象。需要执行的定时任务类,必须注入spring容器中。
- 执行方法: 需要执行的方法名称,底层是通过反射执行方法。
- cron表达式:定时任务通过cron表达式控制任务执行的时间。
- 子任务id:子任务可以是当前已经定义过的任务的id,传入时需要用多个逗号隔开,当主任务执行后,子任务按顺序依次执行。
- 告警邮箱:定时任务执行失败时会将失败信息通过邮箱发送给用户。如果有多个邮箱可以用逗号隔开,如果不需要则不用填。(该功能暂不支持)
- 失败后暂停:选择定时任务失败后是否暂停当前定时任务。
- 任务状态:选择是否开启当前定时任务。
- 参数内容: 填写参数内容,可向后端传一个字符串参数,具体使用方法见下图
前端可以根据该参数向后端传需要执行的内容。
最最最后附上smpe-admin的github地址,smpe-admin是我们团队基于el-admin自己整合的一个框架,定时任务模块是我负责的。谢谢大家支持哦
项目源码
后端源码 | 前端源码 | |
---|---|---|
GitHub | https://github.com/sanyueruanjian/smpe-admin | https://github.com/sanyueruanjian/smpe-admin-web |