前言:
要将quartz的任务持久化,能动态添加任务、修改任务、暂停任务、动态修改触发时间.
调度器:Scheduler;工作:JobDetail ;触发器:Trigger
一、在pom文件中引入jar包
这里只定义quartz需要的jar包,其它和spring相关的省略
org.quartz-scheduler
quartz
2.2.1
org.quartz-scheduler
quartz-jobs
2.2.1
二、配置相应的xml文件
fwoneScheduler
AUTO
org.quartz.simpl.SimpleThreadPool
20
5
org.quartz.impl.jdbcjobstore.JobStoreTX
true
150000
1
12000
QRTZ_
三、建立相关的数据表
因为要持久化到数据库中,所以要建立相关的数据表
1.1.qrtz_blob_triggers : 以Blob 类型存储的触发器。
1.2.qrtz_calendars:存放日历信息, quartz可配置一个日历来指定一个时间范围。
1.3.qrtz_cron_triggers:存放cron类型的触发器。
1.4.qrtz_fired_triggers:存放已触发的触发器。
1.5.qrtz_job_details:存放一个jobDetail信息。
1.6.qrtz_job_listeners:job**监听器**。
1.7.qrtz_locks: 存储程序的悲观锁的信息(假如使用了悲观锁)。
1.8.qrtz_paused_trigger_graps:存放暂停掉的触发器。
1.9.qrtz_scheduler_state:调度器状态。
1.10.qrtz_simple_triggers:简单触发器的信息。
1.11.qrtz_trigger_listeners:触发器监听器。 1.12.qrtz_triggers:触发器的基本信息。
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;
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)
);
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)
);
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)
);
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(200) 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)
);
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)
);
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),
FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
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)
);
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
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)
);
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)
);
CREATE TABLE QRTZ_LOCKS
(
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);
四、具体实现
1、我本地自己建立的一个保存任务的数据表
DROP TABLE IF EXISTS `schedule_job`;
CREATE TABLE `schedule_job` (
`job_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务id',
`bean_name` varchar(200) DEFAULT NULL COMMENT 'spring bean名称',
`method_name` varchar(100) DEFAULT NULL COMMENT '方法名',
`params` varchar(2000) DEFAULT NULL COMMENT '参数',
`cron_expression` varchar(100) DEFAULT NULL COMMENT 'cron表达式',
`status` tinyint(4) DEFAULT NULL COMMENT '任务状态',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`job_id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='定时任务';
2、项目启动时先初始化
说明:我的项目集成了mybatis,像数据库调用的那些就不说了。
ScheduleJobEntity这个是对应表`schedule_job`建立的实体类,schedulerJobDao是数据调用接口。
/**
* 项目启动时,初始化定时器
*/
@PostConstruct
public void myInit(){
//先查出数据库中有多少个任务,将其初始化
List list=schedulerJobDao.queryList(null);
if(list!=null)
{
list.forEach
(
scheduleJob->
{
//获取各个任务对应的触发器,如果不为空,则更新;为空则创建任务;
Trigger trigger= MyScheduleUtils.getTrigger(scheduler,scheduleJob.getJobId());
if(trigger==null)
MyScheduleUtils.createScheduleJob(scheduler,scheduleJob);
else
MyScheduleUtils.updateScheduleJob(scheduler,scheduleJob);
}
);
}
}
3、建立工具类MyScheduleUtils
package com.fwone.utils;
import com.fwone.entity.ScheduleJobEntity;
import org.quartz.*;
public class MyScheduleUtils {
private static final String JOB_NAME="TASK_";
/**
* 获取触发器Key
* @param jobId
* @return
*/
public static TriggerKey getTriggerKey(Long jobId){
return TriggerKey.triggerKey(JOB_NAME+jobId);
}
public static JobKey getJobKey(Long jobId){
return JobKey.jobKey(JOB_NAME+jobId);
}
/**
* 获取触发器
* @param scheduler
* @param jobId
* @return
*/
public static CronTrigger getTrigger(Scheduler scheduler, Long jobId){
try {
JobDetail jobDetail=scheduler.getJobDetail(getJobKey(jobId));
Class c=jobDetail.getJobClass();
System.out.println(c);
return (CronTrigger)scheduler.getTrigger(getTriggerKey(jobId));
} catch (SchedulerException e) {
throw new RRException("获取定时任务CronTrigger出现异常",e);
}
}
/**
* 创建任务
* @param scheduler
* @param scheduleJob
*/
public static void createScheduleJob(Scheduler scheduler, ScheduleJobEntity scheduleJob){
try {
/** 重要:
* 定义一个job,并绑定到我们自定义的MyScheduleJob的class对象
* 这里并不会马上创建一个MyScheduleJob实例,实例创建是在scheduler安排任务触发执行时创建的
* 这种机制也为后面使用Spring集成提供了便利
*/
JobDetail jobDetail = JobBuilder.newJob(MyScheduleJob.class)
.withIdentity(getJobKey(scheduleJob.getJobId()))
.build();
/** 重要:
* 定义一个表达式调度构建器:不触发立即执行,等待下次Cron触发频率到达时刻开始按照Cron频率依次执行
* 如果任务失败,策略为:所有的misfire不管,执行下一个周期的任务
*/
CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder
.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();
/**重要:
* 按新的cronExpression表达式构建一个新的trigger
*/
Trigger trigger = TriggerBuilder
.newTrigger()
.withIdentity(getTriggerKey(scheduleJob.getJobId()))
.withSchedule(cronScheduleBuilder)
.build();
//放入参数,运行时的方法可以获取
jobDetail.getJobDataMap().put(ScheduleJobEntity.JOB_PARAM_KEY, scheduleJob);
/**重要:
* 把定义好的job放入调度器scheduler
*/
scheduler.scheduleJob(jobDetail, trigger);
//判断是否暂停任务
if(scheduleJob.getStatus()==null||
scheduleJob.getStatus()== Constant.ScheduleStatus.PAUSE.getValue())
pauseJob(scheduler,scheduleJob.getJobId());
}catch (SchedulerException e){
throw new RRException("创建定时任务失败", e);
}
}
/**
* 更新任务
* @param scheduler
* @param scheduleJob
*/
public static void updateScheduleJob(Scheduler scheduler, ScheduleJobEntity scheduleJob){
try {
CronScheduleBuilder scheduleBuilder=CronScheduleBuilder
.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();
CronTrigger trigger=getTrigger(scheduler,scheduleJob.getJobId());
//按新的cronExpression表达式重新构建trigger
trigger=trigger.getTriggerBuilder()
.withIdentity(getTriggerKey(scheduleJob.getJobId()))
.withSchedule(scheduleBuilder)
.build();
trigger.getJobDataMap().put(ScheduleJobEntity.JOB_PARAM_KEY,scheduleJob);
scheduler.rescheduleJob(getTriggerKey(scheduleJob.getJobId()),trigger);
//判断是否暂停任务
if(scheduleJob.getStatus()==null||
scheduleJob.getStatus()== Constant.ScheduleStatus.PAUSE.getValue())
pauseJob(scheduler,scheduleJob.getJobId());
} catch (SchedulerException e) {
throw new RRException("更新定时任务失败",e);
}
}
/**
* 暂停任务
* @param scheduler
* @param jobId
*/
public static void pauseJob(Scheduler scheduler,Long jobId){
try {
scheduler.pauseJob(getJobKey(jobId));
} catch (SchedulerException e) {
throw new RRException("暂停定时任务失败", e);
}
}
/**
* 立即运行一次任务
* @param scheduler
* @param scheduleJob
*/
public static void run(Scheduler scheduler,ScheduleJobEntity scheduleJob){
try {
JobDataMap jobDataMap=new JobDataMap();
jobDataMap.put(ScheduleJobEntity.JOB_PARAM_KEY,scheduleJob);
scheduler.triggerJob(getJobKey(scheduleJob.getJobId()),jobDataMap);
} catch (SchedulerException e) {
throw new RRException("立即执行任务失败",e);
}
}
/**
*恢复任务
* @param scheduler
* @param jobId
*/
public static void resumeJob(Scheduler scheduler,Long jobId){
try {
scheduler.resumeJob(getJobKey(jobId));
} catch (SchedulerException e) {
throw new RRException("恢复任务失败",e);
}
}
}
自定义工作类,用于 执行任务
package com.fwone.utils;
import com.fwone.entity.ScheduleJobEntity;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.quartz.QuartzJobBean;
import java.util.concurrent.Future;
/**
**另外Trigger中也可以设置JobDataMap属性,这是为了在多个Trigger中使用相同的Job。
**JobExecutionContext 将会合并JobDetail与Trigger的JobDataMap,如果其中属性名相同,后者将覆盖前者。
**可以使用JobExecutionContext.getMergedJobDataMap()方法来获取合并后的JobDataMap。
*/
public class MyScheduleJob extends QuartzJobBean {
private Logger logger= LoggerFactory.getLogger(MyScheduleJob.class);
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
ScheduleJobEntity scheduleJob=(ScheduleJobEntity)context
.getMergedJobDataMap().get(ScheduleJobEntity.JOB_PARAM_KEY);
ThreadPoolTaskExecutor taskExecutor=SpringContextUtil.getBean("taskExecutor");
//任务开始时间
long startTime = System.currentTimeMillis();
logger.info("我的任务准备执行,任务ID:" + scheduleJob.getJobId());
try {
/**
* ScheduleRunnable是一个任务类,通过反射执行真正的任务
*/
ScheduleRunnable task=new ScheduleRunnable(scheduleJob.getBeanName(),
scheduleJob.getMethodName(),scheduleJob.getParams());
Future> future=taskExecutor.submit(task);
future.get();//线程结束之前下面语句不会执行
long times=System.currentTimeMillis()-startTime;
logger.info("我的任务执行完毕,任务ID:" + scheduleJob.getJobId() + " 总共耗时:" + times + "毫秒");
} catch (Exception e) {
logger.error("我的任务执行失败,任务ID:" + scheduleJob.getJobId(), e);
}
}
}
定义任务类ScheduleRunnable,通过反射执行真正的任务
package com.fwone.utils;
import org.apache.commons.lang.StringUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
/**
* 执行定时任务
*
* @author chenshun
* @email [email protected]
* @date 2016年11月30日 下午12:49:33
*/
public class ScheduleRunnable implements Runnable {
private Object target;//bean名称
private Method method;//bean的方法名称
private String params;//bean的参数
/**
* 初始化
* @param beanName
* @param methodName
* @param params
* @throws NoSuchMethodException
* @throws SecurityException
*/
public ScheduleRunnable(String beanName, String methodName, String params) throws NoSuchMethodException, SecurityException {
this.target = SpringContextUtil.getBean(beanName);
this.params = params;
if(StringUtils.isNotBlank(params)){
this.method = target.getClass().getDeclaredMethod(methodName, String.class);
}else{
this.method = target.getClass().getDeclaredMethod(methodName);
}
}
@Override
public void run() {
try {
//让方法跳过安全检查
ReflectionUtils.makeAccessible(method);
if(StringUtils.isNotBlank(params)){
method.invoke(target, params);
}else{
method.invoke(target);
}
}catch (Exception e) {
throw new RRException("执行定时任务失败", e);
}
}
}
最后,为了安全,还要在spring容器关闭时把调度器、线程池关闭。我试过如果不手动关闭,会发出警告说内存有可能溢出。
定义监听器,这样在容器关闭时能做一些处理工作.
package com.fwone.listener;
import com.fwone.utils.RRException;
import com.fwone.utils.SpringContextUtil;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.servlet.annotation.WebListener;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
@WebListener
public class MyContextLoaderListener implements ServletContextListener{
@Override
public void contextInitialized(ServletContextEvent sce) {
}
/**
* 容器关闭时触发
* @param sce
*/
@Override
public void contextDestroyed(ServletContextEvent sce) {
/**
* SpringContextUtil是我实现的一个获取bean的工具类,网上有很多例子,就不写出来了
* 这里主要是把调度器Scheduler和线程池ExecutorService关闭
*/
ExecutorService taskExecutor=SpringContextUtil.getBean("taskExecutor");
Scheduler scheduler=(Scheduler)SpringContextUtil.getBean("scheduler");
if(scheduler!=null){
try {
if(scheduler.isStarted()){
scheduler.shutdown();
}
} catch (SchedulerException e) {
throw new RRException("定时容器关闭失败");
}
}
try {
taskExecutor.shutdown();
if(taskExecutor.awaitTermination(2000l, TimeUnit.MILLISECONDS))
taskExecutor.shutdownNow();
}catch (InterruptedException e){
e.printStackTrace();
taskExecutor.shutdown();
}
}
}
至此大功告成,以后想添加定时任务,只需把写好的bean名称传进去就可以了。