前言
我写了一个简单的Demo项目,有需要的文末可获取项目github地址,该项目我会一直保持更新。
主要实现功能如下:
- 基于quartz2.3.1实现动态管理定时任务。
- 使用swagger实现接口文档。
- 前后端统一使用JSON格式交互。
- 使用Hutool工具类直接连接数据库,避免Job任务中不能使用Autowired问题。
swagger文档如下图:
后续文章
一、Quartz简介
Quartz是一款功能强大的任务调度框架,官网介绍说:“Quartz可以执行数以百计甚至数以万计的简单或复杂的定时任务,并且可以集成到任何Java应用程序中”。他具有以下功能:
- 任务管理:可动态对任务进行管理,包括创建、暂停、删除、恢复等。
- 任务持久化:任务可通过JDBC存储到数据库中。
- 任务监听:可以实现侦听器接口来捕获调度事件,以监视或控制作业/触发器的行为。
- 支持集群和JTA事务。
二、版本介绍
Quartz的最新稳定版本2.3.0于2017年4月19号发布,2018年3月15号发布了2.3.1版本,2.3.2版本仍在GitHub quartz-2.3.x分支下进行中!他们都需要基于JDK7版本,2.3.1版本与2.3.2版本相比于2.3.0没有大的变化,主要都是功能和代码的完善修复。目前Quartz的最新版本2.4.0在Github主分支下更新,该版本将JDK版本升级到了JDK8。更多详细信息可以看Quartz的变更日志:changelog.adoc
本文基于的2.3.1版本完成,使用2.3.0与2.3.2版本替换均不受影响。
三、概念介绍
Quartz中有三个核心元素:
- Job:被调度的定时任务。
- Trigger:用来定义定时任务的触发时间。
- Schedule:这是Quartz框架的核心,他是真正执行定时任务的控制器。
Schedule维护着一个定时任务和触发器的注册表,当两者注册之后,如果触发器到达规定时间触发的时候,Schedule负责执行与触发器关联的定时任务。值得注意的是一个定时任务可以对应多个触发器,当每个触发器触发的时候都会执行其关联的定时任务,但是一个触发器只能对应一个定时任务。
四、代码示例
看完上面的概念,那么具体需要怎么做呢?
1:导入依赖
本文使用Springboot 2.3.1版本,配置如下Maven依赖即可开始使用Quartz。
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.1</version>
</dependency>
2:创建定时任务类
Quartz框架中提供一个Job接口,源码如下:
public interface Job {
void execute(JobExecutionContext context) throws JobExecutionException;
}
当我们需要创建一个定时任务时,只需要实现该接口,并在execute()方法中写自己需要执行的任务即可。
该方法的参数JobExecutionContext主要用来保存定时任务执行期间的上下文信息,我们可以通过JobExecutionContext获取到定时任务的详细信息(后续文章中会详细介绍)。本例创建一个定时任务:打印定时任务的名称。
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
/**
* @author frost2
* @date 2020-09-30 4:11:00
*/
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
//获取我们设置的定时任务名称
String name = context.getJobDetail().getKey().getName();
System.out.println("name = " + name);
}
}
3:启动任务
我们通过如下代码可以启动刚刚创建定时任务HelloJob,他的执行的频率为每30秒执行一次,并始终重复执行。
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
/**
* @author frost2
* @date 2020-9-30 16:22:39
*/
public class StartJob {
public void addJob() throws SchedulerException {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
//定义JobDetail
JobDetail job = JobBuilder.newJob(HelloJob.class) //设置需要执行的定时任务类
.withIdentity("myJob", "group1") //设置任务名称和任务组名称,他们两者唯一标识一个JobDetail
.build(); //创建JobDetail
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "group1") //设置触发器名称和触发器组名称,他们两者唯一标识一个JobDetail
.startNow() //设置从当前时刻开始执行
//withSchedule用于设置我们使用哪种触发器,在真正使用的时候是调用这里设置的ScheduleBuilder.build创建一个真正的触发器,这里我们使用SimpleScheduleBuilder。总共有四种触发器,其中我们功能最强大、使用最多是CronScheduleBuilder这我们会在后面讲到。
.withSchedule(SimpleScheduleBuilder.simpleSchedule() //实例化一个SimpleScheduleBuilder
.withIntervalInSeconds(30) //设置没30秒执行一次
.repeatForever()) //设置始终重复执行
.build();
scheduler.scheduleJob(job, trigger); //关联Trigger和JobDetail并启动定时任务。
}
}
上面这段代码中可以看到几个关键内容:
- JobDetail是对定时任务Job的封装,他保存了定时任务的详细信息。
- 更关键的是Trigger,他决定了一个任务什么时候执行,怎么执行。
- 当调用
schedulerFactory.getScheduler()
之后,我们的应用程序只有调用scheduler.shutdown()
之后才会终止。 - 当执行
scheduler.start()
之后,Scheduler将处于“待机”模式,此时并不会触发Trigger执行定时任务。之后将定义好的Trigger和JobDetail通过scheduler.scheduleJob
关联之后,Scheduler才会触发Trigger执行任务。
(1) ScheduleBuilder
通过上面的例子可以看到,一个定时任务执行的关键信息都是通过withSchedule
这个方法来设置的,其源码如下:
public <SBT extends T> TriggerBuilder<SBT> withSchedule(ScheduleBuilder<SBT> schedBuilder) {
this.scheduleBuilder = schedBuilder;
return (TriggerBuilder<SBT>) this;
}
所以真正重要的就是ScheduleBuilder接口,用来定义任务执行的具体规则。它共有四个实现类,其中最重要也是实际开发中使用最多的是CronScheduleBuilder。CronScheduleBuilder的核心就是Cron表达式,我们只要将定义好的Cron表达式传递到CronScheduleBuilder中,Scheduler就会按照这个表达式定时执行Job任务。
(2) QuartzUtil 工具类
看完上面的内容,大致已经了解什么是quartz,并清楚如何创建一个quartz定时任务。下面是我封装的一个工具类,可以动态的时候对任务的CRUD。文末还提供一个Demo有需要的可以下载下载下来跑跑看。
package com.frost2.quartz.common.util;
import com.frost2.quartz.common.bean.Code;
import com.frost2.quartz.common.customException.CustomException;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.matchers.GroupMatcher;
import org.quartz.impl.triggers.CronTriggerImpl;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @author 陈伟平
* @date 2020-9-17 15:38:43
*/
public class QuartzUtil {
private static SchedulerFactory schedulerFactory = new StdSchedulerFactory();
private static Scheduler scheduler;
private final static String groupName = "DEFAULT";
static {
try {
scheduler = schedulerFactory.getScheduler();
} catch (SchedulerException e) {
throw new CustomException(Code.EXECUTION_ERROR);
}
}
/**
* 传入:任务名称、触发器名称、任务描述、要执行的任务类、cron表达式创建定时任务,
* 返回是否创建成功
* <p>
* 注:
* 在创建任务时未设置jobGroup和triggerGroup,Job创建后其均为默认值:DEFAULT,
* 因此新创建的任务的jobName和triggerName,均不能与之前任务的重复.
*
* @param jobName 任务名
* @param triggerName 触发器名
* @param description 对该任务的秒数(非必须)
* @param jobClass 要执行的任务
* @param cron cron表达式
* @return true:创建Job成功,false:创建Job失败
*/
public static <T extends Job> boolean addJob(String jobName, String triggerName, String description,
Class<T> jobClass, String cron) {
try {
scheduler.start();
JobDetail job = JobBuilder.newJob(jobClass)
.withIdentity(jobName, groupName)
.withDescription(description)
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(triggerName, groupName)
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
scheduler.scheduleJob(job, trigger);
return scheduler.isStarted();
} catch (Exception e) {
throw new CustomException(Code.EXECUTION_ERROR);
}
}
/**
* 修改一个任务的触发时间
*
* @param jobName 任务名称
* @param triggerName 触发器名
* @param cron cron表达式
* @return true:修改Job成功,false:修改Job失败
*/
public static Boolean rescheduleJob(String jobName, String triggerName, String cron) {
try {
TriggerKey triggerKey = TriggerKey.triggerKey(triggerName);
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
if (trigger == null) {
return false;
}
checkJobNameAndTriggerName(jobName, trigger);
Date latestFireTime = new Date();
if (!trigger.getCronExpression().equalsIgnoreCase(cron)) {
trigger = TriggerBuilder.newTrigger()
.withIdentity(triggerName)
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.build();
//rescheduleJob()执行成功返回最近一次执行的时间,如果失败返回null
latestFireTime = scheduler.rescheduleJob(triggerKey, trigger);
}
return null != latestFireTime;
} catch (SchedulerException e) {
throw new CustomException(Code.EXECUTION_ERROR);
}
}
/**
* 根据jobName和triggerName删除该JOB
*
* @param jobName 任务名称
* @param triggerName 触发器名
* @return true:删除Job成功,false:删除Job失败
*/
public static boolean removeJob(String jobName, String triggerName) {
boolean flag;
try {
Trigger trigger = scheduler.getTrigger(new TriggerKey(triggerName));
if (null == trigger) {
return false;
}
checkJobNameAndTriggerName(jobName, trigger);
TriggerKey triggerKey = trigger.getKey();
scheduler.pauseTrigger(triggerKey);
flag = scheduler.unscheduleJob(triggerKey);
//删除trigger之后无需在删除job,因为相关的job如果不是持久的,则将被自动删除。下面这种写法flag=false
// if (flag) {
// flag = scheduler.deleteJob(JobKey.jobKey(jobName));
// }
} catch (SchedulerException e) {
throw new CustomException(Code.EXECUTION_ERROR);
}
return flag;
}
/**
* 根据jobName和triggerName查询该JOB
*
* @param jobName 任务名称
* @param triggerName 触发器名
* @return 该Job相关信息[详见getJobInfo方法]
*/
public static HashMap<String, String> getJob(String jobName, String triggerName) {
try {
JobDetail jobDetail = scheduler.getJobDetail(new JobKey(jobName));
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(new TriggerKey(triggerName));
if (null == jobDetail || null == trigger) {
return new HashMap<>();
}
checkJobNameAndTriggerName(jobName, trigger);
return getJobInfo(jobDetail, trigger);
} catch (SchedulerException e) {
throw new CustomException(Code.EXECUTION_ERROR);
}
}
/**
* 查询所有正在执行的JOB
*
* @return 该Job相关信息[详见getJobInfo方法]
*/
public static List<HashMap<String, String>> getJobs() {
List<HashMap<String, String>> list = new ArrayList<>();
try {
List<String> triggerGroupNames = scheduler.getTriggerGroupNames();
for (String groupName : triggerGroupNames) {
GroupMatcher<TriggerKey> groupMatcher = GroupMatcher.groupEquals(groupName);
//获取所有的triggerKey
Set<TriggerKey> triggerKeySet = scheduler.getTriggerKeys(groupMatcher);
for (TriggerKey triggerKey : triggerKeySet) {
//获取CronTrigger
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//获取trigger对应的JobDetail
JobDetail jobDetail = scheduler.getJobDetail(trigger.getJobKey());
list.add(getJobInfo(jobDetail, trigger));
}
}
} catch (SchedulerException e) {
throw new CustomException(Code.EXECUTION_ERROR);
}
return list;
}
/**
* @param cron cron表达式
* @return 最近5次的执行时间
*/
public static List<String> getRecentTriggerTime(String cron) {
List<String> list = new ArrayList<>();
try {
CronTriggerImpl cronTriggerImpl = new CronTriggerImpl();
cronTriggerImpl.setCronExpression(cron);
List<Date> dateList = TriggerUtils.computeFireTimes(cronTriggerImpl, null, 5);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
dateList.forEach(date -> list.add(dateFormat.format(date)));
} catch (ParseException e) {
throw new CustomException(Code.EXECUTION_ERROR);
}
return list;
}
/**
* 校验cron表达式是否正确
*
* @param cronExpression cron表达式
* @return true:正确,false:不正确
*/
@SuppressWarnings("all")
public static boolean checkCronExpression(String cronExpression) {
return CronExpression.isValidExpression(cronExpression);
}
/**
* 获取该Job对应的相关信息
*/
private static HashMap<String, String> getJobInfo(JobDetail jobDetail, CronTrigger trigger) {
HashMap<String, String> map = new HashMap<>();
map.put("jobName", jobDetail.getKey().getName());
map.put("jobGroup", jobDetail.getKey().getGroup());
map.put("corn", trigger.getCronExpression());
map.put("triggerName", trigger.getKey().getName());
map.put("description", jobDetail.getDescription());
return map;
}
/**
* 校验jobName和triggerName是否匹配
* 如不匹配抛出自定义异常
*/
private static void checkJobNameAndTriggerName(String jobName, Trigger trigger) {
String name = trigger.getJobKey().getName();
if (!name.equals(jobName)) {
throw new CustomException(Code.PARAM_FORMAT_ERROR.getCode(), "jobName与triggerName不匹配");
}
}
}
五、GitHub
https://github.com/frost-2/QuartzManager
六、声明
本人会经常更新博客,并在文章附上更新时间! 转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/weipinggg/article/details/108753457
欢迎大家关注我的公众号:frost2