虽然Spring Boot融合了基于注解的定时任务,但是任务的定时器需在代码中配置,代码入侵较强,例如:若需改动某一定时任务的执行周期,则需改动代码再发版,且无法实现具体任务的暂停与启动。
Quartz可实现定时任务配置的持久化,将相关定时任务信息(对应任务类名、任务当前状态、cron表达式)持久化到数据库,从而在页面实现对具体任务的暂停、启动、执行周期的修改等操作而不需要改动代码。
Java 8、Spring Boot 1.5.13、MyBatis、Quartz 2.3.0
1.Quartz框架必须表
-- in your Quartz properties file, you'll need to set org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
-- 你需要在你的quartz.properties文件中设置org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
-- StdJDBCDelegate说明支持集群,所有的任务信息都会保存到数据库中,可以控制事物,还有就是如果应用服务器关闭或者重启,任务信息都不会丢失,并且可以恢复因服务器关闭或者重启而导致执行失败的任务
-- This is the script from Quartz to create the tables in a MySQL database, modified to use INNODB instead of MYISAM
-- 这是来自quartz的脚本,在MySQL数据库中创建以下的表,修改为使用INNODB而不是MYISAM
-- 你需要在数据库中执行以下的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;
2.新建任务实体表,通过该表控制具体的任务执行
CREATE TABLE `qrtz_job_entity` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增id',
`job_class_name` varchar(100) NOT NULL COMMENT '任务对应类名',
`job_group` varchar(20) NOT NULL COMMENT '任务组名',
`cron` varchar(50) NOT NULL COMMENT 'cron表达式',
`description` varchar(50) NOT NULL COMMENT '任务描述',
`vm_param` varchar(100) DEFAULT NULL COMMENT '参数(暂时不用)',
`status` varchar(10) NOT NULL COMMENT '任务状态:OPEN/CLOSE',
`delete_flag` int(1) NOT NULL DEFAULT '0' COMMENT '删除标记',
`create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1.加入Quartz maven依赖
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.0</version>
</dependency>
2.配置文件quartz.properties
# 固定前缀org.quartz
# 主要分为scheduler、threadPool、jobStore、plugin等部分
org.quartz.scheduler.instanceName=DefaultQuartzScheduler
org.quartz.scheduler.rmi.export=false
org.quartz.scheduler.rmi.proxy=false
org.quartz.scheduler.wrapJobExecutionInUserTransaction=false
# 实例化ThreadPool时,使用的线程类为SimpleThreadPool
org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
# threadCount和threadPriority将以setter的形式注入ThreadPool实例
# 并发个数
org.quartz.threadPool.threadCount=5
org.quartz.spi.JobFactory
# 优先级
org.quartz.threadPool.threadPriority=5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true
org.quartz.jobStore.misfireThreshold=5000
# 默认存储在内存中
#org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
# 持久化
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix=qrtz_
org.quartz.jobStore.dataSource=qzDS
org.quartz.dataSource.qzDS.connectionProvider.class=com.zhl.common.conf.DruidConnectionProvider
org.quartz.dataSource.qzDS.driver=com.mysql.jdbc.Driver
org.quartz.dataSource.qzDS.URL=jdbc:mysql://localhost:3306/schedule?autoReconnect=true&useUnicode=true&characterEncoding=utf8&allowMultiQueries=true
org.quartz.dataSource.qzDS.user=root
org.quartz.dataSource.qzDS.password=123456
org.quartz.dataSource.qzDS.maxConnection=10
3.Job对象的实例化过程是在Quartz中进行,所以是无法在Job Bean中注入由Spring管理的Bean的,在这里我们通过以下两个类实现将Job Bean自动装配到Spring
package com.zhl.common.conf;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
/**
* @author hailunZhao
* @date 2020/7/16 15:50
*/
@Component("myAdaptableJobFactory")
public class MyAdaptableJobFactory extends AdaptableJobFactory {
/**
* AutowireCapableBeanFactory 可以将一个对象添加到SpringIOC容器中,并且完成该对象注入
*/
@Resource
private AutowireCapableBeanFactory autowireCapableBeanFactory;
/**
* 该方法需要将实例化的任务对象手动的添加到SpringIOC容器中并且完成对象的注入
*/
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object object = super.createJobInstance(bundle);
//将object对象添加到SpringIOC容器中,并完成注入
this.autowireCapableBeanFactory.autowireBean(object);
return object;
}
}
package com.zhl.common.conf;
import com.zhl.common.constant.Base;
import com.zhl.common.utils.PropertyUtil;
import org.quartz.Scheduler;
import org.quartz.ee.servlet.QuartzInitializerListener;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.Properties;
/**
* @author hailunZhao
* @date 2020/6/12 9:43
*/
@Component
public class QuartzFactoryBean {
@Resource
private MyAdaptableJobFactory myAdaptableJobFactory;
@Bean(name = "schedulerFactoryBean")
public SchedulerFactoryBean schedulerFactoryBean(@Qualifier("quartzProperties") Properties properties) {
SchedulerFactoryBean factoryBean = new SchedulerFactoryBean();
factoryBean.setQuartzProperties(properties);
factoryBean.setJobFactory(myAdaptableJobFactory);
return factoryBean;
}
@Bean(name = "quartzProperties")
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource(PropertyUtil.getEnvironment() + Base.Properties.QUARTZ));
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
@Bean
public QuartzInitializerListener quartzInitializerListener() {
return new QuartzInitializerListener();
}
@Bean
public Scheduler scheduler(SchedulerFactoryBean schedulerFactoryBean) {
return schedulerFactoryBean.getScheduler();
}
}
4.job_entity对应实体
package com.zhl.model.entry;
import lombok.Data;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;
/**
* @author hailunZhao
* @date 2020/7/7 19:21
*/
@Table(name = "qrtz_job_entity")
@Data
public class JobEntity implements Serializable {
@Id
private Integer id;
/**
* Des:执行job的类名
*/
private String jobClassName;
/**
* Des:job组名
*/
private String jobGroup;
/**
* Des:执行的cron
*/
private String cron;
/**
* Des:job描述信息
*/
private String description;
/**
* Des:vm参数
*/
private String vmParam;
/**
* Des:job的执行状态,这里我设置为OPEN/CLOSE且只有该值为OPEN才会执行该Job
*/
private String status;
private int deleteFlag;
private String createTime;
private String updateTime;
}
5.Job工具类
package com.zhl.common.utils;
import com.zhl.model.entry.JobEntity;
import org.quartz.*;
/**
* Job工具类
*
* @author hailunZhao
* @date 2020/6/12 13:56
*/
public class JobUtil {
/**
* 获取指定字符串的class
* 因为定时任务中要求的是一个泛型是job类型的class,只有job类型的对象才会有这个class,所以先实例化对象,强转成job,然后再重新获取class
*
* @param jobClassName jobClassName
* @return
*/
public static Class<? extends Job> getClass(String jobClassName) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
Class<?> aClass = Class.forName(jobClassName);
Class<? extends Job> aClass1 = ((Job) aClass.newInstance()).getClass();
return aClass1;
}
/**
* Des:获取JobDataMap.(Job参数对象)
*
* @param job job
* @return org.quartz.JobDataMap
* @author hailunZhao
* @date 2020/7/10 16:17
*/
public static JobDataMap getJobDataMap(JobEntity job) {
JobDataMap map = new JobDataMap();
map.put("id", job.getId());
map.put("jobClassName", job.getJobClassName());
map.put("jobGroup", job.getJobGroup());
map.put("cronExpression", job.getCron());
map.put("jobDescription", job.getDescription());
map.put("vmParam", job.getVmParam());
map.put("status", job.getStatus());
return map;
}
/**
* Des:获取JobDetail,JobDetail是任务的定义,而Job是任务的执行逻辑,JobDetail里会引用一个Job Class来定义
*
* @param job job
* @return org.quartz.JobDetail
* @author hailunZhao
* @date 2020/7/10 16:17
*/
public static JobDetail getJobDetail(JobEntity job) throws Exception {
return JobBuilder.newJob(JobUtil.getClass(job.getJobClassName()))
.withIdentity(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()))
.withDescription(job.getDescription())
.setJobData(getJobDataMap(job))
.storeDurably()
.build();
}
/**
* Des:获取Trigger (Job的触发器,执行规则)
*
* @param job job
* @return org.quartz.Trigger
* @author hailunZhao
* @date 2020/7/10 16:17
*/
public static Trigger getTrigger(JobEntity job) {
return TriggerBuilder.newTrigger()
.withIdentity(job.getJobClassName(), job.getJobGroup())
.withSchedule(CronScheduleBuilder.cronSchedule(job.getCron()))
.build();
}
/**
* Des:获取JobKey,包含Name和Group
*
* @param jobClassName jobClassName
* @param jobGroup jobGroup
* @return org.quartz.JobKey
* @author hailunZhao
* @date 2020/7/10 16:17
*/
public static JobKey getJobKey(String jobClassName, String jobGroup) {
return JobKey.jobKey(jobClassName, jobGroup);
}
/**
* Des:TriggerKey,包含Name和Group
*
* @param jobClassName jobClassName
* @param jobGroup jobGroup
* @return org.quartz.TriggerKey
* @author hailunZhao
* @date 2020/7/10 16:17
*/
public static TriggerKey getTriggerKey(String jobClassName, String jobGroup) {
return TriggerKey.triggerKey(jobClassName, jobGroup);
}
}
6.service接口与实现类
package com.zhl.service;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.PageInfo;
import com.zhl.common.result.Result;
import com.zhl.model.entry.JobEntity;
import java.util.List;
/**
* @author hailunZhao
* @date 2020/7/7 17:39
*/
public interface IDynamicJobService {
/**
* Des:任务列表(分页)
*
* @param page 页码
* @param pageSize 条数/页
* @param className 任务类名
* @return com.github.pagehelper.PageInfo
* @author hailunZhao
* @date 2020/7/10 15:17
*/
PageInfo<JSONObject> queryJob(Integer page, Integer pageSize, String className) throws Exception;
/**
* Des:通过Id获取Job
*
* @param id 任务id
* @return com.zhl.model.entry.JobEntity
* @author hailunZhao
* @date 2020/6/12 11:31
*/
JobEntity getJobEntityById(Integer id);
/**
* Des:从数据库中加载获取到所有Job
*
* @param
* @return java.util.List
* @author hailunZhao
* @date 2020/6/12 11:31
*/
List<JobEntity> loadJobs();
/**
* Des:添加任务
*
* @param jsonObject 任务
* @return void
* @author hailunZhao
* @date 2020/6/12 13:41
*/
Result addJob(JSONObject jsonObject);
/**
* Des:暂停任务
*
* @param jobId 任务id
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/6/12 16:20
*/
Result pauseJob(Integer jobId);
/**
* Des:恢复任务
*
* @param jobId 任务id
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/6/12 16:26
*/
Result resumeJob(Integer jobId);
/**
* Des:恢复任务
*
* @param jobId 任务id
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/6/12 16:26
*/
Result deleteJob(Integer jobId);
/**
* Des:更新触发器时间
*
* @param requestJson 任务id cron表达式
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/6/12 16:29
*/
Result rescheduleJob(JSONObject requestJson);
}
package com.zhl.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.zhl.common.constant.Base;
import com.zhl.common.constant.ResultCode;
import com.zhl.common.exception.BizException;
import com.zhl.common.result.Result;
import com.zhl.common.utils.Assert;
import com.zhl.common.utils.DateUtil;
import com.zhl.common.utils.JobUtil;
import com.zhl.dao.JobEntryMapper;
import com.zhl.model.entry.JobEntity;
import com.zhl.service.IDynamicJobService;
import org.apache.commons.lang3.StringUtils;
import org.quartz.*;
import org.springframework.stereotype.Service;
import tk.mybatis.mapper.entity.Condition;
import tk.mybatis.mapper.entity.Example;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* 动态定时任务
*
* @author hailunZhao
* @date 2020/6/12 11:16
*/
@Service
public class DynamicJobServiceImpl implements IDynamicJobService {
@Resource
private JobEntryMapper jobEntryMapper;
@Resource
private Scheduler scheduler;
@Override
public PageInfo<JSONObject> queryJob(Integer page, Integer pageSize, String className) throws Exception {
PageHelper.startPage(page, pageSize, true);
Condition condition = new Condition(JobEntity.class);
Example.Criteria criteria = condition.createCriteria();
criteria.andEqualTo("deleteFlag", 0);
if (StringUtils.isNotEmpty(className)) {
criteria.andLike("jobClassName", "%" + className + "%");
}
condition.orderBy("id").desc();
List<JobEntity> list = jobEntryMapper.selectByCondition(condition);
List<JSONObject> dataList = new ArrayList<>(list.size());
for (JobEntity job : list) {
job.setUpdateTime(DateUtil.dateToStr(DateUtil.strToDate(job.getUpdateTime(), DateUtil.FORMAT_2), DateUtil.FORMAT_4));
JSONObject json = JSONObject.parseObject(JSON.toJSONString(job));
TriggerKey triggerKey = JobUtil.getTriggerKey(job.getJobClassName(), job.getJobGroup());
Trigger.TriggerState state = scheduler.getTriggerState(triggerKey);
json.put("state", state);
dataList.add(json);
}
PageInfo<JSONObject> pageInfo = new PageInfo<JSONObject>(dataList);
return pageInfo;
}
@Override
public JobEntity getJobEntityById(Integer id) {
return jobEntryMapper.selectByPrimaryKey(id);
}
@Override
public List<JobEntity> loadJobs() {
Condition condition = new Condition(JobEntity.class);
condition.createCriteria().andEqualTo("deleteFlag", 0);
return jobEntryMapper.selectByCondition(condition);
}
@Override
public Result addJob(JSONObject jsonObject) {
Assert.hasAllRequired(jsonObject, new String[]{"jobClassName", "jobGroup", "cron", "description", "status"});
JobEntity job = JSON.toJavaObject(jsonObject, JobEntity.class);
// cron表达式校验
if (!CronExpression.isValidExpression(job.getCron())) {
throw new BizException(ResultCode.PARAM_IS_INVALID);
}
try {
// class是否存在校验
JobUtil.getClass(job.getJobClassName());
} catch (Exception e) {
e.printStackTrace();
return Result.error(ResultCode.PARAM_IS_INVALID);
}
// job是否已存在
Condition condition = new Condition(JobEntity.class);
condition.createCriteria().andEqualTo("deleteFlag", 0)
.andEqualTo("jobClassName", job.getJobClassName());
int existCount = jobEntryMapper.selectCountByCondition(condition);
if (existCount>0) {
throw new BizException(ResultCode.DATA_ALREADY_EXISTED);
}
job.setId(null);
job.setCreateTime(DateUtil.dateToStr(new Date(), DateUtil.FORMAT_2));
job.setUpdateTime(DateUtil.dateToStr(new Date(), DateUtil.FORMAT_2));
jobEntryMapper.insert(job);
// 开启定时器
if (Base.Job.OPEN.equals(job.getStatus())) {
try {
JobDetail jobDetail = JobUtil.getJobDetail(job);
Trigger trigger = JobUtil.getTrigger(job);
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
} catch (Exception e) {
e.printStackTrace();
return Result.error(ResultCode.ERROR);
}
}
return Result.success();
}
@Override
public Result pauseJob(Integer jobId) {
JobEntity job = jobEntryMapper.selectByPrimaryKey(jobId);
Assert.notNull(job, ResultCode.RESULT_DATA_NONE.message());
try {
scheduler.pauseJob(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()));
} catch (SchedulerException e) {
e.printStackTrace();
return Result.error(ResultCode.ERROR);
}
return Result.success();
}
@Override
public Result resumeJob(Integer jobId) {
JobEntity job = jobEntryMapper.selectByPrimaryKey(jobId);
Assert.notNull(job, ResultCode.RESULT_DATA_NONE.message());
try {
scheduler.resumeJob(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()));
} catch (SchedulerException e) {
e.printStackTrace();
return Result.error(ResultCode.ERROR);
}
return Result.success();
}
@Override
public Result deleteJob(Integer jobId) {
JobEntity job = jobEntryMapper.selectByPrimaryKey(jobId);
Assert.notNull(job, ResultCode.RESULT_DATA_NONE.message());
if (Base.Job.OPEN.equals(job.getStatus())) {
try {
//先暂停任务
scheduler.pauseTrigger(TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup()));
//停止任务
scheduler.unscheduleJob(TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup()));
//删除任务
scheduler.deleteJob(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()));
} catch (SchedulerException e) {
e.printStackTrace();
return Result.error(ResultCode.ERROR);
}
}
job.setStatus(Base.Job.CLOSE);
job.setDeleteFlag(1);
jobEntryMapper.updateByPrimaryKey(job);
return Result.success();
}
@Override
public Result rescheduleJob(JSONObject requestJson) {
Assert.hasAllRequired(requestJson, new String[]{"id", "cron", "status"});
Integer jobId = requestJson.getInteger("id");
String cronExpression = requestJson.getString("cron");
String status = requestJson.getString("status");
JobEntity job = jobEntryMapper.selectByPrimaryKey(jobId);
Assert.notNull(job, ResultCode.RESULT_DATA_NONE.message());
// cron表达式校验
if (!CronExpression.isValidExpression(cronExpression)) {
throw new BizException(ResultCode.PARAM_IS_INVALID);
}
try {
// OPEN->OPEN
if (Base.Job.OPEN.equals(job.getStatus()) && Base.Job.OPEN.equals(status)) {
//先找到之前的触发器
TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup());
//根据表达式获取新的时间规则
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
//获取原始的触发器
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
//新的触发器
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
scheduler.rescheduleJob(triggerKey, trigger);
}
// OPEN->CLOSE
if (Base.Job.OPEN.equals(job.getStatus()) && Base.Job.CLOSE.equals(status)) {
//先暂停任务
scheduler.pauseTrigger(TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup()));
//停止任务
scheduler.unscheduleJob(TriggerKey.triggerKey(job.getJobClassName(), job.getJobGroup()));
//删除任务
scheduler.deleteJob(JobKey.jobKey(job.getJobClassName(), job.getJobGroup()));
}
// CLOSE->OPEN
if (Base.Job.CLOSE.equals(job.getStatus()) && Base.Job.OPEN.equals(status)) {
JobDetail jobDetail = JobUtil.getJobDetail(job);
Trigger trigger = JobUtil.getTrigger(job);
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
} catch (Exception e) {
e.printStackTrace();
return Result.error(ResultCode.ERROR);
}
job.setCron(cronExpression);
job.setStatus(status);
jobEntryMapper.updateByPrimaryKey(job);
return Result.success();
}
}
7.Rest接口
package com.zhl.controller.quartz;
import com.alibaba.fastjson.JSONObject;
import com.github.pagehelper.PageInfo;
import com.zhl.common.constant.ResultCode;
import com.zhl.common.result.Result;
import com.zhl.service.IDynamicJobService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* @author hailunZhao
* @date 2020/6/12 9:45
*/
@Slf4j
@RestController
@RequestMapping("/job")
public class QuartzController {
@Resource
private IDynamicJobService jobService;
/**
* Des:任务列表
*
* @param pageNum 页码
* @param pageRow 条数
* @param className 任务类名
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/7/9 11:19
*/
@GetMapping("/query_job")
public Result queryJob(@RequestParam(name = "pageNum", defaultValue = "1") int pageNum,
@RequestParam(name = "pageRow", defaultValue = "15") int pageRow,
@RequestParam(name = "className", defaultValue = "") String className) {
try {
PageInfo<JSONObject> pageInfo = jobService.queryJob(pageNum, pageRow, className);
return Result.success(pageInfo);
}catch (Exception e) {
log.error(e.getMessage());
return Result.error(ResultCode.ERROR);
}
}
/**
* Des:新加任务
*
* @param requestJson job对象
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/7/9 11:19
*/
@PostMapping("/add_job")
public Result addJob(@RequestBody JSONObject requestJson) {
return jobService.addJob(requestJson);
}
/**
* Des:暂停任务
*
* @param id jobId
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/7/9 11:20
*/
@GetMapping("/pause_job")
public Result pauseJob(Integer id) {
return jobService.pauseJob(id);
}
/**
* Des:恢复任务
*
* @param id jobId
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/7/9 11:20
*/
@GetMapping("/resume_job")
public Result resumeJob(Integer id) {
return jobService.resumeJob(id);
}
/**
* Des:删除任务
*
* @param id jobId
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/7/9 11:20
*/
@GetMapping("/delete_job")
public Result deleteJob(Integer id) {
return jobService.deleteJob(id);
}
/**
* Des:更新触发器的时间
*
* @param requestJson jobId cronExpression
* @return com.zhl.common.result.Result
* @author hailunZhao
* @date 2020/7/9 11:20
*/
@PostMapping("/reschedule_job")
public Result rescheduleJob(@RequestBody JSONObject requestJson) {
return jobService.rescheduleJob(requestJson);
}
}
8.新建两个定时任务并加入注解@DisallowConcurrentExecution
,该注解的作用是禁止同一任务并发执行,不同任务之间的并发执行是不影响的
package com.zhl.task;
import com.zhl.common.utils.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Date;
/**
* @author hailunZhao
* @date 2020/6/12 9:51
*/
@Slf4j
@DisallowConcurrentExecution
public class HelloJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("hello job执行了, 时间是: {}, 线程是: {}", DateUtil.dateToStr(new Date(), DateUtil.FORMAT_2), Thread.currentThread().getName());
}
}
package com.zhl.task;
import com.zhl.common.utils.DateUtil;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.util.Date;
/**
* @author hailunZhao
* @date 2020/6/12 9:51
*/
@Slf4j
@DisallowConcurrentExecution
public class TaskJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
log.info("task job执行了, 时间是: {}, 线程是: {}", DateUtil.dateToStr(new Date(), DateUtil.FORMAT_2), Thread.currentThread().getName());
}
}
9.将以上两个任务的信息加入表qrtz_job_entity
INSERT INTO `qrtz_job_entity` (`job_class_name`, `job_group`, `cron`, `description`, `vm_param`, `status`, `delete_flag`, `create_time`, `update_time`) VALUES ('com.zhl.task.HelloJob', 'helloworld', '0/2 * * * * ? ', '第一个', '', 'CLOSE', '0', '2020-07-09 17:10:56', '2020-07-10 17:23:42');
INSERT INTO `qrtz_job_entity` (`job_class_name`, `job_group`, `cron`, `description`, `vm_param`, `status`, `delete_flag`, `create_time`, `update_time`) VALUES ('com.zhl.task.TaskJob', 'helloworld', '0/5 * * * * ? ', 'test', NULL, 'CLOSE', '0', '2020-07-09 17:10:56', '2020-07-09 17:11:09');
10.项目启动初始化已有任务
package com.zhl.common.conf;
import com.zhl.common.utils.JobUtil;
import com.zhl.model.entry.JobEntity;
import com.zhl.service.IDynamicJobService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Set;
/**
* Des:项目启动执行
*
* @author hailunZhao
* @date 2020/7/14 16:52
*/
@Slf4j
@Component
@Order(2)
public class StartRunner implements CommandLineRunner {
@Resource
private Scheduler scheduler;
@Resource
private IDynamicJobService jobService;
@Override
public void run(String... arg0) throws Exception {
log.info("程序启动...");
reStartAllJobs();
}
/**
* 重新启动所有的job
*/
private void reStartAllJobs() throws Exception {
// 只允许一个线程进入操作
synchronized (log) {
Set<JobKey> set = scheduler.getJobKeys(GroupMatcher.anyGroup());
// 暂停所有JOB
scheduler.pauseJobs(GroupMatcher.anyGroup());
// 删除从数据库中注册的所有JOB
for (JobKey jobKey : set) {
scheduler.unscheduleJob(TriggerKey.triggerKey(jobKey.getName(), jobKey.getGroup()));
scheduler.deleteJob(jobKey);
}
// 从数据库中注册的所有JOB
for (JobEntity job : jobService.loadJobs()) {
log.info("Job register name : {} , group : {} , cron : {}, status : {}",
job.getJobClassName(), job.getJobGroup(), job.getCron(), job.getStatus());
JobDetail jobDetail = JobUtil.getJobDetail(job);
if (job.getStatus().equals("OPEN")) {
scheduler.scheduleJob(jobDetail, JobUtil.getTrigger(job));
} else {
log.info("Job jump name : {} , Because {} status is {}", job.getJobClassName(), job.getJobClassName(), job.getStatus());
}
}
}
}
}
11.前端页面设计如下,按钮调用相应接口即可实现任务的修改、暂停、恢复
Quartz与Spring的结合很好的实现了对定时任务的动态灵活配置,与Spring Boot自带的定时任务相比更加灵活,可控性更强,但也有不足之处,因为Job信息是持久化到数据库,当任务数量众多时势必在性能上会稍逊半分。