一、Quartz介绍
1.1 介绍说明
Quartz 是 OpenSymphony 开源组织在任务调度领域的一个开源项目,自2001年发布以来,已经被众多项目作为任务调度的解决方案。
Quartz是一个任务调度框架,完全基于 Java 实现,该项目于 2009 年被 Terracotta 收购目前是 Terracotta 旗下的一个项目
1.2 地址和类库说明
官方地址:http://www.quartz-scheduler.org/
从官方地址下载quartz-2.3.0-distribution.tar.gz包(当时最新的稳定版本),然后解压开如下:
目录说明
- examples目录放的是官方提供的15个Demo实例
- javadoc目录是Quartz官方的API文档,HTML格式的
- lib目录放的是Quartz类库jar包
- src目录放的是Quartz类库源码和sql文件
在quartz-2.3.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore目录下面有官方提供的各种数据库表sql,比如:tables_mysql_innodb.sql,在集群配置的时候需要用到。如果是quartz-2.3.以下版本,则在quartz-2.2.3\docs\dbTables目录下存放各种数据库的表sql
1.3 Quartz基础结构
Quartz有调度器、任务和触发器3个核心概念,以下分别介绍说明
-
Job:是一个接口,只有一个方法void execute(JobExecutionContext var1) throws JobExecutionException,开发者通过实现这个接口来定义需要执行的任务,JobExecutionContext 类提供了调度上下文的各种信息,job运行时的信息保存在JobDataMap中
-
JobDetail: Quartz在每次执行job时,都重新创建一个Job实例,所以它不是直接接收一个Job实例而是接收一个Job实现类,以便运行时通过反射调用机制实例化Job。因此需要通过一个类来描述Job的实现类及其他相关的静态信息,如Job名称,描述等信息,JobDetail接口相当于将Job接口包装了一下。
-
Trigger:触发器描述触发Job执行的时间触发规则。有CronTrigger、CalendarIntervalTrigger、DailyTimeIntervalTrigger、SimpleTrigger等4个子接口,其中CronTrigger用的比较多。
-
Scheduler:任务调度器,是实际执行任务调度的控制器。Quartz通过Scheduler调度器来注册、暂停、删除Trigger和JobDetail。Trigger和JobDetail在Scheduler容器中拥有各自的组及名称,组和名称是Scheduler查找定位容器中某一个对象的依据,Trigger的组及名称的组和必须唯一,JobDetail的组及名称的组和也必须唯一(可以和Trigger的组及名称相同)。Scheduler将Trigger绑定到某一JobDetail中,这样当Trigger被触发时,对应的Job就被执行。一个Job可以对应多个Trigger,但一个Trigger只能对应一个Job。
-
SchedulerFactory:任务调度器工厂,用于创建Scheduler实例,Spring还提供了SchedulerFactoryBean类,封装了SchedulerFactory。SchedulerFactory有两个实现:DirectSchedulerFactory和 StdSchdulerFactory。前者可以用来在代码里定制你自己的Schduler参数。后者是直接读取classpath下的quartz.properties(不存在就都使用默认值)配置来实例化Schduler。通常来讲,我们使用StdSchdulerFactory也就足够了。
-
ThreadPool 线程池,Quartz有自己的线程池实现。所有任务的都会由线程池执行
JobDetail和Trigger都有name属性和group属性。name属性是它们在这个Sheduler里面的唯一标识。group属性是一个组织单元,sheduler会提供一些对整组操作的API,比如 scheduler.resumeJobs(GroupMatcher matcher)),就是恢复某一个分组里的所有JobDetails
Quartz有完善的事件和监听体系,大部分组件都拥有事件,例如在任务执行前的事件、任务执行后事件、触发器触发前事件、触发器触发后事件、调度器开始事件、调度器关闭事件等等。
一个Scheduler可以拥有多个Trigger和多个JobDetail,它们可以分到不同的组中。在注册Trigger和JobDetail时,如果不显示指定所属组,那Scheduler将放入默认组中,默认组的组名为Scheduler.DEFAULT_GROUP。所属组名和名称组成了对象的全名,同一类对象的全名不能相同。
当Scheduler比较繁忙或者资源不足(线程池)的时候,可能存在同一个时刻,有多个Trigger被触发了,这时候可以指定Trigger的优先级,优先级高的先执行。需要注意的是设置优先级只有在同一时刻执行的Trigger之间才会起作用,优先级的值默认是 Thread.NORM_PRIORITY即5。
Misfire(错失触发):当Scheduler资源不足的时候或者机器崩溃重启时,有可能导致某一些Trigger在应该触发的时间点没有被触发,也就是Miss Fire了。这个时候Trigger需要一个策略来处理这种情况。也就是Trigger到了该触发执行的时候,上一个执行还未完成,并且线程池中没有空闲线程可以使用(或有空闲线程可以使用但job设置为@DisallowConcurrentExecution)且过期时间已经超过misfireThreshold就认为是misfire了,错失触发了
二、Spring Boot集成Quartz
Jdk1.8
Spring Boot 2.3.8.RELEASE
Mybatis-plus 3.4.2
2.1添加quartz依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
2.2 Cron Triggers简单例子
SimpleJob.java
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobKey;
import java.util.Date;
/**
* @author
* @Description 简单任务
* @Date 2021/4/10 23:00
* @Version 1.0
*/
public class SimpleJob implements Job {
/**
* 日志工具
*/
Log logger = LogFactory.get();
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobKey jobKey = jobExecutionContext.getJobDetail().getKey();
logger.info("简单任务(SimpleJob) says: " + jobKey + " executing at " + new Date());
}
}
QuartzTest.java
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
/**
* @author David
* @Description TODO
* @Date 2021/4/10 22:59
* @Version 1.0
*/
public class QuartzTest {
public static void main(String[] args) {
try {
//Scheduler 调度器实例
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
// 创建jobDetail实例,绑定Job实现类
JobDetail job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("job1", "group1").build();
//定义调度触发规则,这里使用cornTrigger规则,每10S触发一次
CronTrigger trigger = TriggerBuilder
.newTrigger().withIdentity("trigger1", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
//指定trigger优先级
.withPriority(Thread.MIN_PRIORITY)
.build();
// 把作业和触发器注册到任务调度中
scheduler.scheduleJob(job, trigger);
//第二个job
job = JobBuilder.newJob(SimpleJob.class)
.withIdentity("job2", "group1").build();
//第二个触发器,每16秒跑一次
trigger = TriggerBuilder
.newTrigger().withIdentity("trigger2", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0/16 * * * * ?"))
.build();
scheduler.scheduleJob(job, trigger);
//启动调度器
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
执行QuartzTest 类就可以看到打印的结果
三、自定义动态配置任务
假设Web项目需要能够实时的启动或暂停某一个任务,添加的新任务要保存到数据库中,并且项目重启的时候要能够把有效状态的定时器自动启动执行,要记录每个任务执行的时间。
3.1创建表存储任务信息
这里就先不用Quartz官方提供的表结构来存储任务,自己定一个MySql表结构如下:
CREATE TABLE `t_system_shedule` (
`ID` VARCHAR(32) NOT NULL COMMENT '定时ID' COLLATE 'utf8_general_ci',
`SHEDULE_NAME` VARCHAR(128) NULL DEFAULT NULL COMMENT '定时器名称' COLLATE 'utf8_general_ci',
`SHEDULE_CODE` VARCHAR(32) NULL DEFAULT NULL COMMENT '定时器编码' COLLATE 'utf8_general_ci',
`SHEDULE_CRON` VARCHAR(256) NULL DEFAULT NULL COMMENT '定时器CRON表达式' COLLATE 'utf8_general_ci',
`SHEDULE_CLASSNAME` VARCHAR(256) NULL DEFAULT NULL COMMENT '定时器处理类' COLLATE 'utf8_general_ci',
`SHEDULE_STATUS` CHAR(1) NULL DEFAULT '1' COMMENT '定时器状态(1:启用:-1:停用)' COLLATE 'utf8_general_ci',
`SHEDULE_BINDIP` CHAR(16) NULL DEFAULT NULL COMMENT '绑定调度的IP地址' COLLATE 'utf8_general_ci',
`SHEDULE_GROUP` VARCHAR(128) NULL DEFAULT 'DEFAULT' COMMENT '定时器分组' COLLATE 'utf8_general_ci',
`SHEDULE_DESCRIPTION` VARCHAR(512) NULL DEFAULT NULL COMMENT '定时器描述' COLLATE 'utf8_general_ci',
`CREATE_TIME` DATETIME NULL DEFAULT NULL COMMENT '创建时间',
`UPDATE_TIME` DATETIME NULL DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`ID`) USING BTREE
)
COMMENT='定时任务表'
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;
3.2 Quartz配置
applicatiion.yml
spring:
profiles:
active: dev
application:
name: fire
quartz:
# 将任务等持久化到数据库
#job-store-type: jdbc
# 程序结束时会等待quartz相关的内容结束
wait-for-jobs-to-complete-on-shutdown: true
properties:
org:
quartz:
threadPool:
class: org.quartz.simpl.SimpleThreadPool
#线程数
threadCount: 4
# 线程优先级
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
#.....还有Druid数据源配置,就不展示了
QuartzConfig.java
import com.soft.fire.listener.QuartzJobListener;
import org.quartz.Scheduler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;
/**
* @author David
* @Description quartz定时任务配置
* @Date 2021/4/8 15:20
* @Version 1.0
*/
@Configuration
public class QuartzConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
//指定JobFactory,用于自定义Job中注入SpringBean
schedulerFactoryBean.setJobFactory(new SpringBeanJobFactory());
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContextKey");
//设置全局任务监听器
schedulerFactoryBean.setGlobalJobListeners(new QuartzJobListener());
return schedulerFactoryBean;
}
/**
* 创建scheduler
*
* @return
*/
@Bean
public Scheduler quartzScheduler() {
Scheduler scheduler = schedulerFactoryBean().getScheduler();
//添加TriggerListener监听器
//scheduler.getListenerManager().addTriggerListener();
//添加 SchedulerListener监听器,可以监听JobDetail的部署、卸载、暂停和恢复
//scheduler.getListenerManager().addSchedulerListener();
//添加局部任务监听器
/* scheduler.getListenerManager()
.addJobListener(new QuartzJobListener(),
KeyMatcher.keyEquals(JobKey.jobKey("jobname","groupname")));*/
return scheduler;
}
}
QuartzJobListener.java,任务监听可以用于记录日志
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
/**
* @author David
* @Description 定时器执行监听器,用于跟踪定时器执行过程状态变化
* @Date 2021/4/9 9:29
* @Version 1.0
*/
public class QuartzJobListener implements JobListener {
/**
* 日志操作对象
*/
private Log logger = LogFactory.get();
/**
* 返回一个字符串用以说明 JobListener 的名称
* 对于注册为全局的监听器,getName() 主要用于记录日志
* 对于由特定 Job 引用的 JobListener,注册在 JobDetail 上的监听器名称必须匹配从监听器上 getName() 方法的返回值。
*
* @return
*/
@Override
public String getName() {
return this.getClass().getSimpleName();
}
/**
* Scheduler 在 JobDetail 将要被执行时调用这个方法。
*/
@Override
public void jobToBeExecuted(JobExecutionContext jobExecutionContext) {
String jobName = jobExecutionContext.getJobDetail().getKey().getName();
logger.info("--------------jobToBeExecuted定时器开始执行之前调用的监听方法---jobName={}------", jobName);
}
/**
* Scheduler 在 JobDetail 即将被执行,但又被 TriggerListener 否决了时调用这个方法。
*
* @param jobExecutionContext
*/
@Override
public void jobExecutionVetoed(JobExecutionContext jobExecutionContext) {
String jobName = jobExecutionContext.getJobDetail().getKey().getName();
logger.info("------------jobExecutionVetoed定时器开始执行过程调用的监听方法---jobName={}------", jobName);
}
/**
* Scheduler 在 JobDetail 被执行之后调用这个方法。
*/
@Override
public void jobWasExecuted(JobExecutionContext jobExecutionContext, JobExecutionException e) {
String jobName = jobExecutionContext.getJobDetail().getKey().getName();
logger.info("--------------jobWasExecuted定时器执行完成之后调用的监听方法---jobName={}------", jobName);
}
}
3.3 注册、卸载、暂停、恢复任务操作
SheduleServiceImpl.java
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.soft.fire.constant.enums.SheduleStatusEnum;
import com.soft.fire.platform.system.mapper.SheduleMapper;
import com.soft.fire.platform.system.model.Shedule;
import com.soft.fire.platform.system.model.SheduleDTO;
import com.soft.fire.platform.system.service.SheduleService;
import org.quartz.*;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 定时任务表 服务实现类
* </p>
*
* @author David
* @since 2021-04-08
*/
@Service
public class SheduleServiceImpl extends ServiceImpl<SheduleMapper, Shedule>
implements SheduleService {
/**
* 注入mapper依赖
*/
@Resource
private SheduleMapper sheduleMapper;
/**
* 注入scheduler
*/
@Resource
private Scheduler quartzScheduler;
/**
* 初始化定时任务
*/
@Override
public void initShedule() throws Exception {
//从自定义表里面查询所有启用状态的任务信息 注册到Scheduler中
List<Shedule> sheduleList = this.list(new QueryWrapper<Shedule>()
.eq("shedule_status", SheduleStatusEnum.QUARTZ_ENABLE.getValue()));
for (Shedule shedule : sheduleList) {
addScheduleJob(shedule);
}
}
/**
* 添加定时任务
*
* @param shedule
*/
@Override
public void addScheduleJob(Shedule shedule) throws Exception {
// 创建jobDetail实例,绑定Job实现类,然后指明job的名称,所在组的名称
JobDetail jobDetail = JobBuilder.newJob((Class<? extends Job>) Class.forName(shedule.getSheduleClassname()))
.withIdentity(shedule.getSheduleCode(), shedule.getSheduleGroup()).build();
// 定义调度触发规则,这里使用cornTrigger规则
CronTrigger cronTrigger = TriggerBuilder.newTrigger()
.withIdentity(shedule.getSheduleCode(), shedule.getSheduleGroup())
.startAt(DateBuilder.futureDate(1, DateBuilder.IntervalUnit.SECOND))
.withSchedule(CronScheduleBuilder.cronSchedule(shedule.getSheduleCron()))
.startNow()
.build();
// 把作业和触发器注册到任务调度中
quartzScheduler.scheduleJob(jobDetail, cronTrigger);
//启动
if (!quartzScheduler.isStarted()) {
quartzScheduler.start();
}
}
/**
* 新增定时任务
*
* @param sheduleDTO
*/
@Override
public void saveShedule(SheduleDTO sheduleDTO) throws Exception {
Shedule shedule = new Shedule();
BeanUtil.copyProperties(sheduleDTO, shedule);
this.addScheduleJob(shedule);
// 保存任务信息到自定义表
this.save(shedule);
}
/**
* 修改定时任务
*
* @param sheduleDTO
*/
@Override
public void modifyShedule(SheduleDTO sheduleDTO) throws Exception {
//修改的是启用状态的并且Scheduler已存在的任务
if (SheduleStatusEnum.QUARTZ_ENABLE.getValue().equals(sheduleDTO.getSheduleStatus())
&& quartzScheduler.checkExists(JobKey.jobKey(sheduleDTO.getSheduleCode(),
sheduleDTO.getSheduleGroup()))) {
TriggerKey triggerKey = TriggerKey.triggerKey(sheduleDTO.getSheduleCode(), sheduleDTO.getSheduleGroup());
CronTrigger trigger = (CronTrigger) quartzScheduler.getTrigger(triggerKey);
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(sheduleDTO.getSheduleCron());
trigger = trigger.getTriggerBuilder()
.withIdentity(triggerKey)
.withSchedule(cronScheduleBuilder).build();
quartzScheduler.rescheduleJob(triggerKey, trigger);
}
Shedule shedule = new Shedule();
BeanUtil.copyProperties(sheduleDTO, shedule);
//修改数据库任务信息
this.updateById(shedule);
}
/**
* 暂停定时器
*
* @param shedule
*/
@Override
public void pauseShedule(Shedule shedule) throws Exception {
if (quartzScheduler.checkExists(JobKey.jobKey(shedule.getSheduleCode(),
shedule.getSheduleGroup()))) {
quartzScheduler.pauseJob(new JobKey(shedule.getSheduleCode(),
shedule.getSheduleGroup()));
}
UpdateWrapper<Shedule> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("shedule_status", SheduleStatusEnum.QUARTZ_DISABLE.getValue());
updateWrapper.eq("id", shedule.getId());
//修改数据库状态
this.update(updateWrapper);
}
/**
* 恢复定时器
*
* @param shedule
*/
@Override
public void resumeShedule(Shedule shedule) throws Exception {
//先判断Scheduler中是否存在
if (quartzScheduler.checkExists(JobKey.jobKey(shedule.getSheduleCode(),
shedule.getSheduleGroup()))) {
quartzScheduler.resumeJob(new JobKey(shedule.getSheduleCode(), shedule.getSheduleGroup()));
} else {
//不存在就加入到Scheduler调度中
this.addScheduleJob(shedule);
}
UpdateWrapper<Shedule> updateWrapper = new UpdateWrapper<>();
updateWrapper.set("shedule_status", SheduleStatusEnum.QUARTZ_ENABLE.getValue());
updateWrapper.eq("id", shedule.getId());
//修改数据库状态
this.update(updateWrapper);
}
/**
* 删除定时器
*
* @param shedule
*/
@Override
public void deleteShedule(Shedule shedule) throws Exception {
if (quartzScheduler.checkExists(JobKey.jobKey(shedule.getSheduleCode(),
shedule.getSheduleGroup()))) {
quartzScheduler.deleteJob(new JobKey(shedule.getSheduleCode(), shedule.getSheduleGroup()));
}
//删除定义表里面的任务数据
this.removeById(shedule.getId());
}
}
3.4 Controller和Model
SheduleController.java
import cn.hutool.core.util.StrUtil;
import com.soft.fire.platform.system.model.Shedule;
import com.soft.fire.platform.system.model.SheduleDTO;
import com.soft.fire.platform.system.service.SheduleService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* <p>
* 定时任务表 前端控制器
* </p>
*
* @author David
* @since 2021-04-08
*/
@RestController
@RequestMapping("/system/shedule")
@Api(tags = "quartz定时任务API")
public class SheduleController {
/**
* 注入service
*/
@Resource
private SheduleService sheduleService;
/**
* 查询出所有的定时任务信息
*
* @return
*/
@GetMapping
@ApiOperation("查询出所有定时任务")
public List<Shedule> all() {
return sheduleService.list();
}
/**
* 新增定时任务
*
* @param sheduleDTO
* @return
*/
@PostMapping
@ApiOperation(value = "新增或修改定时任务", notes = "如果是修改操作需要ID属性")
public String shedule(@RequestBody SheduleDTO sheduleDTO) throws Exception {
//TODO 判断code是否重复
if (StrUtil.isBlank(sheduleDTO.getId())) {
sheduleService.saveShedule(sheduleDTO);
} else {
sheduleService.modifyShedule(sheduleDTO);
}
return "success";
}
/**
* 暂停定时器
*
* @param jobId
* @return
*/
@PostMapping("/pause")
@ApiOperation("暂停定时器")
public String pauseShedule(@RequestParam("jobId") String jobId) throws Exception {
Shedule shedule = sheduleService.getById(jobId);
sheduleService.pauseShedule(shedule);
return "success";
}
/**
* 恢复定时任务
*
* @param jobId
* @return
*/
@PostMapping("/resume")
@ApiOperation("恢复定时任务")
public String resumeShedule(@RequestParam("jobId") String jobId) throws Exception {
Shedule shedule = sheduleService.getById(jobId);
sheduleService.resumeShedule(shedule);
return "success";
}
@DeleteMapping("{jobId}")
@ApiOperation("删除定时任务")
public String deleteShedule(@PathVariable("jobId") String jobId) throws Exception {
Shedule shedule = sheduleService.getById(jobId);
sheduleService.deleteShedule(shedule);
return "success";
}
}
任务实体对象Shedule.java,SheduleDTO继承自Shedule
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.sql.Timestamp;
/**
* <p>
* 定时任务表
* </p>
*
* @author David
* @since 2021-04-08
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("t_system_shedule")
@ApiModel(value = "定时任务", description = "定时任务实体对象")
public class Shedule implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 定时ID
*/
@ApiModelProperty("定时ID")
private String id;
/**
* 定时器名称
*/
@ApiModelProperty(value = "定时器名称", required = true)
private String sheduleName;
/**
* 定时器编码
*/
@ApiModelProperty(value = "定时器编码", required = true)
private String sheduleCode;
/**
* 定时器CRON表达式
*/
@ApiModelProperty(value = "定时器CRON表达式", required = true)
private String sheduleCron;
/**
* 定时器处理类
*/
@ApiModelProperty(value = "定时器处理类", required = true)
private String sheduleClassname;
/**
* 定时器状态(1:启用:-1:停用)
*/
@ApiModelProperty(value = "定时器状态", hidden = true)
private String sheduleStatus;
/**
* 定时器分组
*/
@ApiModelProperty(value = "定时器分组", required = true)
private String sheduleGroup;
/**
* 定时器描述
*/
@ApiModelProperty(value = "定时器描述")
private String sheduleDescription;
/**
* 绑定调度的IP地址,如果是应用是负载均衡部署,则这个属性可以用到
* 如果是用Docker容器化部署的IP地址不固定就用不上
*/
@ApiModelProperty(value = "绑定调度的IP地址")
private String sheduleBindip;
/**
* 创建时间
*/
@ApiModelProperty(value = "创建时间", hidden = true)
@TableField(fill = FieldFill.INSERT)
private Timestamp createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.UPDATE)
@ApiModelProperty(value = "更新时间", hidden = true)
private Timestamp updateTime;
}
定时任务状态枚举类,SheduleStatusEnum.java
/**
* @author David
* @Description quartz定时任务状态枚举
* @Date 2021/4/8 16:18
* @Version 1.0
*/
public enum SheduleStatusEnum {
/**
* 定时任务启用
*/
QUARTZ_ENABLE("启用", "1"),
/**
* 定时任务禁用
*/
QUARTZ_DISABLE("停用", "0");
SheduleStatusEnum(String displayName, String value) {
this.displayName = displayName;
this.value = value;
}
/**
* 显示的枚举名称
*/
private String displayName;
/**
* 枚举值
*/
private String value;
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
3.5 SpringBoot启动监听
用于在SpringBoot项目启动的时候自动加载数据库里面的定时任务执行
QuartzJobInitListener.java
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;
import com.soft.fire.platform.system.service.SheduleService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author David
* @Description quartz定时任务初始化监听器, 在springboot项目启动之后执行
* @Date 2021/4/8 15:43
* @Version 1.0
*/
@Component
@Order(value = 1)
public class QuartzJobInitListener implements CommandLineRunner {
/**
* 日志工具
*/
private Log logger = LogFactory.get();
/**
* sheduleService
*/
@Resource
private SheduleService sheduleService;
@Override
public void run(String... args) throws Exception {
logger.info("---------开始执行quartz定时任务初始化!-----------");
try {
sheduleService.initShedule();
} catch (Exception e) {
logger.error("quartz初始化失败!!", e);
}
logger.info("---------quartz定时任务初始化工作结束!-----------");
}
}
3.6 新增一个任务测试下
用Swagger调试如下
四、集群配置(分布式任务)
quartz是支持分布式的,通过表来管理各节点之间的关系
4.1 运行tables_mysql_innodb.sql
执行 quartz-2.3.0-SNAPSHOT\src\org\quartz\impl\jdbcjobstore\tables_mysql_innodb.sql文件,创建Quartz官方提供的11张表
11张表说明
表名称 | 说明描述 |
---|---|
QRTZ_JOB_DETAILS | 存储每一个已配置的JOB的详细信息 |
QRTZ_TRIGGERS | 存储已配置的TRIGGER的信息 |
QRTZ_SIMPLE_TRIGGERS | 存储已配置的SIMPLE TRIGGER的信息 |
QRTZ_SIMPROP_TRIGGERS | xx |
QRTZ_CRON_TRIGGERS | 存储CRON TRIGGER,包括CRON表达式和时区信息 |
QRTZ_BLOB_TRIGGERS | TRIGGER作为BLOB类型存储(用于QUARTZ用户用JDBC创建他们自己定制的TRIGGER类型,JOBSTORE并不知道如何存储实例的时候) |
QRTZ_CALENDARS | 以BLOB类型存储QUARTZ的CALENDAR日历信息,QUARTZ可配置一个日历来指定一个时间范围 |
QRTZ_PAUSED_TRIGGER_GRPS | 存储已暂停的TRIGGER组的信息 |
QRTZ_FIRED_TRIGGERS | 存储与错失触发的TRIGGER相关的状态信息,以及相联JOB的执行信息 |
QRTZ_SCHEDULER_STATE | 存储少量的有关 SCHEDULER的状态信息,和别的 SCHEDULER 实例(假如是用于一个集群中) |
QRTZ_LOCKS | 存储程序的非观锁的信息(假如使用了悲观锁) |
4.2 分布式配置
application.yml修改为如下
spring:
profiles:
active: dev
application:
name: fire
quartz:
# 将任务等持久化到数据库
job-store-type: jdbc
# 程序结束时会等待quartz相关的内容结束
wait-for-jobs-to-complete-on-shutdown: true
properties:
org:
quartz:
#Scheduler 调度器属性配置
scheduler:
# 调度标识名 集群中每一个实例都必须使用相同的名称
instanceName: ClusterQuartz
# ID设置为自动获取 每一个必须不同
instanceId: AUTO
#配置JobStore
jobStore:
# 是否加入集群
isClustered: true
# 数据保存方式为数据库持久化
class: org.quartz.impl.jdbcjobstore.JobStoreTX
#数据库代理类,一般org.quartz.impl.jdbcjobstore.StdJDBCDelegate可以满足大部分数据库
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 表的前缀,默认QRTZ_
tablePrefix: QRTZ_
# 保证待执行的任务是锁定的,持有锁超时,数据库会自动释放
acquireTriggersWithinLock: true
# 调度实例失效的检查时间间隔 ms 检查集群的状态间隔
clusterCheckinInterval: 5000
# 事务的隔离级别 推荐使用默认级别 设置为true容易造成死锁和不可重复读的一些事务问题
txIsolationLevelSerializable: false
#配置ThreadPool
threadPool:
# 线程池的实现类(一般使用SimpleThreadPool即可满足几乎所有用户的需求)
class: org.quartz.simpl.SimpleThreadPool
#指定线程数,一般设置为1-100直接的整数,根据系统资源配置
threadCount: 4
# 线程优先级 默认值为Thread.NORM_PRIORITY(5)
threadPriority: 5
threadsInheritContextClassLoaderOfInitializingThread: true
#下面还有Druid数据源配置
QuartzConfig.java增加数据源配置
@Configuration
public class QuartzConfig {
/**
* 注入数据源
*/
@Resource
private DataSource dataSource;
@Bean
public SchedulerFactoryBean schedulerFactoryBean() {
SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();
//指定JobFactory,用于自定义Job中注入SpringBean
schedulerFactoryBean.setJobFactory(new SpringBeanJobFactory());
// QuartzScheduler启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录
schedulerFactoryBean.setOverwriteExistingJobs(true);
schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContextKey");
//设置全局任务监听器
schedulerFactoryBean.setGlobalJobListeners(new QuartzJobListener());
//设置自动启动, 默认就是true,这里可以不设置
//如果设置了false,SpringBoot应用启动的时候就不会自动去加载数据库里面的任务执行
schedulerFactoryBean.setAutoStartup(false);
//如果需要使用官方提供的11张表,则需要配置数据源
//使用Spring配置的dataSource替换quartz的dataSource
schedulerFactoryBean.setDataSource(dataSource);
return schedulerFactoryBean;
}
.......
4.3 修改SheduleServiceImpl
新增定时器
@Override
public void saveShedule(SheduleDTO sheduleDTO) throws Exception {
Shedule shedule = new Shedule();
BeanUtil.copyProperties(sheduleDTO, shedule);
this.addScheduleJob(shedule);
// 保存任务信息到自定义表
// this.save(shedule);
}
就是把原来操作自定表的代码去掉,其他方法也是稍微修改下就能使用。
稍微修改下原来的恢复、暂停、删除定时任务方法。
增加查询所有任务方法
/**
* 查询所有任务
*
* @return
*/
@Override
public List<SheduleDTO> listAllJobs() throws SchedulerException{
List<SheduleDTO> sheduleDTOList = new ArrayList<>();
Set<JobKey> jobKeys = quartzScheduler.getJobKeys(null);
jobKeys.forEach(jobKey ->{
SheduleDTO sheduleDTO = new SheduleDTO();
sheduleDTO.setSheduleCode(jobKey.getName());
sheduleDTO.setSheduleGroup(jobKey.getGroup());
try {
sheduleDTO.setSheduleClassname(quartzScheduler.getJobDetail(jobKey).getJobClass().getName());
List<? extends Trigger> triggers = quartzScheduler.getTriggersOfJob(jobKey);
sheduleDTO.setSheduleCron(((CronTrigger)triggers.get(0)).getCronExpression());
sheduleDTO.setSheduleStatus(quartzScheduler.getTriggerState(((CronTrigger) triggers.get(0)).getKey()).toString());
} catch (SchedulerException e) {
e.printStackTrace();
}
sheduleDTOList.add(sheduleDTO);
});
return sheduleDTOList;
}
SheduleController.java增加方法
@GetMapping("/all")
@ApiOperation("查询出集群配置下的所有定时任务")
public List<SheduleDTO> listAllJobs() throws SchedulerException {
return sheduleService.listAllJobs();
}
4.4 去除QuartzJobInitListener
默认情况下SchedulerFactoryBean.setAutoStartup(true);SpringBoot应用启动的时候就会自动去加载数据库里面的任务执行