前面一篇文章(SpringBoot-定时任务)中介绍了如何用SpringBoot框架中的注解方式来实现定时任务,这种方式的好处是不使用第三方的依赖,仅凭几个方便的注解,即可编写一个简单的定时任务处理。
实际开发中为了满足复杂的业务场景,比如多个节点之间的任务切换、恢复等,对任务本身的暂停、启动、执行时间修改等操作,使用简单的定时任务就很难满足了。这一节我们来学习SpringBoot集成Quartz框架来实现复杂的任务调度处理。
Quartz是一个由Java语言编写的开源任务调度框架,具有简单高效、容错、支持分布式等优点。
主要API:
Quartz提供两种基本作业存储类型。第一种类型叫做RAMJobStore,第二种类型叫做JDBC作业存储。
RAMJobStore的好处是不需要外部数据库的支持,作业存储在内存中,执行效率优于数据库保存的方式。缺点是一旦应用奔溃或者服务器宕机,任务作业数据就会丢失,所以这种方式是不能支持分布式部属的。
这里我们使用JDBC类型的存储方式,需要执行以下SQL语句:
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
DROP TABLE IF EXISTS QRTZ_LOCKS;
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
DROP TABLE IF EXISTS QRTZ_CALENDARS;
-- 存储每一个已配置的Job的详细信息
CREATE TABLE QRTZ_JOB_DETAILS(
SCHED_NAME VARCHAR(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL,
IS_DURABLE VARCHAR(1) NOT NULL,
IS_NONCONCURRENT VARCHAR(1) NOT NULL,
IS_UPDATE_DATA VARCHAR(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR(1) NOT NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;
-- 存储已配置的Trigger的信息
CREATE TABLE QRTZ_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT(13) NULL,
PREV_FIRE_TIME BIGINT(13) NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT(13) NOT NULL,
END_TIME BIGINT(13) NULL,
CALENDAR_NAME VARCHAR(200) NULL,
MISFIRE_INSTR SMALLINT(2) NULL,
JOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP))
ENGINE=InnoDB;
-- 存储已配置的Simple Trigger的信息
CREATE TABLE QRTZ_SIMPLE_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
REPEAT_COUNT BIGINT(7) NOT NULL,
REPEAT_INTERVAL BIGINT(12) NOT NULL,
TIMES_TRIGGERED BIGINT(10) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
-- 存储Cron Trigger,包括Cron表达式和时区信息
CREATE TABLE QRTZ_CRON_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13,4) NULL,
DEC_PROP_2 NUMERIC(13,4) NULL,
BOOL_PROP_1 VARCHAR(1) NULL,
BOOL_PROP_2 VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
-- Trigger作为Blob类型存储(用于Quartz用户用JDBC创建他们自己定制的Trigger类型,JobStore并不知道如何存储实例的时候)
CREATE TABLE QRTZ_BLOB_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
BLOB_DATA BLOB NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
-- 以Blob类型存储Quartz的Calendar日历信息,quartz可配置一个日历来指定一个时间范围
CREATE TABLE QRTZ_CALENDARS (
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BLOB NOT NULL,
PRIMARY KEY (SCHED_NAME,CALENDAR_NAME))
ENGINE=InnoDB;
-- 存储已暂停的Trigger组的信息
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS (
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP))
ENGINE=InnoDB;
-- 存储与已触发的Trigger相关的状态信息,以及相联Job的执行信息
CREATE TABLE QRTZ_FIRED_TRIGGERS (
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT(13) NOT NULL,
SCHED_TIME BIGINT(13) NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_NONCONCURRENT VARCHAR(1) NULL,
REQUESTS_RECOVERY VARCHAR(1) NULL,
PRIMARY KEY (SCHED_NAME,ENTRY_ID))
ENGINE=InnoDB;
-- 存储少量的有关 Scheduler的状态信息,和别的 Scheduler 实例(假如是用于一个集群中)
CREATE TABLE QRTZ_SCHEDULER_STATE (
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
LAST_CHECKIN_TIME BIGINT(13) NOT NULL,
CHECKIN_INTERVAL BIGINT(13) NOT NULL,
PRIMARY KEY (SCHED_NAME,INSTANCE_NAME))
ENGINE=InnoDB;
-- 存储程序的非观锁的信息(假如使用了悲观锁)
CREATE TABLE QRTZ_LOCKS (
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME))
ENGINE=InnoDB;
CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY ON QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_J_GRP ON QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_J ON QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_JG ON QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_T_C ON QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME);
CREATE INDEX IDX_QRTZ_T_G ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_T_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME ON QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST ON QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP ON QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME);
CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
CREATE INDEX IDX_QRTZ_FT_J_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_JG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP);
CREATE INDEX IDX_QRTZ_FT_T_G ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
CREATE INDEX IDX_QRTZ_FT_TG ON QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP);
commit;
快速构建一个SpringBoot项目,添加依赖:
>
>org.springframework.boot >
>spring-boot-starter-data-jpa >
>
>
>org.springframework.boot >
>spring-boot-starter-web >
>
>
>mysql >
>mysql-connector-java >
>
>
>org.springframework.boot >
>spring-boot-starter-quartz >
>
>
>org.projectlombok >
>lombok >
>true >
>
创建项目的包结构:
在application.yml中添加如下配置文件:
server:
port: 8080
spring:
application:
name: boot-quartz-demo
datasource: #数据库配置
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/quartz?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: 123456
jpa: #JPA配置
database: mysql
show-sql: true
quartz:
#quartz相关属性配置
properties:
org:
quartz:
scheduler:
instanceName: clusteredScheduler #调度器的实例名
instanceId: AUTO #调度器编号自动生成
jobStore:
class: org.quartz.impl.jdbcjobstore.JobStoreTX
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
tablePrefix: qrtz_ #数据库表名前缀
isClustered: true #开启分布式部署
clusterCheckinInterval: 10000 #分布式节点有效性检查时间间隔,单位:秒
useProperties: false
threadPool:
class: org.quartz.simpl.SimpleThreadPool #自带的线程池实现类
threadCount: 10 #开启10个线程
threadPriority: 5 #工作者线程的优先级
threadsInheritContextClassLoaderOfInitializingThread: true
#数据库方式
job-store-type: jdbc
构建好如上项目结构后,我们开始编写实现代码,首先我们需要封装一个任务执行的类存放与model包下:
/**
* @ClassName Task
* @Description 定时任务基本属性
*/
@Data
@Builder
public class Task {
/**
* 任务名称
*/
@NotEmpty(message = "任务名称不能为空")
private String name;
/**
* 任务分组
*/
@NotEmpty(message = "任务分组不能为空")
private String goup;
/**
* corn表达式
*/
@NotEmpty(message = "定时任务的表达式不能为空")
private String cron;
/**
* 任务描述
*/
private String desc;
/**
* 执行任务的逻辑类
*/
@NotNull(message = "执行任务的逻辑类名不能为空")
private String jobClass;
/**
* 元数据
*/
private Map<?,?> jobDataMap;
public JobKey getJobKey(){
return JobKey.jobKey(this.name,this.goup);
}
}
编写业务类QuartzJobService存放与service包下:
@Slf4j
@Service
public class QuartzJobService {
//Quartz定时任务核心的功能实现类
private Scheduler scheduler;
public QuartzJobService(@Autowired SchedulerFactoryBean schedulerFactoryBean) {
scheduler = schedulerFactoryBean.getScheduler();
}
/**
* 创建和启动定时任务
* {@link org.quartz.Scheduler#scheduleJob(JobDetail, Trigger)}
*
* @param task 定时任务
*/
public void scheduleJob(Task task) throws SchedulerException, ClassNotFoundException {
//定时任务的名字和组名
JobKey jobKey = task.getJobKey();
//定时任务的元数据
JobDataMap jobDataMap = getJobDataMap(task.getJobDataMap());
//定时任务的描述
String desc = task.getDesc();
//定时任务的逻辑实现类
Class<? extends Job> jobClass = (Class<? extends Job>) Class.forName(task.getJobClass());
//定时任务的cron表达式
String cron = task.getCron();
JobDetail jobDetail = getJobDetail(jobKey, desc, jobDataMap, jobClass);
Trigger trigger = getTrigger(jobKey, desc, jobDataMap, cron);
scheduler.scheduleJob(jobDetail, trigger);
}
/**
* 暂停Job
* {@link org.quartz.Scheduler#pauseJob(JobKey)}
*/
public void pauseJob(JobKey jobKey) throws SchedulerException {
scheduler.pauseJob(jobKey);
}
/**
* 恢复Job
* {@link org.quartz.Scheduler#resumeJob(JobKey)}
*/
public void resumeJob(JobKey jobKey) throws SchedulerException {
scheduler.resumeJob(jobKey);
}
/**
* 删除Job
* {@link org.quartz.Scheduler#deleteJob(JobKey)}
*/
public void deleteJob(JobKey jobKey) throws SchedulerException {
scheduler.deleteJob(jobKey);
}
/**
* 修改Job的cron表达式
*/
public boolean modifyJobCron(Task task) {
String cron = task.getCron();
//如果cron表达式的格式不正确,则返回修改失败
if (!CronExpression.isValidExpression(cron)) return false;
JobKey jobKey = task.getJobKey();
TriggerKey triggerKey = new TriggerKey(jobKey.getName(), jobKey.getGroup());
try {
CronTrigger cronTrigger = (CronTrigger) scheduler.getTrigger(triggerKey);
JobDataMap jobDataMap = getJobDataMap(task.getJobDataMap());
//如果cron发生变化了,则按新cron触发 进行重新启动定时任务
if (!cronTrigger.getCronExpression().equalsIgnoreCase(cron)) {
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity(triggerKey)
.withSchedule(CronScheduleBuilder.cronSchedule(cron))
.usingJobData(jobDataMap)
.build();
scheduler.rescheduleJob(triggerKey, trigger);
}
} catch (SchedulerException e) {
log.error("printStackTrace", e);
return false;
}
return true;
}
/**
* 获取定时任务的定义
* JobDetail是任务的定义,Job是任务的执行逻辑
*
* @param jobKey 任务的名称和组名
* @param desc 任务的描述
* @param jobDataMap 任务的元数据
* @param jobClass {@link org.quartz.Job} 定时任务的 真正执行逻辑定义类
*/
public JobDetail getJobDetail(JobKey jobKey, String desc, JobDataMap jobDataMap, Class<? extends Job> jobClass) {
return JobBuilder.newJob(jobClass)
.withIdentity(jobKey)
.withDescription(desc)
.setJobData(jobDataMap)
.usingJobData(jobDataMap)
.requestRecovery()
.storeDurably()
.build();
}
/**
* 获取Trigger (Job的触发器,执行规则)
*
* @param jobKey 任务的名称和组名
* @param description 任务的描述
* @param jobDataMap 任务的元数据
* @param cronExpression 任务的执行cron表达式
*/
public Trigger getTrigger(JobKey jobKey, String description, JobDataMap jobDataMap, String cronExpression) {
return TriggerBuilder.newTrigger()
.withIdentity(jobKey.getName(), jobKey.getGroup())
.withDescription(description)
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
.usingJobData(jobDataMap)
.build();
}
public JobDataMap getJobDataMap(Map<?, ?> map) {
return map == null ? new JobDataMap() : new JobDataMap(map);
}
}
编写执行任务的逻辑类JobOne存放与jobs包下:
/**
* @ClassName JobOne
* @Description 定时任务的具体执行逻辑类
*/
@Slf4j
@DisallowConcurrentExecution
public class JobOne implements Job {
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
String time = LocalDateTime.ofInstant(new Date().toInstant(),
ZoneId.systemDefault()).
format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
log.info(time.concat("JobOne.execute"));
//获取JobDataMap
JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();
log.info(jobDataMap.getString("userName"));
log.info(jobDataMap.getString("passWord"));
}
}
编写JobController存放与controller包下:
/**
* @ClassName JobController
* @Description JobController
*/
@RestController
public class JobController {
@Autowired
QuartzJobService quartzJobService;
//创建&启动
@PostMapping("startJob")
public String startJob(@RequestBody Task task) throws SchedulerException, ClassNotFoundException {
quartzJobService.scheduleJob(task);
return "startJob Success!";
}
//暂停
@PostMapping("pauseJob")
public String pauseJob(@RequestBody Task task)throws SchedulerException{
quartzJobService.pauseJob(task.getJobKey());
return "pauseJob Success!";
}
//恢复
@PostMapping("resumeJob")
public String resumeJob(@RequestBody Task task)throws SchedulerException{
quartzJobService.resumeJob(task.getJobKey());
return "resumeJob Success!";
}
//删除
@PostMapping("delJob")
public String delJob(@RequestBody Task task)throws SchedulerException{
quartzJobService.deleteJob(task.getJobKey());
return "delJob Success!";
}
//修改
@PostMapping("modifyJob")
public String modifyJob(@RequestBody Task task){
if(quartzJobService.modifyJobCron(task)){
return "modifyJob Success!";
}else{
return "modifyJob Fail!";
}
}
}
启动项目,使用postman工具请求接口,创建一个任务,观察控制台:
请求接口:http://localhost:8080/startJob
上面的任务每5秒执行一次,控制台打印如下:
点击这里获取示例源码