Springboot2.x整合Quartz,动态配置定时任务实现

项目中遇到了关于定时任务的需求,在要求实现定时任务的前提下,同时要求对任务进行动态配置,简言之就是可以随时对定时任务进行增删改查,并且能够实时生效。

在之前的多个项目中也做过类似的功能,因为比较简单,通常都是从网上拿过来直接用,一般网上写的都是通过properties配置文件和XML文件进行实现,但是用此种方法无法实现动态配置的需求。在网上找了一些关于动态配置定时任务的文章,结合实际情况,最终实现了该需求。在此记录下来,便于日后再次遇到同样需求,同时也进行下分享,希望能帮助更多的人。

具体的细节此处暂时不说,因为目前也没有时间做特别深入的了解,后面如果有时间会把自己的理解随时更新上来。

废话不多说,具体实现步骤如下:

1、Quartz依赖引入。由于Springboot2.0加入了针对Quartz的自动化配置,省去了很多繁琐的步骤,所以我们将之前准备采用的quartz相关依赖删除,替换为spring-boot-starter-quartz,如下所示:

<!--quartz依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2、初始化Quartz使用的数据库表。每个quartz的版本引入的数据库表数量应该是略有差异的。本文只以自己项目中引入的为准。建库脚本可以在http://www.quartz-scheduler.org/downloads/  下载对应版本的tar.gz,对应脚本文件在\src\org\quartz\impl\jdbcjobstore目录下可以找到,选择自己数据库的脚本,本项目用到的是oracle数据库,如下所示:

--
-- A hint submitted by a user: Oracle DB MUST be created as "shared" and the 
-- job_queue_processes parameter  must be greater than 2
-- However, these settings are pretty much standard after any
-- Oracle install, so most users need not worry about this.
--
-- Many other users (including the primary author of Quartz) have had success
-- runing in dedicated mode, so only consider the above as a hint ;-)
--

delete from qrtz_fired_triggers;
delete from qrtz_simple_triggers;
delete from qrtz_simprop_triggers;
delete from qrtz_cron_triggers;
delete from qrtz_blob_triggers;
delete from qrtz_triggers;
delete from qrtz_job_details;
delete from qrtz_calendars;
delete from qrtz_paused_trigger_grps;
delete from qrtz_locks;
delete from qrtz_scheduler_state;

drop table qrtz_calendars;
drop table qrtz_fired_triggers;
drop table qrtz_blob_triggers;
drop table qrtz_cron_triggers;
drop table qrtz_simple_triggers;
drop table qrtz_simprop_triggers;
drop table qrtz_triggers;
drop table qrtz_job_details;
drop table qrtz_paused_trigger_grps;
drop table qrtz_locks;
drop table qrtz_scheduler_state;


CREATE TABLE qrtz_job_details
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    JOB_NAME  VARCHAR2(200) NOT NULL,
    JOB_GROUP VARCHAR2(200) NOT NULL,
    DESCRIPTION VARCHAR2(250) NULL,
    JOB_CLASS_NAME   VARCHAR2(250) NOT NULL, 
    IS_DURABLE VARCHAR2(1) NOT NULL,
    IS_NONCONCURRENT VARCHAR2(1) NOT NULL,
    IS_UPDATE_DATA VARCHAR2(1) NOT NULL,
    REQUESTS_RECOVERY VARCHAR2(1) NOT NULL,
    JOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_JOB_DETAILS_PK PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
CREATE TABLE qrtz_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    JOB_NAME  VARCHAR2(200) NOT NULL, 
    JOB_GROUP VARCHAR2(200) NOT NULL,
    DESCRIPTION VARCHAR2(250) NULL,
    NEXT_FIRE_TIME NUMBER(13) NULL,
    PREV_FIRE_TIME NUMBER(13) NULL,
    PRIORITY NUMBER(13) NULL,
    TRIGGER_STATE VARCHAR2(16) NOT NULL,
    TRIGGER_TYPE VARCHAR2(8) NOT NULL,
    START_TIME NUMBER(13) NOT NULL,
    END_TIME NUMBER(13) NULL,
    CALENDAR_NAME VARCHAR2(200) NULL,
    MISFIRE_INSTR NUMBER(2) NULL,
    JOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_TRIGGERS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_TRIGGER_TO_JOBS_FK 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 VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    REPEAT_COUNT NUMBER(7) NOT NULL,
    REPEAT_INTERVAL NUMBER(12) NOT NULL,
    TIMES_TRIGGERED NUMBER(10) NOT NULL,
    CONSTRAINT QRTZ_SIMPLE_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_SIMPLE_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
	REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_cron_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    CRON_EXPRESSION VARCHAR2(120) NOT NULL,
    TIME_ZONE_ID VARCHAR2(80),
    CONSTRAINT QRTZ_CRON_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_CRON_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
      REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_simprop_triggers
  (          
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    STR_PROP_1 VARCHAR2(512) NULL,
    STR_PROP_2 VARCHAR2(512) NULL,
    STR_PROP_3 VARCHAR2(512) NULL,
    INT_PROP_1 NUMBER(10) NULL,
    INT_PROP_2 NUMBER(10) NULL,
    LONG_PROP_1 NUMBER(13) NULL,
    LONG_PROP_2 NUMBER(13) NULL,
    DEC_PROP_1 NUMERIC(13,4) NULL,
    DEC_PROP_2 NUMERIC(13,4) NULL,
    BOOL_PROP_1 VARCHAR2(1) NULL,
    BOOL_PROP_2 VARCHAR2(1) NULL,
    CONSTRAINT QRTZ_SIMPROP_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_SIMPROP_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
      REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_blob_triggers
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    BLOB_DATA BLOB NULL,
    CONSTRAINT QRTZ_BLOB_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
    CONSTRAINT QRTZ_BLOB_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) 
        REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_calendars
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    CALENDAR_NAME  VARCHAR2(200) NOT NULL, 
    CALENDAR BLOB NOT NULL,
    CONSTRAINT QRTZ_CALENDARS_PK PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);
CREATE TABLE qrtz_paused_trigger_grps
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    TRIGGER_GROUP  VARCHAR2(200) NOT NULL, 
    CONSTRAINT QRTZ_PAUSED_TRIG_GRPS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_fired_triggers 
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    ENTRY_ID VARCHAR2(95) NOT NULL,
    TRIGGER_NAME VARCHAR2(200) NOT NULL,
    TRIGGER_GROUP VARCHAR2(200) NOT NULL,
    INSTANCE_NAME VARCHAR2(200) NOT NULL,
    FIRED_TIME NUMBER(13) NOT NULL,
    SCHED_TIME NUMBER(13) NOT NULL,
    PRIORITY NUMBER(13) NOT NULL,
    STATE VARCHAR2(16) NOT NULL,
    JOB_NAME VARCHAR2(200) NULL,
    JOB_GROUP VARCHAR2(200) NULL,
    IS_NONCONCURRENT VARCHAR2(1) NULL,
    REQUESTS_RECOVERY VARCHAR2(1) NULL,
    CONSTRAINT QRTZ_FIRED_TRIGGER_PK PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);
CREATE TABLE qrtz_scheduler_state 
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    INSTANCE_NAME VARCHAR2(200) NOT NULL,
    LAST_CHECKIN_TIME NUMBER(13) NOT NULL,
    CHECKIN_INTERVAL NUMBER(13) NOT NULL,
    CONSTRAINT QRTZ_SCHEDULER_STATE_PK PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);
CREATE TABLE qrtz_locks
  (
    SCHED_NAME VARCHAR2(120) NOT NULL,
    LOCK_NAME  VARCHAR2(40) NOT NULL, 
    CONSTRAINT QRTZ_LOCKS_PK PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);

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);


3、配置文件编写。可以作为单独配置文件引入,也可直接配置到application.properties中(当然你也可以用yml格式)。本项目直接放到application.properties当中。如下所示:

#######################################quartz设置开始######################################
spring.quartz.job-store-type=jdbc
#使用自己的配置文件
spring.quartz.properties.org.quartz.jobStore.useProperties=false
#默认或是自己改名字都行
spring.quartz.properties.org.quartz.scheduler.instanceName=DefaultQuartzScheduler
#如果使用集群,instanceId必须唯一,设置成AUTO
spring.quartz.properties.org.quartz.scheduler.instanceId=AUTO

spring.quartz.properties.org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
spring.quartz.properties.org.quartz.threadPool.threadCount=10
spring.quartz.properties.org.quartz.threadPool.threadPriority=5
spring.quartz.properties.org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread=true

#存储方式使用JobStoreTX,也就是数据库
spring.quartz.properties.org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
spring.quartz.properties.org.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate

#是否使用集群(如果项目只部署到 一台服务器,就不用了)
spring.quartz.properties.org.quartz.jobStore.isClustered = false
spring.quartz.properties.org.quartz.jobStore.clusterCheckinInterval=20000

#数据库中quartz表的表名前缀
spring.quartz.properties.org.quartz.jobStore.tablePrefix = QRTZ_

#######################################quartz设置结束######################################

4、由于是动态配置定时任务,还需要自己建一张表,后面会说此表用途,如下所示:

create table T_SCHEDULEJOB
(
  JOBNAME         VARCHAR2(200) not null,
  JOBGROUP        VARCHAR2(200) not null,
  JOBSTATUS       CHAR(1),
  CRONEXPRESSION  VARCHAR2(100),
  REMARK          VARCHAR2(200),
  JOBTASKTYPE     VARCHAR2(2),
  APIURL          VARCHAR2(100),
  PARAMS          VARCHAR2(500),
  JOBTYPE         CHAR(1),
  TRIGGERNAME     VARCHAR2(200),
  TRIGGERGROUP    VARCHAR2(200),
  ISNOWRUN        CHAR(1),
  STARTDATE       DATE,
  ENDDATE         DATE,
  JOBCLASSNAME    VARCHAR2(200),
  CREATETIMESTAMP VARCHAR2(20),
  UPDATETIMESTAMP VARCHAR2(20),
  REMOVETAG       CHAR(1)
)
-- Add comments to the table 
comment on table T_SCHEDULEJOB
  is '定时任务调度';
-- Add comments to the columns 
comment on column T_SCHEDULEJOB.JOBNAME
  is '任务名称';
comment on column T_SCHEDULEJOB.JOBGROUP
  is '任务分组';
comment on column T_SCHEDULEJOB.JOBSTATUS
  is '任务状态, 0禁用 1启用';
comment on column T_SCHEDULEJOB.CRONEXPRESSION
  is '任务cron表达式';
comment on column T_SCHEDULEJOB.REMARK
  is '任务描述';
comment on column T_SCHEDULEJOB.JOBTASKTYPE
  is '任务调度类型,0:接口,1:存储过程';
comment on column T_SCHEDULEJOB.APIURL
  is '接口地址';
comment on column T_SCHEDULEJOB.PARAMS
  is '参数';
comment on column T_SCHEDULEJOB.JOBTYPE
  is '任务类型,0:周期性,1:一次性';
comment on column T_SCHEDULEJOB.TRIGGERNAME
  is '触发器名字';
comment on column T_SCHEDULEJOB.TRIGGERGROUP
  is '触发器分组';
comment on column T_SCHEDULEJOB.ISNOWRUN
  is '是否立即运行,0:否,1:是';
comment on column T_SCHEDULEJOB.STARTDATE
  is '生效日期';
comment on column T_SCHEDULEJOB.ENDDATE
  is '失效日期';
comment on column T_SCHEDULEJOB.JOBCLASSNAME
  is '执行类名';
comment on column T_SCHEDULEJOB.CREATETIMESTAMP
  is '任务创建时间';
comment on column T_SCHEDULEJOB.UPDATETIMESTAMP
  is '任务更新时间';
comment on column T_SCHEDULEJOB.REMOVETAG
  is '删除标记';

5、代码层面。

定时任务实体类,如下所示(省略get/set方法):

public class Job extends JobKey implements Serializable {
	@ApiModelProperty(value="任务状态, 0禁用 1启用 2删除",name="jobstatus")
    private String jobstatus;

	@ApiModelProperty(value="任务cron表达式",name="cronexpression")
    private String cronexpression;

	@ApiModelProperty(value="任务描述",name="remark")
    private String remark;

	@ApiModelProperty(value="任务调度类型,0:接口,1:存储过程",name="jobtasktype")
    private String jobtasktype;

	@ApiModelProperty(value="接口地址",name="apiurl")
    private String apiurl;

	@ApiModelProperty(value="参数",name="params")
    private String params;

	@ApiModelProperty(value="任务类型,0:周期性,1:一次性",name="jobtype")
    private String jobtype;

	@ApiModelProperty(value="触发器名字",name="triggername")
    private String triggername;

	@ApiModelProperty(value="触发器分组",name="triggergroup")
    private String triggergroup;

	@ApiModelProperty(value="执行类名",name="jobclassname")
    private String jobclassname;

	@ApiModelProperty(value="是否立即运行,0:否,1:是",name="isnowrun")
    private String isnowrun;

	@ApiModelProperty(value="任务生效日期",name="startdate")
	@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date startdate;

	@ApiModelProperty(value="任务失效日期",name="enddate")
	@JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
    private Date enddate;

    private String createtimestamp;

    private String updatetimestamp;

    private String removetag;

    private static final long serialVersionUID = 1L;
}
public class JobKey extends BaseModel implements Serializable {
	@ApiModelProperty(value="任务名称",name="jobname")
    private String jobname;

	@ApiModelProperty(value="任务分组",name="jobgroup")
    private String jobgroup;

    private static final long serialVersionUID = 1L;
}

实现层,如下所示:

	@Autowired
	private Scheduler scheduler;

	@Override
	@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, timeout = 36000, rollbackFor = BaseException.class)
	public void saveTask(Job record) throws BaseException {
		/*
		 * 1.将任务记录插入T_SCHEDULEJOB 2.将任务交由Scheduler安排触发 3.如果是立即执行,则首先触发一次任务
		 */
		try {
			record.setTriggername("trigger" + record.getJobname());
			record.setTriggername(record.getJobgroup());
			record.setJobstatus("1");
			this.save(record);
		} catch (BaseException e) {
			throw new BaseException("定时任务插入数据库失败");
		}
		try {
			Class cls = Class.forName(record.getJobclassname());
			cls.newInstance();
			// 构建job信息
			JobDetail job = JobBuilder.newJob(cls).withIdentity(record.getJobname(), record.getJobgroup())
					.withDescription(record.getRemark()).build();
			// 触发时间点
			CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(record.getCronexpression());
			Trigger trigger = TriggerBuilder.newTrigger()
					.withIdentity("trigger" + record.getJobname(), record.getJobgroup()).startNow()
					.withSchedule(cronScheduleBuilder).build();
			// 传递的参数
			job.getJobDataMap().put("jobtasktype", record.getJobtasktype());
			job.getJobDataMap().put("jobtype", record.getJobtype());
			job.getJobDataMap().put("apiurl", record.getApiurl());
			job.getJobDataMap().put("params", record.getParams());
			// 交由Scheduler安排触发
			scheduler.scheduleJob(job, trigger);
			if ("1".equals(record.getIsnowrun())) { // 如果是立即运行则首先触发一次任务
				JobKey key = new JobKey(record.getJobname(), record.getJobgroup());
				scheduler.triggerJob(key, job.getJobDataMap());
			}
		} catch (Exception e) {
			throw new BaseException("新增任务失败");
		}
	}

	@Override
	public void trigger(Job record) throws BaseException {
		long temp = System.currentTimeMillis();
		try {
			Class cls = Class.forName(record.getJobclassname());
			cls.newInstance();
			// 构建job信息
			JobDetail job = JobBuilder.newJob(cls).withIdentity(record.getJobname()+temp, record.getJobgroup())
					.withDescription(record.getRemark()).build();
			// 触发时间点
			CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule("0 0 2 * * ? *");
			Trigger trigger = TriggerBuilder.newTrigger()
					.withIdentity("trigger" + record.getJobname()+temp, record.getJobgroup()).startNow()
					.withSchedule(cronScheduleBuilder).build();
			// 传递的参数
			job.getJobDataMap().put("jobtasktype", record.getJobtasktype());
			job.getJobDataMap().put("jobtype", record.getJobtype());
			job.getJobDataMap().put("apiurl", record.getApiurl());
			job.getJobDataMap().put("params", record.getParams());
			scheduler.scheduleJob(job, trigger);
			JobKey key = new JobKey(record.getJobname()+temp, record.getJobgroup());
			scheduler.triggerJob(key, job.getJobDataMap());
		} catch (Exception e) {
			throw new BaseException("触发任务失败");
		}
		//此处做延时处理,否则移除任务时偶尔会nullexception
		try {
			Thread.sleep(500);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		try {
			TriggerKey triggerKey = TriggerKey.triggerKey(record.getJobname()+temp, record.getJobgroup());
			// 停止触发器
			scheduler.pauseTrigger(triggerKey);
			// 移除触发器
			scheduler.unscheduleJob(triggerKey);
			// 删除任务
			scheduler.deleteJob(JobKey.jobKey(record.getJobname()+temp, record.getJobgroup()));
			logger.info("移除临时触发:" + JobKey.jobKey(record.getJobname()+temp));
		} catch (Exception e) {
			throw new BaseException("移除临时触发任务失败");
		}
	}

	@Override
	public void pause(Job record) throws BaseException {
		try {
			record.setJobstatus("0");
			this.update(record);
//			JobKey key = new JobKey(record.getJobname(), record.getJobgroup());
//			scheduler.pauseJob(key);
			//如果暂停后恢复,会立刻执行一次,所以暂停时先删掉定时任务,恢复是新增
			TriggerKey triggerKey = TriggerKey.triggerKey(record.getJobname(), record.getJobgroup());
			// 停止触发器
			scheduler.pauseTrigger(triggerKey);
			// 移除触发器
			scheduler.unscheduleJob(triggerKey);
			// 删除任务
			scheduler.deleteJob(JobKey.jobKey(record.getJobname(), record.getJobgroup()));
			logger.info("暂停任务:" + JobKey.jobKey(record.getJobname()));
		} catch (Exception e) {
			throw new BaseException("暂停任务失败");
		}
	}

	@Override
	public void resume(Job record) throws BaseException {
		try {
			record.setJobstatus("1");
			this.update(record);
//			JobKey key = new JobKey(record.getJobname(), record.getJobgroup());
//			scheduler.resumeJob(key);
			//新增任务
			Class cls = Class.forName(record.getJobclassname());
			cls.newInstance();
			// 构建job信息
			JobDetail job = JobBuilder.newJob(cls).withIdentity(record.getJobname(), record.getJobgroup())
					.withDescription(record.getRemark()).build();
			// 触发时间点
			CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(record.getCronexpression());
			Trigger trigger = TriggerBuilder.newTrigger()
					.withIdentity("trigger" + record.getJobname(), record.getJobgroup()).startNow()
					.withSchedule(cronScheduleBuilder).build();
			// 传递的参数
			job.getJobDataMap().put("jobtasktype", record.getJobtasktype());
			job.getJobDataMap().put("jobtype", record.getJobtype());
			job.getJobDataMap().put("apiurl", record.getApiurl());
			job.getJobDataMap().put("params", record.getParams());
			// 交由Scheduler安排触发
			scheduler.scheduleJob(job, trigger);
		} catch (Exception e) {
			throw new BaseException("恢复任务失败");
		}
	}

	@Override
	@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, timeout = 36000, rollbackFor = BaseException.class)
	public void remove(String jobname, String jobgroup) throws BaseException {
		try {
			com.chinastock.quartz.model.JobKey jobKey = new com.chinastock.quartz.model.JobKey();
			jobKey.setJobname(jobname);
			jobKey.setJobgroup(jobgroup);
			this.deleteByPrimaryKey(jobKey);
			TriggerKey triggerKey = TriggerKey.triggerKey(jobname, jobgroup);
			// 停止触发器
			scheduler.pauseTrigger(triggerKey);
			// 移除触发器
			scheduler.unscheduleJob(triggerKey);
			// 删除任务
			scheduler.deleteJob(JobKey.jobKey(jobname, jobgroup));
			logger.info("removeJob:" + JobKey.jobKey(jobname));
		} catch (Exception e) {
			throw new BaseException("移除任务失败");
		}
	}

	@Override
	@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.DEFAULT, timeout = 36000, rollbackFor = BaseException.class)
	public void updateTask(Job record) throws BaseException {
		/*
		 * 1.更新 2.删除任务 新建任务
		 */
		//更新
		try {
			this.update(record);
			//移除任务
			TriggerKey triggerKey = TriggerKey.triggerKey(record.getJobname(), record.getJobgroup());
			// 停止触发器
			scheduler.pauseTrigger(triggerKey);
			// 移除触发器
			scheduler.unscheduleJob(triggerKey);
			// 删除任务
			scheduler.deleteJob(JobKey.jobKey(record.getJobname(), record.getJobgroup()));
			logger.info("removeJob:" + JobKey.jobKey(record.getJobname()));
			//新增任务
			Class cls = Class.forName(record.getJobclassname());
			cls.newInstance();
			// 构建job信息
			JobDetail job = JobBuilder.newJob(cls).withIdentity(record.getJobname(), record.getJobgroup())
					.withDescription(record.getRemark()).build();
			// 触发时间点
			CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(record.getCronexpression());
			Trigger trigger = TriggerBuilder.newTrigger()
					.withIdentity("trigger" + record.getJobname(), record.getJobgroup()).startNow()
					.withSchedule(cronScheduleBuilder).build();
			// 传递的参数
			job.getJobDataMap().put("jobtasktype", record.getJobtasktype());
			job.getJobDataMap().put("jobtype", record.getJobtype());
			job.getJobDataMap().put("apiurl", record.getApiurl());
			job.getJobDataMap().put("params", record.getParams());
			// 交由Scheduler安排触发
			scheduler.scheduleJob(job, trigger);
			if ("1".equals(record.getIsnowrun())) { // 如果是立即运行则首先触发一次任务
				JobKey key = new JobKey(record.getJobname(), record.getJobgroup());
				scheduler.triggerJob(key, job.getJobDataMap());
			}
		} catch (Exception e) {
			throw new BaseException("更新任务失败");
		}
	}

任务执行类。如下所示:

@Service
public class ApiJob implements Job, Serializable {
	public static Logger logger = LoggerFactory.getLogger(ApiJob.class);
		
	@Autowired
	private Scheduler scheduler;
	
	@Autowired
    private JobMapper mapper;
	
	@Override
	@Transactional(propagation = Propagation.REQUIRED,isolation = Isolation.DEFAULT,timeout=36000,rollbackFor=BaseException.class)
	public void execute(JobExecutionContext context) throws JobExecutionException {
		// 取得job详情
		JobDetail jobDetail = context.getJobDetail();
		// 取得job名称
		String jobName = jobDetail.getKey().getName();
		String jobgroup = jobDetail.getKey().getGroup();
		logger.info("=========定时任务 |  任务分组:" + jobgroup + ",任务名称:" + jobName + ",调用接口服务执行开始=========");
		logger.info("定时任务名称: " + jobName);
		// 取得job的类
		logger.info("定时任务执行类名: " + jobDetail.getJobClass());
		// 取得jobgroup名称
		logger.info("任务组名: " + jobgroup);
		// 取得job开始时间
		logger.info(jobName + " 开始时间: " + DateUtils.format(context.getFireTime(), "yyyy-MM-dd HH:mm:ss"));
		// 取得job下次触发时间
		logger.info("下次执行时间: " + DateUtils.format(context.getNextFireTime(), "yyyy-MM-dd HH:mm:ss"));
		JobDataMap dataMap = jobDetail.getJobDataMap();
		String jobtasktype = dataMap.getString("jobtasktype");
		String jobtype = dataMap.getString("jobtype");
		String apiurl = dataMap.getString("apiurl");
		String params = dataMap.getString("params");
		logger.info("jobtasktype: " + jobtasktype);
		logger.info("jobtype: " + jobtype);
		logger.info("apiurl: " + apiurl);
		logger.info("params: " + params);
		String result = HttpUtil.doHttpPost(Constants.SUM_URL+apiurl, params);
		logger.info("定时任务接口调用返回结果:" + result);
		/*
		 * 1.判断如果为一次性执行,则执行后,将job状态置为禁用,并移除任务
		 */
		if("1".equals(jobtype) && null != context.getNextFireTime()) {
			com.chinastock.quartz.model.Job job = new com.chinastock.quartz.model.Job();
			job.setJobname(jobName);
			job.setJobgroup(jobgroup);
			job.setJobstatus("0");
			int success = mapper.updateByPrimaryKeySelective(job);
	        if (success <= 0) {
	            logger.error("任务组:" + jobgroup + ",任务名称:" + jobName + ",移除任务错误{}",  job.toString());
	            throw new JobExecutionException("移除任务失败");
	        }
			try {
				TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobgroup);
				// 停止触发器
				scheduler.pauseTrigger(triggerKey);
				// 移除触发器
				scheduler.unscheduleJob(triggerKey);
				// 删除任务
				scheduler.deleteJob(JobKey.jobKey(jobName, jobgroup));
				logger.info("移除任务:" + JobKey.jobKey(jobName));
			} catch (Exception e) {
				logger.info("任务组:" + jobgroup + ",任务名称:" + jobName + ",执行移除任务失败");
				throw new JobExecutionException("移除任务失败");
			}
		}
		logger.info("=========定时任务 |  任务分组:" + jobgroup + ",任务名称:" + jobName + ",调用接口服务执行结束=========");
	}

大体上的代码是如此。就不一一贴出来了。

中间遇到几个问题,也说一下。

1、一开始做这个需求的时候,网上找了下,有些说的不是很清楚,加上自己理解也不到位,所以一直认为动态配置是直接修改quartz原有的10几张表。后来突然想明白了,建了一个中间表T_SCHEDULEJOB,目的是通过中间表存储的相关字段为定时任务做维护。看过代码应该可以知道,对定时任务的动态配置,关键是在于jobname、jobgroup、cronexpression(cron表达式)这几个字段,而T_SCHEDULEJOB表的其他字段则是根据需求来定;

2、cron表达式,是直接通过前段的组件直接转换而来,此处就不多说了;

3、关于暂停任务和恢复任务,做到这里的时候,出现个问题,就是如果使用quartz自己的pasueJob和resumeJob,在resumeJob的时候,触发器会立即执行一次任务。这样显然不是我们所愿意看到的,网上找了下资料,说的是在配置触发时间点的时候加上

.withMisfireHandlingInstructionDoNothing

——不触发立即执行
——等待下次Cron触发频率到达时刻开始按照Cron频率依次执行

使用此方法后,立即执行的问题确实解决了,可是在本地调试的时候,在重启应用后,却发现,会把停止时间内没有运行的任务补上,比如设置为间隔10秒跑一次,应用重启时间如果是1分钟,那么重启后,系统会把这1分钟里没跑的6次都跑一遍。所以当时一不做二不休,索性在暂停任务时候直接将任务移除,恢复任务时候再重新新增,算是绕了个弯路解决了这个问题;

4、关于立即触发任务,就是新增任务,直接在jobname后加入了时间戳来区分每个新增的任务。新增执行后,做了一个延时处理,避免会出现nullException的问题。


以上就是大致的实现过程及解决方案,欢迎大家讨论指正。

  • 4
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
1. 引入依赖 在 `pom.xml` 文件中引入以下依赖: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency> ``` 2. 配置数据源 在 `application.yml` 或 `application.properties` 文件中配置数据源: ``` spring.datasource.url=jdbc:mysql://localhost:3306/quartz?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8&useSSL=false spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ``` 3. 配置Quartz 在 `application.yml` 或 `application.properties` 文件中配置Quartz: ``` # 配置Quartz spring.quartz.job-store-type=jdbc spring.quartz.jdbc.initialize-schema=always spring.quartz.properties.org.quartz.threadPool.threadCount=5 ``` `spring.quartz.job-store-type` 指定Quartz使用的存储类型,这里配置为 `jdbc` 表示使用数据库存储。 `spring.quartz.jdbc.initialize-schema` 指定Quartz是否需要初始化数据库表,这里配置为 `always` 表示每次启动应用程序都会初始化数据库表。 `spring.quartz.properties` 是Quartz的属性配置,这里配置了线程池的线程数为5。 4. 编写任务类 编写Quartz任务类,实现 `org.quartz.Job` 接口即可。 ``` @Component public class MyJob implements Job { @Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { System.out.println("Hello, Quartz!"); } } ``` 5. 配置任务调度器 编写任务调度器类,实现 `org.springframework.scheduling.quartz.SchedulerFactoryBean` 接口即可。 ``` @Configuration public class QuartzConfig { @Autowired private DataSource dataSource; @Autowired private MyJob myJob; @Bean public SchedulerFactoryBean schedulerFactoryBean() { SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean(); schedulerFactoryBean.setDataSource(dataSource); // 自定义JobFactory,用于支持Spring的自动注入 AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory(); jobFactory.setApplicationContext(applicationContext); schedulerFactoryBean.setJobFactory(jobFactory); // 配置JobDetail JobDetail jobDetail = JobBuilder.newJob(myJob.getClass()).withIdentity("myJob").build(); // 配置Trigger SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); trigger.setJobDetail(jobDetail); trigger.setRepeatInterval(5000); trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); // 注册Trigger schedulerFactoryBean.setTriggers(trigger.getObject()); return schedulerFactoryBean; } } ``` 6. 自动注入Spring容器 为了支持Spring的自动注入,需要编写一个自定义的 JobFactory。 ``` public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware { private transient AutowireCapableBeanFactory beanFactory; @Override public void setApplicationContext(final ApplicationContext context) { beanFactory = context.getAutowireCapableBeanFactory(); } @Override protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception { final Object job = super.createJobInstance(bundle); beanFactory.autowireBean(job); return job; } } ``` 7. 测试 在需要执行定时任务的方法上添加 `@Scheduled` 注解即可。 ``` @Component public class TestJob { @Scheduled(cron = "0/5 * * * * ?") public void test() { System.out.println("test"); } } ``` 以上是springboot整合quartz的常见配置,可以根据具体需求进行调整。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值