SpringBoot集成Quartz实现持久化定时接口调用任务
作者:不瑶碧莲
一、基本概念
Quartz 是功能强大的开源作业调度库,几乎可以集成到任何 Java 应用程序中,从最小的独立应用程序到最大的电子商务系统。Quartz 可用于创建简单或复杂的计划,以执行数以万计的工作;可以执行您编写的所有内容。
Spring Boot 官方也对 Quartz 调度器进行了集成,Spring boot 官网文档:Quartz Scheduler,Java JDK 也带有 计时器 Timer 以及 定时执行服务 ScheduledExecutorService ,Spring 也提供了 @Scheduled 执行定时任务。
如果实际环境中定时任务过多,处理频繁,建议适应第三方封装的调度框架,因为定时器操作底层都是多线程的操作,任务的启动、暂停、恢复、删除、实质是线程的启动、暂停、中断、唤醒等操作。
二、Quartz-scheduler 的核心流程
Scheduler - 调度器
1、Scheduler 用来对 Trigger 和 Job 进行管理,Trigger 和 JobDetail 可以注册到 Scheduler 中,两者在 Scheduler 中都拥有自己的唯一的组(group)和名称(name)用来进行彼此的区分,Scheduler 可以通过任务组和名称来对 Trigger 和 JobDetail 进行管理。
2、每个 Scheduler 都有一个 SchedulerContext,用来保存 Scheduler 的上下文数据,Job 和 Trigger 都可以获取其中的信息。
3、Scheduler 是由 SchedulerFactory 创建,它有两个实现:DirectSchedulerFactory 、StdSchdulerFactory ,前者可以用来在代码里定制 Schduler 参数,后者直接读取 classpath 下的 quartz.properties(不存在就都使用默认值)配置来实例化 Scheduler。
Job - 任务
1、Job 是一个任务接口,开发者可以实现该接口定义自己的任务,JobExecutionContext 中提供了调度上下文的各种信息。
2、Job 中的任务有可能并发执行,例如任务的执行时间过长,而每次触发的时间间隔太短,则会导致任务会被并发执行。如果是并发执行,就需要一个数据库锁去避免一个数据被多次处理。可以在 execute()方法上添加 @DisallowConcurrentExecution 注解解决这个问题。
JobDetail - 任务详情
1、JobDetail 对象是在将 job 注册到 scheduler 时,由客户端程序创建的,它包含 job 的各种属性设置,以及用于存储 job 实例状态信息的 JobDataMap。
2、JobDetail 由 JobBuilder 创建/定义,Quartz 不存储 Job 的实际实例,但是允许通过使用 JobDetail 定义一个实例。
3、Job 有一个与其关联的名称和组,应该在单个 Scheduler 中唯一标识它们。
4、一个 Trigger(触发器) 只能对应一个 Job(任务),但是一个 Job 可以对应多个 Trigger。JobDetal 与 Trigger 一对多
Trigger - 触发器
1、Trigger 用于触发 Job 的执行。TriggerBuilder 用于定义/构建触发器实例。
2、Trigger也有一个相关联的 JobDataMap,用于给Job传递一些触发相关的参数。
3、Quartz自带了各种不同类型的 Trigger,最常用的主要是SimpleTrigger和CronTrigger。
JobDataMap
1、JobDataMap 实现了 JDK 的 Map 接口,可以以 Key-Value 的形式存储数据。
2、JobDetail、Trigger 实现类中都定义 JobDataMap 成员变量及其 getter、setter 方法,可以用来设置参数信息,Job 执行 execute() 方法的时候,JobExecutionContext 可以获取到 JobDataMap 中的信息。
三、实践
1、新建一个quartz-service服务
添加依赖:
<modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>quartz-servicer</artifactId> <version>1.0.0</version> <properties> <maven.compiler.source>11</maven.compiler.source> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.target>11</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.5</version> </dependency> <!-- 公共依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>2.0.19</version> </dependency> <!-- quartz依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> <version>2.5.4</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.31</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> <version>2.7.3</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.14</version> <exclusions> <exclusion> <artifactId>spring-boot-autoconfigure</artifactId> <groupId>org.springframework.boot</groupId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
2、配置数据源相关链接
quartz需要单据的数据库,所以需要单据创建一个库来给quartz使用,我新建了一个scheduler的库
#1 保存已经触发的触发器状态信息 DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; #2 存放暂停掉的触发器表表 DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; #3 调度器状态表 DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; #4 存储程序的悲观锁的信息(假如使用了悲观锁) DROP TABLE IF EXISTS QRTZ_LOCKS; #5 简单的触发器表 DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; #6 存储两种类型的触发器表 DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; #7 定时触发器表 DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; #8 以blob 类型存储的触发器 DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; #9 触发器表 DROP TABLE IF EXISTS QRTZ_TRIGGERS; #10 job 详细信息表 DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; #11 日历信息表 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) ); #触发器表 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) ); #定时触发器表,存储 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(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) ); #存储calendarintervaltrigger和dailytimeintervaltrigger两种类型的触发器 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) ); #以blob 类型存储的触发器 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) ); #日历信息表, 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) ); #存放暂停掉的触发器表表 CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( SCHED_NAME VARCHAR(120) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL, PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP) ); # 存储与已触发的 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) ); 3、 调度器状态表 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) ); 4、 存储程序的悲观锁的信息(假如使用了悲观锁) CREATE TABLE QRTZ_LOCKS ( SCHED_NAME VARCHAR(120) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL, PRIMARY KEY (SCHED_NAME,LOCK_NAME) );
3、创建配置文件
Quartz 使用一个名为 quartz.properties 的属性文件进行信息配置,必须位于 classpath 下,是 StdSchedulerFactory 用于创建 Scheduler 的默认属性文件。默认情况下 StdSchedulerFactory 从类路径下加载名为 “quartz.properties” 的属性文件,如果失败,则加载 org/quartz 包中的“quartz.properties”文件,因为我需要的是新建一个Scheduler服务,所以直接使用application.yml,配置如下:
datasource: url: jdbc:mysql://127.0.0.1:3306/scheduler username: *** password: **** driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #定时配置 quartz: #相关属性配置 properties: org: quartz: scheduler: instanceName: local-scheduler-svc instanceId: AUTO jobStore: #表示 quartz 中的所有数据,比如作业和触发器的信息都保存在内存中(而不是数据库中) class: org.springframework.scheduling.quartz.LocalDataSourceJobStore # 驱动配置 driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate # 表前缀 tablePrefix: QRTZ_ #是否为集群 isClustered: false clusterCheckinInterval: 10000 useProperties: false dataSource: quartzDs #线程池配置 threadPool: class: org.quartz.simpl.SimpleThreadPool #线程数 threadCount: 10 #优先级 threadPriority: 5 #线程继承上下文类加载器的初始化线程 threadsInheritContextClassLoaderOfInitializingThread: true #数据库方式 job-store-type: JDBC #初始化表结构 jdbc: initialize-schema: NEVER
4、新建一个任务实体类JobInfo,用户新建,传递任务信息
jobName:任务名称
jobGroup:任务组
jsonParams:任务执行信息(在用户定时远程调用接口的时候,我们可以接口信息封装到这个Map中)
cron:定时任务的cron表达式
timeZoneId:定制执行任务的时区
triggerTime:定时器时间(目前没用上)
@Data public class JobInfo { private String jobName; private String jobGroup; private Map<String, Object> jsonParams; private String cron; private String timeZoneId; private Date triggerTime; }
5、新建一个任务执行类HttpRemoteJob 实现 Job接口,重写execute()方法
execute()里面就是任务的逻辑:
① 使用HttpURLConnection
发送网络请求,利用BufferedReader接收请求返回的结果
② 在任务的Description中取出定时请求的接口信息,解析Description,获取请求url
③ 编辑请求信息,通过URL类编辑请求信息,使用HttpURLConnection发送请求,并接收请求返回的状态码,根据状态码,判断是否请求成功,请求成功便通过BufferedReader读取响应信息,返回请求结果
import com.alibaba.fastjson.JSONObject; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; import java.io.BufferedReader; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.util.Objects; @DisallowConcurrentExecution public class HttpRemoteJob implements Job { //日志 private static final Logger log = LoggerFactory.getLogger(HttpRemoteJob.class); @Override public void execute(JobExecutionContext context)throws JobExecutionException { //用于发送网络请求 HttpURLConnection connection = null; //用于接收请求返回的结果 BufferedReader bufferedReader = null; //获取任务Description述,之前我们把接口请求的信息放在Description里面了 String jsonParams = context.getJobDetail().getDescription(); if (StringUtils.isEmpty(jsonParams)){ return; } //解析Description,获取请求url JSONObject jsonObj= (JSONObject) JSONObject.parse(jsonParams); String callUrl = jsonObj.getString("callUrl"); if(StringUtils.isEmpty(callUrl)) { return; } try { //编辑请求信息 URL realUrl = new URL(callUrl); connection = (HttpURLConnection) realUrl.openConnection(); connection.setRequestMethod("GET"); connection.setDoOutput(true); connection.setDoInput(true); connection.setUseCaches(false); connection.setReadTimeout(5 * 1000); connection.setConnectTimeout(3 * 1000); connection.setRequestProperty("connection", "Keep-Alive"); connection.setRequestProperty("Content-Type", "application/json;charset=UTF-8"); connection.setRequestProperty("Accept-Charset", "application/json;charset=UTF-8"); //发送请求 connection.connect(); //获取请求返回的状态吗 int statusCode = connection.getResponseCode(); if (statusCode != 200){ //请求失败抛出异常 throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid."); } //如果返回值正常,数据在网络中是以流的形式得到服务端返回的数据 bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); StringBuilder stringBuilder = new StringBuilder(); String line; // 从流中读取响应信息 while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line); } String responseMsg = stringBuilder.toString(); log.info(responseMsg); } catch (Exception e) { log.error(e.getMessage()); } finally { //关闭流与请求连接 try { if (Objects.nonNull(bufferedReader)){ bufferedReader.close(); } if (Objects.nonNull(connection)){ connection.disconnect(); } } catch (Exception e) { log.error(e.getMessage()); } } } }
@DisallowConcurrentExecution
的作用:
Quartz定时任务默认是并发执行的,不会等待上一次任务执行完毕,只要有间隔时间到就会执行, 如果定时任执行太长,会长时间占用资源,导致其它任务堵塞。
@DisallowConcurrentExecution
这个注解是加在Job类上的,是禁止并发执行多个相同定义的JobDetail, , 但并不是不能同时执行多个Job, 而是不能并发执行同一个Job Definition(由JobDetail定义), 但是可以同时执行多个不同的JobDetail。
JobExecutionContext
类可以获取很多任务的信息:
@Override public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException { JobKey jobKey = jobExecutionContext.getJobDetail().getKey(); //工作任务名称 String jobName = jobKey.getName(); //工作任务组名称 String groupName = jobKey.getGroup(); //任务类名称(带路径) String classPathName = jobExecutionContext.getJobDetail().getJobClass().getName(); //任务类名称 String className = jobExecutionContext.getJobDetail().getJobClass().getSimpleName(); //获取Trigger内容 TriggerKey triggerKey = jobExecutionContext.getTrigger().getKey(); //触发器名称 String triggerName = triggerKey.getName(); //出发组名称(带路径) String triggerPathName = jobExecutionContext.getTrigger().getClass().getName(); //触发器类名称 String triggerClassName = jobExecutionContext.getTrigger().getClass().getSimpleName(); }
注意:
//解析Description,获取请求url JSONObject jsonObj= (JSONObject) JSONObject.parse(jsonParams); String callUrl = jsonObj.getString("callUrl");
之前我们封装JobInfo信息是将Map<String, Object> jsonParams保存接口请求信息,们去解析接口请求的时候也要用callUrl
去取,那么在封装JobInfo类时,Map中就需要指定key是callUrl
,不然取不到就会报错了。
6、创建定时任务业务层,创建 JobService接口和 JobServiceImpl实现类
JobServiceImpl业务逻辑:
JobInfo携带这我们需要新建任务的信息
① 通过jobName和jobGroup可以查询到任务唯一的jobKey,通过getJobDetail(jobKey),判断是否已有这个任务,有的话先删除在新增这个任务
② 新建一个任务JobDetail,withDescription属性中是指任务描述,JobInfo类中JsonParams属性是一个Map,这里需要将Map格式化一下,不然无法赋给withDescription,这个JsonUtils
在下面:
//任务详情 JobDetail jobDetail = JobBuilder.newJob(HttpRemoteJob.class) .withDescription(JsonUtils.object2Json(jobInfo.getJsonParams())) //任务描述 .withIdentity(jobKey) //指定任务 .build();
③ 创建触发器,触发器有多种类型,需要定时执行就使用cron表达式,创建cron调度器建造器CronScheduleBuilder
,再创建Trigger触发器,调度器建造器有很多种,除了我下面用到的简单调度器构造器SimpleTrigger
,还有:
CalendarIntervalScheduleBuilder
: 每隔一段时间执行一次(年月日)
DailyTimeIntervalScheduleBuilder
: 设置年月日中的某些固定日期,可以设置执行总次数
以后我们再单独写一片介绍;
CronScheduleBuilder和SimpleTrigger的区别在于:CronScheduleBuilder是通过cron表达式定时某个时间点或多个时间点定时直接,而SimpleTrigger是周期性执行,着重与时间间隔,着重与周期执行;
④ 将任务添加到Scheduler中
此外还有一些Scheduler的其他方法:
获取任务触发器 :TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
停止触发器 :scheduler.pauseTrigger(triggerKey);
移除触发器:scheduler.unscheduleJob(triggerKey);
删除任务:scheduler.deleteJob(JobKey.jobKey(jobName,jobGroup));
根据jobName,jobGroup获取jobKey 恢复任务:scheduler.resumeJob(JobKey.jobKey(jobName,jobGroup));
根据jobName,jobGroup获取jobKey 暂停任务: scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup));
根据jobName,jobGroup获取jobKey 立即执行任务: scheduler.triggerJob(JobKey.jobKey(jobName,jobGroup));
在下面的实现类代码中有很好的用例;
JsonUtils工具类
public class JsonUtils { public static final ObjectMapper OBJECT_MAPPER = createObjectMapper(); private static final ObjectMapper IGNORE_OBJECT_MAPPER = createIgnoreObjectMapper(); private static ObjectMapper createIgnoreObjectMapper() { ObjectMapper objectMapper = createObjectMapper(); objectMapper.addMixIn(Object.class, DynamicMixIn.class); return objectMapper; } public static ObjectMapper createObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true); objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true); objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); objectMapper.registerModule(new JavaTimeModule()); return objectMapper; } public static String object2Json(Object o) { StringWriter sw = new StringWriter(); JsonGenerator gen = null; try { gen = new JsonFactory().createGenerator(sw); OBJECT_MAPPER.writeValue(gen, o); } catch (IOException e) { throw new RuntimeException("Cannot serialize object as JSON", e); } finally { if (null != gen) { try { gen.close(); } catch (IOException e) { throw new RuntimeException("Cannot serialize object as JSON", e); } } } return sw.toString(); } }
JobService接口
import liu.qingxu.domain.JobInfo; import org.springframework.web.bind.annotation.RequestBody; /** * @module * @author: qingxu.liu * @date: 2022-11-15 14:37 * @copyright **/ public interface JobService { /** * 新建一个定时任务 * @param jobInfo 任务信息 * @return 任务信息 */ JobInfo save(@RequestBody JobInfo jobInfo); /** * 新建一个简单定时任务 * @param jobInfo 任务信息 * @return 任务信息 */ JobInfo simpleSave(@RequestBody JobInfo jobInfo); /** * 删除任务 * @param jobName 任务名称 * @param jobGroup 任务组 */ void remove( String jobName,String jobGroup); /** * 恢复任务 * @param jobName 任务名称 * @param jobGroup 任务组 */ void resume(String jobName, String jobGroup); /** * 暂停任务 * @param jobName 任务名称 * @param jobGroup 任务组 */ void pause(String jobName, String jobGroup); /** * 立即执行任务一主要是用于执行一次任务的场景 * @param jobName 任务名称 * @param jobGroup 任务组 */ void trigger(String jobName, String jobGroup); }
JobServiceImpl实现类
import liu.qingxu.domain.JobInfo; import liu.qingxu.executors.HttpRemoteJob; import liu.qingxu.service.JobService; import liu.qingxu.utils.JsonUtils; import org.quartz.CronScheduleBuilder; import org.quartz.JobBuilder; import org.quartz.JobDetail; import org.quartz.JobKey; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.TriggerBuilder; import org.quartz.TriggerKey; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Objects; import java.util.TimeZone; /** * @module * @author: qingxu.liu * @date: 2022-11-15 14:48 * @copyright **/ @Service public class JobServiceImpl implements JobService { @Autowired private Scheduler scheduler; @Override public JobInfo save(JobInfo jobInfo) { //查询是否已有相同任务 jobKey可以唯一确定一个任务 JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobInfo.getJobGroup()); try { JobDetail jobDetail = scheduler.getJobDetail(jobKey); if (Objects.nonNull(jobDetail)){ scheduler.deleteJob(jobKey); } } catch (SchedulerException e) { e.printStackTrace(); } //任务详情 JobDetail jobDetail = JobBuilder.newJob(HttpRemoteJob.class) .withDescription(JsonUtils.object2Json(jobInfo.getJsonParams())) //任务描述 .withIdentity(jobKey) //指定任务 .build(); //根据cron,TimeZone时区,指定执行计划 CronScheduleBuilder builder = CronScheduleBuilder.cronSchedule(jobInfo.getCron()) .inTimeZone(TimeZone.getTimeZone(jobInfo.getTimeZoneId())); //触发器 Trigger trigger = TriggerBuilder.newTrigger() .withIdentity(jobInfo.getJobName(), jobInfo.getJobGroup()).startNow() .withSchedule(builder) .build(); //添加任务 try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { System.out.println(e.getMessage()); } return jobInfo; } @Override public JobInfo simpleSave(JobInfo jobInfo) { JobKey jobKey = JobKey.jobKey(jobInfo.getJobName(), jobInfo.getJobGroup()); //作业名称及其组名 //判断是否有相同的作业 try { JobDetail jobDetail = scheduler.getJobDetail(jobKey); if(jobDetail != null){ scheduler.deleteJob(jobKey); } } catch (SchedulerException e) { e.printStackTrace(); } //定义作业的详细信息,并设置要执行的作业类名,设置作业名称及其组名 JobDetail jobDetail = JobBuilder.newJob(HttpRemoteJob.class) .withDescription(JsonUtils.object2Json(jobInfo.getJsonParams())) .withIdentity(jobKey) .build() ; //简单触发器,着重与时间间隔 SimpleTrigger trigger = (SimpleTrigger) TriggerBuilder.newTrigger() .withIdentity(jobInfo.getJobName(), jobInfo.getJobGroup()) .startAt(jobInfo.getTriggerTime()) .build(); try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { System.out.println(e.getMessage()); } return jobInfo; } @Override public void remove(String jobName, String jobGroup) { //获取任务触发器 TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup); try { //停止触发器 scheduler.pauseTrigger(triggerKey); //移除触发器 scheduler.unscheduleJob(triggerKey); //删除任务 scheduler.deleteJob(JobKey.jobKey(jobName,jobGroup)); } catch (SchedulerException e) { System.out.println(e.getMessage()); } } @Override public void resume(String jobName, String jobGroup) { try { //根据jobName,jobGroup获取jobKey 恢复任务 scheduler.resumeJob(JobKey.jobKey(jobName,jobGroup)); } catch (SchedulerException e) { System.out.println(e.getMessage()); } } @Override public void pause(String jobName, String jobGroup) { try { //根据jobName,jobGroup获取jobKey 暂停任务 scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup)); } catch (SchedulerException e) { System.out.println(e.getMessage()); } } @Override public void trigger(String jobName, String jobGroup) { try { //根据jobName,jobGroup获取jobKey 立即执行任务 scheduler.triggerJob(JobKey.jobKey(jobName,jobGroup)); } catch (SchedulerException e) { System.out.println(e.getMessage()); } } }
到此为止,我的定时调用接口任务已经完成了,现在我们写个Controller来试着调用一下:
我们调用test接口,新建一个任务,让任务每隔5秒调用runTest接口,然后在调用deleteTest接口删除任务;
@RestController @RequestMapping("/quartz/job") public class QuartzController { private final JobService jobService; public QuartzController(JobService jobService) { this.jobService = jobService; } @GetMapping("/test") public void test(){ JobInfo jobInfo = new JobInfo(); jobInfo.setJobName("test-job"); jobInfo.setJobGroup("test"); jobInfo.setTimeZoneId("Asia/Shanghai"); //时区指定上海 jobInfo.setCron("0/5 * * * * ? "); //每5秒执行一次 Map<String, Object> params = new HashMap<>(); //添加需要调用的接口信息 String callUrl = "http://127.0.0.1:8080/quartz/job/test/run"; params.put("callUrl", callUrl); jobInfo.setJsonParams(params); jobService.save(jobInfo); } @GetMapping("/test/run") public void runTest(){ System.out.println(new Date()); } @GetMapping("/test/delete") public void deleteTest(){ jobService.remove("test-job","test"); System.out.println("任务已删除"); } }
四、验证结果
可以看到runTest接口每隔5秒就被任务调用了一个,这时候我们去看数据库中的scheduler库的QRTZ_JOB_DETAILS表和QRTZ_TRIGGERS表,可以看到我们我们定时任务的信息,表示我们的定时任务持久化成功,这个时候你关掉服务器,再重启任务也会定时去执行runTest接口;
然后我们再调用deleteTest接口删除任务
此时再看去看数据库中的scheduler库的QRTZ_JOB_DETAILS表和QRTZ_TRIGGERS表中就没有之前的任务数据了
以上就是SpringBoot集成Quartz实现持久化定时接口调用任务的详细内容,更多关于SpringBoot Quartz实现定时任务的资料请关注脚本之家其它相关文章!