SpringBoot集成Quartz實(shí)現(xiàn)持久化定時(shí)接口調(diào)用任務(wù)
一、基本概念
Quartz 是功能強(qiáng)大的開(kāi)源作業(yè)調(diào)度庫(kù),幾乎可以集成到任何 Java 應(yīng)用程序中,從最小的獨(dú)立應(yīng)用程序到最大的電子商務(wù)系統(tǒng)。Quartz 可用于創(chuàng)建簡(jiǎn)單或復(fù)雜的計(jì)劃,以執(zhí)行數(shù)以萬(wàn)計(jì)的工作;可以執(zhí)行您編寫(xiě)的所有內(nèi)容。
Spring Boot 官方也對(duì) Quartz 調(diào)度器進(jìn)行了集成,Spring boot 官網(wǎng)文檔:Quartz Scheduler,Java JDK 也帶有 計(jì)時(shí)器 Timer 以及 定時(shí)執(zhí)行服務(wù) ScheduledExecutorService ,Spring 也提供了 @Scheduled 執(zhí)行定時(shí)任務(wù)。
如果實(shí)際環(huán)境中定時(shí)任務(wù)過(guò)多,處理頻繁,建議適應(yīng)第三方封裝的調(diào)度框架,因?yàn)槎〞r(shí)器操作底層都是多線程的操作,任務(wù)的啟動(dòng)、暫停、恢復(fù)、刪除、實(shí)質(zhì)是線程的啟動(dòng)、暫停、中斷、喚醒等操作。
二、Quartz-scheduler 的核心流程

Scheduler - 調(diào)度器
1、Scheduler 用來(lái)對(duì) Trigger 和 Job 進(jìn)行管理,Trigger 和 JobDetail 可以注冊(cè)到 Scheduler 中,兩者在 Scheduler 中都擁有自己的唯一的組(group)和名稱(chēng)(name)用來(lái)進(jìn)行彼此的區(qū)分,Scheduler 可以通過(guò)任務(wù)組和名稱(chēng)來(lái)對(duì) Trigger 和 JobDetail 進(jìn)行管理。
2、每個(gè) Scheduler 都有一個(gè) SchedulerContext,用來(lái)保存 Scheduler 的上下文數(shù)據(jù),Job 和 Trigger 都可以獲取其中的信息。
3、Scheduler 是由 SchedulerFactory 創(chuàng)建,它有兩個(gè)實(shí)現(xiàn):DirectSchedulerFactory 、StdSchdulerFactory ,前者可以用來(lái)在代碼里定制 Schduler 參數(shù),后者直接讀取 classpath 下的 quartz.properties(不存在就都使用默認(rèn)值)配置來(lái)實(shí)例化 Scheduler。
Job - 任務(wù)
1、Job 是一個(gè)任務(wù)接口,開(kāi)發(fā)者可以實(shí)現(xiàn)該接口定義自己的任務(wù),JobExecutionContext 中提供了調(diào)度上下文的各種信息。
2、Job 中的任務(wù)有可能并發(fā)執(zhí)行,例如任務(wù)的執(zhí)行時(shí)間過(guò)長(zhǎng),而每次觸發(fā)的時(shí)間間隔太短,則會(huì)導(dǎo)致任務(wù)會(huì)被并發(fā)執(zhí)行。如果是并發(fā)執(zhí)行,就需要一個(gè)數(shù)據(jù)庫(kù)鎖去避免一個(gè)數(shù)據(jù)被多次處理??梢栽?execute()方法上添加 @DisallowConcurrentExecution 注解解決這個(gè)問(wèn)題。
JobDetail - 任務(wù)詳情
1、JobDetail 對(duì)象是在將 job 注冊(cè)到 scheduler 時(shí),由客戶端程序創(chuàng)建的,它包含 job 的各種屬性設(shè)置,以及用于存儲(chǔ) job 實(shí)例狀態(tài)信息的 JobDataMap。
2、JobDetail 由 JobBuilder 創(chuàng)建/定義,Quartz 不存儲(chǔ) Job 的實(shí)際實(shí)例,但是允許通過(guò)使用 JobDetail 定義一個(gè)實(shí)例。
3、Job 有一個(gè)與其關(guān)聯(lián)的名稱(chēng)和組,應(yīng)該在單個(gè) Scheduler 中唯一標(biāo)識(shí)它們。
4、一個(gè) Trigger(觸發(fā)器) 只能對(duì)應(yīng)一個(gè) Job(任務(wù)),但是一個(gè) Job 可以對(duì)應(yīng)多個(gè) Trigger。JobDetal 與 Trigger 一對(duì)多
Trigger - 觸發(fā)器
1、Trigger 用于觸發(fā) Job 的執(zhí)行。TriggerBuilder 用于定義/構(gòu)建觸發(fā)器實(shí)例。
2、Trigger也有一個(gè)相關(guān)聯(lián)的 JobDataMap,用于給Job傳遞一些觸發(fā)相關(guān)的參數(shù)。
3、Quartz自帶了各種不同類(lèi)型的 Trigger,最常用的主要是SimpleTrigger和CronTrigger。
JobDataMap
1、JobDataMap 實(shí)現(xiàn)了 JDK 的 Map 接口,可以以 Key-Value 的形式存儲(chǔ)數(shù)據(jù)。
2、JobDetail、Trigger 實(shí)現(xiàn)類(lèi)中都定義 JobDataMap 成員變量及其 getter、setter 方法,可以用來(lái)設(shè)置參數(shù)信息,Job 執(zhí)行 execute() 方法的時(shí)候,JobExecutionContext 可以獲取到 JobDataMap 中的信息。
三、實(shí)踐
1、新建一個(gè)quartz-service服務(wù)

添加依賴(lài):
<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>
<!-- 公共依賴(lài) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.19</version>
</dependency>
<!-- quartz依賴(lài) -->
<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、配置數(shù)據(jù)源相關(guān)鏈接
quartz需要單據(jù)的數(shù)據(jù)庫(kù),所以需要單據(jù)創(chuàng)建一個(gè)庫(kù)來(lái)給quartz使用,我新建了一個(gè)scheduler的庫(kù)

#1 保存已經(jīng)觸發(fā)的觸發(fā)器狀態(tài)信息
DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS;
#2 存放暫停掉的觸發(fā)器表表
DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS;
#3 調(diào)度器狀態(tài)表
DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE;
#4 存儲(chǔ)程序的悲觀鎖的信息(假如使用了悲觀鎖)
DROP TABLE IF EXISTS QRTZ_LOCKS;
#5 簡(jiǎn)單的觸發(fā)器表
DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS;
#6 存儲(chǔ)兩種類(lèi)型的觸發(fā)器表
DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS;
#7 定時(shí)觸發(fā)器表
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
#8 以blob 類(lèi)型存儲(chǔ)的觸發(fā)器
DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS;
#9 觸發(fā)器表
DROP TABLE IF EXISTS QRTZ_TRIGGERS;
#10 job 詳細(xì)信息表
DROP TABLE IF EXISTS QRTZ_JOB_DETAILS;
#11 日歷信息表
DROP TABLE IF EXISTS QRTZ_CALENDARS;
#job 詳細(xì)信息表
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)
);
#觸發(fā)器表
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)
);
#簡(jiǎn)單的觸發(fā)器表,包括重復(fù)次數(shù),間隔,以及已觸發(fā)的次數(shù)
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)
);
#定時(shí)觸發(fā)器表,存儲(chǔ) cron trigger,包括 cron 表達(dá)式和時(shí)區(qū)信息
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)
);
#存儲(chǔ)calendarintervaltrigger和dailytimeintervaltrigger兩種類(lèi)型的觸發(fā)器
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 類(lèi)型存儲(chǔ)的觸發(fā)器
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可配置一個(gè)日歷來(lái)指定一個(gè)時(shí)間范圍
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)
);
#存放暫停掉的觸發(fā)器表表
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
# 存儲(chǔ)與已觸發(fā)的 trigger 相關(guān)的狀態(tài)信息,以及相聯(lián) job 的執(zhí)行信息
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、 調(diào)度器狀態(tài)表
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、 存儲(chǔ)程序的悲觀鎖的信息(假如使用了悲觀鎖)
CREATE TABLE QRTZ_LOCKS
(
SCHED_NAME VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);3、創(chuàng)建配置文件
Quartz 使用一個(gè)名為 quartz.properties 的屬性文件進(jìn)行信息配置,必須位于 classpath 下,是 StdSchedulerFactory 用于創(chuàng)建 Scheduler 的默認(rèn)屬性文件。默認(rèn)情況下 StdSchedulerFactory 從類(lèi)路徑下加載名為 “quartz.properties” 的屬性文件,如果失敗,則加載 org/quartz 包中的“quartz.properties”文件,因?yàn)槲倚枰氖切陆ㄒ粋€(gè)Scheduler服務(wù),所以直接使用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
#定時(shí)配置
quartz:
#相關(guān)屬性配置
properties:
org:
quartz:
scheduler:
instanceName: local-scheduler-svc
instanceId: AUTO
jobStore:
#表示 quartz 中的所有數(shù)據(jù),比如作業(yè)和觸發(fā)器的信息都保存在內(nèi)存中(而不是數(shù)據(jù)庫(kù)中)
class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
# 驅(qū)動(dòng)配置
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
# 表前綴
tablePrefix: QRTZ_
#是否為集群
isClustered: false
clusterCheckinInterval: 10000
useProperties: false
dataSource: quartzDs
#線程池配置
threadPool:
class: org.quartz.simpl.SimpleThreadPool
#線程數(shù)
threadCount: 10
#優(yōu)先級(jí)
threadPriority: 5
#線程繼承上下文類(lèi)加載器的初始化線程
threadsInheritContextClassLoaderOfInitializingThread: true
#數(shù)據(jù)庫(kù)方式
job-store-type: JDBC
#初始化表結(jié)構(gòu)
jdbc:
initialize-schema: NEVER4、新建一個(gè)任務(wù)實(shí)體類(lèi)JobInfo,用戶新建,傳遞任務(wù)信息
jobName:任務(wù)名稱(chēng)
jobGroup:任務(wù)組
jsonParams:任務(wù)執(zhí)行信息(在用戶定時(shí)遠(yuǎn)程調(diào)用接口的時(shí)候,我們可以接口信息封裝到這個(gè)Map中)
cron:定時(shí)任務(wù)的cron表達(dá)式
timeZoneId:定制執(zhí)行任務(wù)的時(shí)區(qū)
triggerTime:定時(shí)器時(shí)間(目前沒(méi)用上)
@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、新建一個(gè)任務(wù)執(zhí)行類(lèi)HttpRemoteJob 實(shí)現(xiàn) Job接口,重寫(xiě)execute()方法
execute()里面就是任務(wù)的邏輯:
① 使用HttpURLConnection發(fā)送網(wǎng)絡(luò)請(qǐng)求,利用BufferedReader接收請(qǐng)求返回的結(jié)果
② 在任務(wù)的Description中取出定時(shí)請(qǐng)求的接口信息,解析Description,獲取請(qǐng)求url
③ 編輯請(qǐng)求信息,通過(guò)URL類(lèi)編輯請(qǐng)求信息,使用HttpURLConnection發(fā)送請(qǐng)求,并接收請(qǐng)求返回的狀態(tài)碼,根據(jù)狀態(tài)碼,判斷是否請(qǐng)求成功,請(qǐng)求成功便通過(guò)BufferedReader讀取響應(yīng)信息,返回請(qǐng)求結(jié)果
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 {
//用于發(fā)送網(wǎng)絡(luò)請(qǐng)求
HttpURLConnection connection = null;
//用于接收請(qǐng)求返回的結(jié)果
BufferedReader bufferedReader = null;
//獲取任務(wù)Description述,之前我們把接口請(qǐng)求的信息放在Description里面了
String jsonParams = context.getJobDetail().getDescription();
if (StringUtils.isEmpty(jsonParams)){
return;
}
//解析Description,獲取請(qǐng)求url
JSONObject jsonObj= (JSONObject) JSONObject.parse(jsonParams);
String callUrl = jsonObj.getString("callUrl");
if(StringUtils.isEmpty(callUrl)) {
return;
}
try {
//編輯請(qǐng)求信息
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");
//發(fā)送請(qǐng)求
connection.connect();
//獲取請(qǐng)求返回的狀態(tài)嗎
int statusCode = connection.getResponseCode();
if (statusCode != 200){
//請(qǐng)求失敗拋出異常
throw new RuntimeException("Http Request StatusCode(" + statusCode + ") Invalid.");
}
//如果返回值正常,數(shù)據(jù)在網(wǎng)絡(luò)中是以流的形式得到服務(wù)端返回的數(shù)據(jù)
bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
StringBuilder stringBuilder = new StringBuilder();
String line;
// 從流中讀取響應(yīng)信息
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line);
}
String responseMsg = stringBuilder.toString();
log.info(responseMsg);
} catch (Exception e) {
log.error(e.getMessage());
} finally {
//關(guān)閉流與請(qǐng)求連接
try {
if (Objects.nonNull(bufferedReader)){
bufferedReader.close();
}
if (Objects.nonNull(connection)){
connection.disconnect();
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
}@DisallowConcurrentExecution 的作用:
Quartz定時(shí)任務(wù)默認(rèn)是并發(fā)執(zhí)行的,不會(huì)等待上一次任務(wù)執(zhí)行完畢,只要有間隔時(shí)間到就會(huì)執(zhí)行, 如果定時(shí)任執(zhí)行太長(zhǎng),會(huì)長(zhǎng)時(shí)間占用資源,導(dǎo)致其它任務(wù)堵塞。
@DisallowConcurrentExecution這個(gè)注解是加在Job類(lèi)上的,是禁止并發(fā)執(zhí)行多個(gè)相同定義的JobDetail, , 但并不是不能同時(shí)執(zhí)行多個(gè)Job, 而是不能并發(fā)執(zhí)行同一個(gè)Job Definition(由JobDetail定義), 但是可以同時(shí)執(zhí)行多個(gè)不同的JobDetail。
JobExecutionContext 類(lèi)可以獲取很多任務(wù)的信息:
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobKey jobKey = jobExecutionContext.getJobDetail().getKey();
//工作任務(wù)名稱(chēng)
String jobName = jobKey.getName();
//工作任務(wù)組名稱(chēng)
String groupName = jobKey.getGroup();
//任務(wù)類(lèi)名稱(chēng)(帶路徑)
String classPathName = jobExecutionContext.getJobDetail().getJobClass().getName();
//任務(wù)類(lèi)名稱(chēng)
String className = jobExecutionContext.getJobDetail().getJobClass().getSimpleName();
//獲取Trigger內(nèi)容
TriggerKey triggerKey = jobExecutionContext.getTrigger().getKey();
//觸發(fā)器名稱(chēng)
String triggerName = triggerKey.getName();
//出發(fā)組名稱(chēng)(帶路徑)
String triggerPathName = jobExecutionContext.getTrigger().getClass().getName();
//觸發(fā)器類(lèi)名稱(chēng)
String triggerClassName = jobExecutionContext.getTrigger().getClass().getSimpleName();
}注意:
//解析Description,獲取請(qǐng)求url
JSONObject jsonObj= (JSONObject) JSONObject.parse(jsonParams);
String callUrl = jsonObj.getString("callUrl");之前我們封裝JobInfo信息是將Map<String, Object> jsonParams保存接口請(qǐng)求信息,們?nèi)ソ馕鼋涌谡?qǐng)求的時(shí)候也要用callUrl去取,那么在封裝JobInfo類(lèi)時(shí),Map中就需要指定key是callUrl,不然取不到就會(huì)報(bào)錯(cuò)了。
6、創(chuàng)建定時(shí)任務(wù)業(yè)務(wù)層,創(chuàng)建 JobService接口和 JobServiceImpl實(shí)現(xiàn)類(lèi)
JobServiceImpl業(yè)務(wù)邏輯:
JobInfo攜帶這我們需要新建任務(wù)的信息
① 通過(guò)jobName和jobGroup可以查詢到任務(wù)唯一的jobKey,通過(guò)getJobDetail(jobKey),判斷是否已有這個(gè)任務(wù),有的話先刪除在新增這個(gè)任務(wù)
② 新建一個(gè)任務(wù)JobDetail,withDescription屬性中是指任務(wù)描述,JobInfo類(lèi)中JsonParams屬性是一個(gè)Map,這里需要將Map格式化一下,不然無(wú)法賦給withDescription,這個(gè)JsonUtils在下面:
//任務(wù)詳情
JobDetail jobDetail = JobBuilder.newJob(HttpRemoteJob.class)
.withDescription(JsonUtils.object2Json(jobInfo.getJsonParams())) //任務(wù)描述
.withIdentity(jobKey) //指定任務(wù)
.build();③ 創(chuàng)建觸發(fā)器,觸發(fā)器有多種類(lèi)型,需要定時(shí)執(zhí)行就使用cron表達(dá)式,創(chuàng)建cron調(diào)度器建造器CronScheduleBuilder,再創(chuàng)建Trigger觸發(fā)器,調(diào)度器建造器有很多種,除了我下面用到的簡(jiǎn)單調(diào)度器構(gòu)造器SimpleTrigger,還有:
CalendarIntervalScheduleBuilder : 每隔一段時(shí)間執(zhí)行一次(年月日)
DailyTimeIntervalScheduleBuilder : 設(shè)置年月日中的某些固定日期,可以設(shè)置執(zhí)行總次數(shù)
以后我們?cè)賳为?dú)寫(xiě)一片介紹;
CronScheduleBuilder和SimpleTrigger的區(qū)別在于:CronScheduleBuilder是通過(guò)cron表達(dá)式定時(shí)某個(gè)時(shí)間點(diǎn)或多個(gè)時(shí)間點(diǎn)定時(shí)直接,而SimpleTrigger是周期性執(zhí)行,著重與時(shí)間間隔,著重與周期執(zhí)行;
④ 將任務(wù)添加到Scheduler中
此外還有一些Scheduler的其他方法:
獲取任務(wù)觸發(fā)器 :TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
停止觸發(fā)器 :scheduler.pauseTrigger(triggerKey);
移除觸發(fā)器:scheduler.unscheduleJob(triggerKey);
刪除任務(wù):scheduler.deleteJob(JobKey.jobKey(jobName,jobGroup));
根據(jù)jobName,jobGroup獲取jobKey 恢復(fù)任務(wù):scheduler.resumeJob(JobKey.jobKey(jobName,jobGroup));
根據(jù)jobName,jobGroup獲取jobKey 暫停任務(wù): scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup));
根據(jù)jobName,jobGroup獲取jobKey 立即執(zhí)行任務(wù): scheduler.triggerJob(JobKey.jobKey(jobName,jobGroup));
在下面的實(shí)現(xiàn)類(lèi)代碼中有很好的用例;
JsonUtils工具類(lèi)
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 {
/**
* 新建一個(gè)定時(shí)任務(wù)
* @param jobInfo 任務(wù)信息
* @return 任務(wù)信息
*/
JobInfo save(@RequestBody JobInfo jobInfo);
/**
* 新建一個(gè)簡(jiǎn)單定時(shí)任務(wù)
* @param jobInfo 任務(wù)信息
* @return 任務(wù)信息
*/
JobInfo simpleSave(@RequestBody JobInfo jobInfo);
/**
* 刪除任務(wù)
* @param jobName 任務(wù)名稱(chēng)
* @param jobGroup 任務(wù)組
*/
void remove( String jobName,String jobGroup);
/**
* 恢復(fù)任務(wù)
* @param jobName 任務(wù)名稱(chēng)
* @param jobGroup 任務(wù)組
*/
void resume(String jobName, String jobGroup);
/**
* 暫停任務(wù)
* @param jobName 任務(wù)名稱(chēng)
* @param jobGroup 任務(wù)組
*/
void pause(String jobName, String jobGroup);
/**
* 立即執(zhí)行任務(wù)一主要是用于執(zhí)行一次任務(wù)的場(chǎng)景
* @param jobName 任務(wù)名稱(chēng)
* @param jobGroup 任務(wù)組
*/
void trigger(String jobName, String jobGroup);
}JobServiceImpl實(shí)現(xiàn)類(lèi)
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) {
//查詢是否已有相同任務(wù) jobKey可以唯一確定一個(gè)任務(wù)
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();
}
//任務(wù)詳情
JobDetail jobDetail = JobBuilder.newJob(HttpRemoteJob.class)
.withDescription(JsonUtils.object2Json(jobInfo.getJsonParams())) //任務(wù)描述
.withIdentity(jobKey) //指定任務(wù)
.build();
//根據(jù)cron,TimeZone時(shí)區(qū),指定執(zhí)行計(jì)劃
CronScheduleBuilder builder = CronScheduleBuilder.cronSchedule(jobInfo.getCron())
.inTimeZone(TimeZone.getTimeZone(jobInfo.getTimeZoneId()));
//觸發(fā)器
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(jobInfo.getJobName(), jobInfo.getJobGroup()).startNow()
.withSchedule(builder)
.build();
//添加任務(wù)
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()); //作業(yè)名稱(chēng)及其組名
//判斷是否有相同的作業(yè)
try {
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
if(jobDetail != null){
scheduler.deleteJob(jobKey);
}
} catch (SchedulerException e) {
e.printStackTrace();
}
//定義作業(yè)的詳細(xì)信息,并設(shè)置要執(zhí)行的作業(yè)類(lèi)名,設(shè)置作業(yè)名稱(chēng)及其組名
JobDetail jobDetail = JobBuilder.newJob(HttpRemoteJob.class)
.withDescription(JsonUtils.object2Json(jobInfo.getJsonParams()))
.withIdentity(jobKey)
.build()
;
//簡(jiǎn)單觸發(fā)器,著重與時(shí)間間隔
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) {
//獲取任務(wù)觸發(fā)器
TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
try {
//停止觸發(fā)器
scheduler.pauseTrigger(triggerKey);
//移除觸發(fā)器
scheduler.unscheduleJob(triggerKey);
//刪除任務(wù)
scheduler.deleteJob(JobKey.jobKey(jobName,jobGroup));
} catch (SchedulerException e) {
System.out.println(e.getMessage());
}
}
@Override
public void resume(String jobName, String jobGroup) {
try {
//根據(jù)jobName,jobGroup獲取jobKey 恢復(fù)任務(wù)
scheduler.resumeJob(JobKey.jobKey(jobName,jobGroup));
} catch (SchedulerException e) {
System.out.println(e.getMessage());
}
}
@Override
public void pause(String jobName, String jobGroup) {
try {
//根據(jù)jobName,jobGroup獲取jobKey 暫停任務(wù)
scheduler.pauseJob(JobKey.jobKey(jobName,jobGroup));
} catch (SchedulerException e) {
System.out.println(e.getMessage());
}
}
@Override
public void trigger(String jobName, String jobGroup) {
try {
//根據(jù)jobName,jobGroup獲取jobKey 立即執(zhí)行任務(wù)
scheduler.triggerJob(JobKey.jobKey(jobName,jobGroup));
} catch (SchedulerException e) {
System.out.println(e.getMessage());
}
}
}到此為止,我的定時(shí)調(diào)用接口任務(wù)已經(jīng)完成了,現(xiàn)在我們寫(xiě)個(gè)Controller來(lái)試著調(diào)用一下:
我們調(diào)用test接口,新建一個(gè)任務(wù),讓任務(wù)每隔5秒調(diào)用runTest接口,然后在調(diào)用deleteTest接口刪除任務(wù);
@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"); //時(shí)區(qū)指定上海
jobInfo.setCron("0/5 * * * * ? "); //每5秒執(zhí)行一次
Map<String, Object> params = new HashMap<>();
//添加需要調(diào)用的接口信息
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("任務(wù)已刪除");
}
}四、驗(yàn)證結(jié)果


可以看到runTest接口每隔5秒就被任務(wù)調(diào)用了一個(gè),這時(shí)候我們?nèi)タ磾?shù)據(jù)庫(kù)中的scheduler庫(kù)的QRTZ_JOB_DETAILS表和QRTZ_TRIGGERS表,可以看到我們我們定時(shí)任務(wù)的信息,表示我們的定時(shí)任務(wù)持久化成功,這個(gè)時(shí)候你關(guān)掉服務(wù)器,再重啟任務(wù)也會(huì)定時(shí)去執(zhí)行runTest接口;


然后我們?cè)僬{(diào)用deleteTest接口刪除任務(wù)

此時(shí)再看去看數(shù)據(jù)庫(kù)中的scheduler庫(kù)的QRTZ_JOB_DETAILS表和QRTZ_TRIGGERS表中就沒(méi)有之前的任務(wù)數(shù)據(jù)了
以上就是SpringBoot集成Quartz實(shí)現(xiàn)持久化定時(shí)接口調(diào)用任務(wù)的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot Quartz實(shí)現(xiàn)定時(shí)任務(wù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- SpringBoot+Quartz實(shí)現(xiàn)定時(shí)任務(wù)的代碼模版分享
- SpringBoot集成quartz實(shí)現(xiàn)定時(shí)任務(wù)詳解
- SpringBoot集成Quartz實(shí)現(xiàn)定時(shí)任務(wù)的方法
- 淺談SpringBoot集成Quartz動(dòng)態(tài)定時(shí)任務(wù)
- Springboot整個(gè)Quartz實(shí)現(xiàn)動(dòng)態(tài)定時(shí)任務(wù)的示例代碼
- springboot整合Quartz實(shí)現(xiàn)動(dòng)態(tài)配置定時(shí)任務(wù)的方法
相關(guān)文章
SpringBoot3.x打包Docker容器的實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot3.x打包Docker容器的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11
SpringBoot使用Flyway進(jìn)行數(shù)據(jù)庫(kù)遷移的實(shí)現(xiàn)示例
Flyway是一個(gè)數(shù)據(jù)庫(kù)遷移工具,它提供遷移歷史和回滾的功能,本文主要介紹了如何使用Flyway來(lái)管理Spring Boot應(yīng)用程序中的SQL數(shù)據(jù)庫(kù)架構(gòu),感興趣的可以了解一下2023-08-08
spring注入配置文件屬性到j(luò)ava類(lèi)
這篇文章主要為大家介紹了spring注入配置文件屬性到j(luò)ava類(lèi)實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07
Java數(shù)組創(chuàng)建的3種方法6種寫(xiě)法代碼示例
這篇文章主要給大家介紹了關(guān)于Java數(shù)組創(chuàng)建的3種方法6種寫(xiě)法,在Java中我們可以使用關(guān)鍵字new來(lái)創(chuàng)建一個(gè)數(shù)組,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01
劍指Offer之Java算法習(xí)題精講二叉樹(shù)專(zhuān)題篇上
跟著思路走,之后從簡(jiǎn)單題入手,反復(fù)去看,做過(guò)之后可能會(huì)忘記,之后再做一次,記不住就反復(fù)做,反復(fù)尋求思路和規(guī)律,慢慢積累就會(huì)發(fā)現(xiàn)質(zhì)的變化2022-03-03
Spring擴(kuò)展之基于HandlerMapping實(shí)現(xiàn)接口灰度發(fā)布實(shí)例
這篇文章主要介紹了Spring擴(kuò)展之基于HandlerMapping實(shí)現(xiàn)接口灰度發(fā)布實(shí)例,灰度發(fā)布是指在黑與白之間,能夠平滑過(guò)渡的一種發(fā)布方式,灰度發(fā)布可以保證整體系統(tǒng)的穩(wěn)定,在初始灰度的時(shí)候就可以發(fā)現(xiàn)、調(diào)整問(wèn)題,以保證其影響度,需要的朋友可以參考下2023-08-08
使用@SpringBootTest注解進(jìn)行單元測(cè)試
這篇文章主要介紹了使用@SpringBootTest注解進(jìn)行單元測(cè)試,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09

