目錄
quartz.properties Quartz Configuration
quartz-scheduler 石英調度器概述
1、Quartz 是功能強大的開源作業調度庫,幾乎可以集成到任何 Java 應用程序中,從最小的獨立應用程序到最大的電子商務系統。Quartz 可用於創建簡單或複雜的計劃,以執行數以萬計的工作;可以執行您編寫的所有內容。
2、Quartz Scheduler 包含許多企業級功能,例如對 JTA 事務和集羣的支持。Quartz 是免費使用的,並根據 Apache 2.0 許可獲得許可。
Quartz 官網:http://www.quartz-scheduler.org/
二進制 jar 包下載地址:http://www.quartz-scheduler.org/downloads/
官網文檔:http://www.quartz-scheduler.org/documentation/
github 開源地址:https://github.com/quartz-scheduler/quartz
w3c 教程:https://www.w3cschool.cn/quartz_doc/
3、Spring Boot 官方也對 Quartz 調度器進行了集成,Spring boot 官網文檔:Quartz Scheduler
4、Java JDK 有原生的 計時器 Timer 以及 定時執行服務 ScheduledExecutorService ,Spring 也提供了 @Scheduled 執行定時任務。
5、生產中如果定時任務多,處理頻繁,則強烈建議使用第三方封裝的調度框架,因爲定時器操作底層都是多線程的操作,任務的啓動、暫停、恢復、刪除、實質是線程的啓動、暫停、中斷、喚醒等操作。
quartz-scheduler HelloWorld
1、本文環境 Spring Boot 2.1.3 + java jdk 1.8 ,pom.xml 文件中導入 quartz 依賴:
<!--quartz 定時器 https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
源碼:https://github.com/wangmaoxiong/quartzapp/blob/master/pom.xml
spring-boot-starter-quartz 組件內部依賴瞭如下的組件:
ategory/License | Group / Artifact | Version |
---|---|---|
Job Scheduling/Apache 2.0 | org.quartz-scheduler » quartz | 2.3.2 |
Apache 2.0 | org.springframework » spring-context-support | 5.2.4.RELEASE |
Transactions/Apache 2.0 | org.springframework » spring-tx | 5.2.4.RELEASE |
Apache 2.0 | org.springframework.boot » spring-boot-starter | 2.2.5.RELEASE |
2、由淺入深,下面先以一個簡單 main 方法調用來介紹 quartz 作業調度器的基本使用步驟。
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 自定義作業實現 org.quartz.Job 接口,execute 方法中寫作業邏輯.
*/
public class HelloJob implements Job {
private static Logger logger = LoggerFactory.getLogger(HelloJob.class);
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
//獲取觸發器的名稱及其組名稱,獲取作業詳細信息的名稱及其組名稱.
String triggerName = context.getTrigger().getKey().getName();
String triggerGroup = context.getTrigger().getKey().getGroup();
String jobDetailName = context.getJobDetail().getKey().getName();
String jobDetailGroup = context.getJobDetail().getKey().getGroup();
logger.info("執行作業,作業名稱={},作業所屬組={},觸發器名稱={},觸發器所屬組={}",
jobDetailName, jobDetailGroup, triggerName, triggerGroup);
}
}
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
public class HelloTest {
public static void main(String[] args) throws SchedulerException, InterruptedException {
//1)讀取 classpath 下的 quartz.properties(不存在就都使用默認值)配置來實例化 Scheduler
//可以在類路徑下使用同名文件覆蓋 quartz-x.x.x.jar 包下的 org\quartz\quartz.properties 屬性文件
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//2、定義作業的詳細信息,並設置要執行的作業的類名。設置作業的名稱及其組名.
JobDetail jobDetail = JobBuilder.newJob(HelloJob.class).withIdentity("helloJob", "jobGroup").build();
//3、創建觸發器,設置觸發器名稱與組名稱,設置 CronTrigger 觸發器的調度規則爲每 10 秒觸發一次.
//startNow():表示立即觸發任務,否則需要等待下一個觸發點
CronTrigger cronTrigger = TriggerBuilder.newTrigger().withIdentity("helloTrigger", "triggerGroup")
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?")).build();
//4、將 jobDetail 與 trigger 註冊到調度器 scheduler 並啓動。
scheduler.scheduleJob(jobDetail, cronTrigger);
scheduler.start();
TimeUnit.MINUTES.sleep(1);//1分鐘以後停掉調度器
scheduler.shutdown();
}
}
quartz-scheduler 核心 API
Scheduler · 調度器 |
1、Scheduler 用來對 Trigger 和 Job 進行管理,Trigger 和 JobDetail 可以註冊到 Scheduler 中,兩者在 Scheduler 中都擁有自己的唯一的組(group)和名稱(name)用來進行彼此的區分,Scheduler 可以通過組和名稱來對 Trigger 和 JobDetail 進行管理。 |
Job · 任務 |
1、Job 是一個任務接口,開發者可以實現該接口定義自己的任務,JobExecutionContext 中提供了調度上下文的各種信息。 |
JobDetail · 任務詳情 |
1、JobDetail 對象是在將 job 註冊到 scheduler 時,由客戶端程序創建的,它包含 job 的各種屬性設置,以及用於存儲 job 實例狀態信息的 JobDataMap。 |
Trigger·觸發器 |
1、Trigger 用於觸發 Job 的執行。TriggerBuilder 用於定義/構建觸發器實例。 |
JobDataMap | 1、JobDataMap 實現了 JDK 的 Map 接口,可以以 Key-Value 的形式存儲數據。 2、JobDetail、Trigger 實現類中都定義 JobDataMap 成員變量及其 getter、setter 方法,可以用來設置參數信息,Job 執行 execute() 方法的時候,JobExecutionContext 可以獲取到 JobDataMap 中的信息。 |
1、Scheduler 實例化後,可以啓動(start)、暫停(stand-by)、停止(shutdown)。
2、Scheduler 被停止後,除非重新實例化,否則不能重新啓動;只有當 scheduler 啓動後,trigger纔會被觸發(job纔會被執行;暫停狀態 trigger 不會觸發執行任務。
3、Scheduler 生命週期:從 SchedulerFactory 創建它開始,到 Scheduler 調用 shutdown() 方法結束;Scheduler 被創建後,可以增加、刪除和列舉 Job 和 Trigger,以及執行其它與調度相關的操作(如暫停Trigger)。
4、SimpleTrigger 觸發器主要用於一次性執行的 Job(只在某個特定的時間點執行一次),或者每間隔T個時間單位執行一次。CronTrigger 觸發器基於日曆進行調度,如“每個星期五的正午”,或者“每月的第十天的上午10:15”等。
5、Job 被創建後,可以保存在 Scheduler 中,與 Trigger 是獨立的,同一個 Job 可以有多個 Trigger;這種松耦合的一個好處是可以修改或者替換 Trigger,而不用重新定義與之關聯的 Job。
6、Job 和 Trigger 註冊到 Scheduler 時,可以爲它們設置 key,配置其身份屬性。Job 和 Trigger 的key(JobKey和TriggerKey)可以用於將 Job 和 Trigger 放到不同的分組(group)裏,然後基於分組進行操作。同一個分組下的 Job 或 Trigger 的名稱必須唯一,即一個 Job 或 Trigger 的 key 由名稱(name)和分組(group)組成。
quartz.properties Quartz Configuration
1、Quartz 使用一個名爲 quartz.properties 的屬性文件進行信息配置,必須位於 classpath 下,是 StdSchedulerFactory 用於創建 Scheduler 的默認屬性文件。
2、默認情況下 StdSchedulerFactory 從類路徑下加載名爲 “quartz.properties” 的屬性文件,如果失敗,則加載 org/quartz 包中的“quartz.properties”文件,其默認內容如下:
#計劃程序的名稱
org.quartz.scheduler.instanceName: DefaultQuartzScheduler
org.quartz.scheduler.rmi.export: false
org.quartz.scheduler.rmi.proxy: false
org.quartz.scheduler.wrapJobExecutionInUserTransaction: false
org.quartz.threadPool.class: org.quartz.simpl.SimpleThreadPool
#線程池中的線程個數.10個線程表示最多可以同時執行10個任務/作業
org.quartz.threadPool.threadCount: 10
org.quartz.threadPool.threadPriority: 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread: true
org.quartz.jobStore.misfireThreshold: 60000
#表示 quartz 中的所有數據,比如作業和觸發器的信息都保存在內存中(而不是數據庫中)
org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
3、quartz.properties 可用屬性的完整配置信息可以參考官網 Quartz Configuration,根據不同的需求,配置分類如下:
- Main Configuration (configuration of primary scheduler settings, transactions)
- Configuration of ThreadPool (tune resources for job execution)
- Configuration of Listeners (your application can receive notification of scheduled events)
- Configuration of Plug-Ins (add functionality to your scheduler)
- Configuration of RMI Server and Client (use a Quartz instance from a remote process)
- Configuration of RAMJobStore (store jobs and triggers in memory)
- Configuration of JDBC-JobStoreTX (store jobs and triggers in a database via JDBC)
- Configuration of JDBC-JobStoreCMT (JDBC with JTA container-managed transactions)
- Configuration of DataSources (for use by the JDBC-JobStores)
- Configuration of Database Clustering (achieve fail-over and load-balancing with JDBC-JobStore)
- Configuration of TerracottaJobStore (Clustering without a database!)
JobDataMap 任務數據對象
1、創建 JobDetail 時,將要執行的 job 的類名傳給了 JobDetail,所以 scheduler 就知道了要執行何種類型的 job;每次當scheduler 執行 job 的 execute(…) 方法之前會創建該類的一個新的實例;執行完畢,對該 Job 實例的引用就被丟棄了,實例會被垃圾回收;
2、Job 實現類中必須有一個無參的構造函數(當使用默認的JobFactory時),Job 實現類中,不應該定義有狀態的數據屬性,因爲在 job 的多次執行中,這些屬性的值不會保留。應該使用 JobDataMap 給 Job 實例設置屬性,用於在多次執行中跟蹤 Job 的狀態。
3、JobDetail、Trigger 實現類中都定義 JobDataMap 成員變量及其 getter、setter 方法,可以用來設置參數信息,Job 執行 execute() 方法的時候,JobExecutionContext 可以獲取到 JobDataMap 中的信息。
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
/**
* @author wangmaoxiong
*/
public class HiJobTest {
public static void main(String[] args) {
try {
//1)讀取 classpath 下的 quartz.properties(不存在就都使用默認值)配置來實例化 Scheduler
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
//2)創建任務詳情。設置任務名稱與所屬組名,同組內的任務名稱必須唯一。
// 爲任務添加數據,可以直接 usingJobData,也可以先 jobDetail.getJobDataMap(),然後 put
JobDetail jobDetail = JobBuilder.newJob(HiJob.class)
.withIdentity("hiJob", "hiJobGroup")
.usingJobData("url", "https://wangmaoxiong.blog.csdn.net/article/details/105057405")
.build();
//3)設置觸發器,設置觸發器名稱與所屬組名,同組內的觸發器名稱必須唯一。
// 爲觸發器添加數據,可以直接 usingJobData,也可以先 jobDetail.getJobDataMap(),然後 put
// startNow() 表示啓動後立即執行
// withSchedule(ScheduleBuilder<SBT> scheduleBuilder):設置觸發器調度計劃,withIntervalInSeconds:間隔多少秒執行
// repeatForever:表示用於重複。
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("hiTrigger", "hiTriGroup")
.startNow()
.usingJobData("totalCount", 3)
.withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(5).repeatForever())
.build();
//4)註冊任務詳情與觸發器,然後啓動
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
/**
* @author wangmaoxiong
* quartz 任務/作業
*/
public class HiJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
//getJobDetail() 返回 JobDetail, getTrigger() 返回 Trigger,然後都可以獲取自己的 JobDataMap 進行取值或者設值
//context.getMergedJobDataMap() 返回的 JobDataMap 包含了 JobDetail 與 getTrigger 設置的所有屬性
JobDataMap jobDetailDataMap = context.getJobDetail().getJobDataMap();
JobDataMap triggerDataMap = context.getTrigger().getJobDataMap();
String url = jobDetailDataMap.getString("url");
int totalCount = triggerDataMap.getInt("totalCount");
}
}
1、context.getMergedJobDataMap() 返回的 JobDataMap 是 JobDetail 中的 JobDataMap 和 Trigger 中的 JobDataMap 的並集,但是如果存在相同的數據,則後者會覆蓋前者的值。
2、如果在 Job 類中爲 JobDataMap 中存儲數據的 key 增加 setter 方法,那麼 Quartz 的默認 JobFactory 實現會在 job 被實例化的時候自動調用這些 setter 方法進行注值,這樣就不需要在 execute() 方法中顯式地從 JobDataMap 中取數據了。
示例源碼:https://github.com/wangmaoxiong/quartzapp/tree/master/src/test/java/com/wmx/quartzapp/helloworld
Job/JobDetail 實例 與 併發
1、一個 Job 實現類可以與多個 JobDetail 實例關聯(JobDetal 與 Trigger 一對多),每一個 JobDetail 實例都有自己的屬性集和 JobDataMap,最後將所有的實例都註冊到 scheduler 中。如 "YearEndSettlementJob" 類實現 Job 接口,該 job 實現需要 JobdataMap 傳入一個參數,表示年終結算的季度(1-4)。因此可以創建該 job 的多個實例(JobDetail),如 "jobDetail1"、"jobDetail2"、"jobDetail3"、"jobDetail4", 然後將季度"1,2,3,4"作爲 JobDataMap 的屬性傳入。當一個 trigger 被觸發時,與之關聯的 JobDetail 實例會被加載,JobDetail 引用的 job 類通過配置在 Scheduler 上的 JobFactory 進行初始化,然後嘗試調用 JobDataMap 中的 key 的 setter 方法注入屬性值。、
2、Job 的狀態數據(JobDataMap)和併發性會被下面兩個註解所影響:
@DisallowConcurrentExecution |
添加到 Job 的實現類上,表示不要併發地執行 Job 實現的同一個實例(JobDetail)。假如上面的“YearEndSettlementJob”類上有該註解,則同一時刻僅允許執行一個“jobDetail1”實例,但可以併發地執行“jobDetail2”、"jobDetail3"、"jobDetail4" 實例。所以該併發限制實際是針對JobDetail 的,而不是 job 的。 |
@PersistJobDataAfterExecution |
添加到 Job 實現類上,表示調度器在成功執行了 job 類的 execute 方法後(沒有發生任何異常),更新 JobDetail 中 JobDataMap 的數據,使得該 JobDetail 在下一次執行的時候JobDataMap 中是更新後的數據,而不是更新前的舊數據。 和 @DisallowConcurrentExecution 註解一樣,儘管註解是加在 job 類上,但其限制作用是針對JobDetil 的,而不是 job 類。 |
3、生產環境如果任務執行存在併發問題,則強烈建議加上 @PersistJobDataAfterExecution 、@DisallowConcurrentExecution 註解,因爲當同一個 JobDetail 實例被併發執行時,由於競爭,JobDataMap 中存儲的數據很可能是不確定的。
4、當任務的執行時間過長,而觸發的時間間隔小於執行時間,則會導致同一個 JobDetil 實例被併發執行。所以如果想要驗證註解非常簡單,只要將執行間隔縮小,然後 Job 實現類的 execute 方法中使用 Thread.sleep() 硬性延遲。查看任務執行情況,然後加上上面的註解才查看。源碼如下:
5、通過 JobDetail 對象還可以給 job 實例配置的以下屬性:
Durability | 指示 job 是否是持久性的。如果 job 是非持久的,當沒有活躍的 trigger 與之關聯時,就會被自動地從 scheduler 中刪除。即非持久的 job 的生命期是由 trigger 的存在與否決定的. |
RequestsRecovery | 指示 job 遇到故障重啓後,是否是可恢復的。如果 job 是可恢復的,在其執行的時候,如果 scheduler 發生硬關閉(hard shutdown)(比如運行的進程崩潰了,或者關機了),則當 scheduler 重啓時,該 job 會被重新執行。此時該 job 的JobExecutionContext.isRecovering() 返回true。 |
//2)創建任務詳情。設置任務名稱與所屬組名,同組內的任務名稱必須唯一。
// 爲任務添加數據,可以直接 usingJobData,也可以先 jobDetail.getJobDataMap(),然後 put
JobDetail jobDetail = JobBuilder.newJob(HiJob.class)
.withIdentity("hiJob", "hiJobGroup")
.usingJobData("url", "https://wangmaoxiong.blog.csdn.net/article/details/105057405")
.requestRecovery(true) //設置 job 遇到故障重啓後,是否是可恢復的,默認爲 false.
.storeDurably(true) //設置 job 是否是持久性的,默認爲 false.
.build();
MVC 訪問調度 Quartz 任務
1、爲了學習 Quartz 方便,所以使用了 mian 運行,其實對於 web 應用操作步驟完全同理,如下源碼所示:
https://github.com/wangmaoxiong/quartzapp/tree/master/src/main/java/com/wmx/quartzapp
2、其中 WeatherController 控制層提供訪問接口,方法中使用 Quartz 調度器調度 WeatherRequestJob 作業,用於定時請求天氣信息接口,獲取城市的天氣數據。
3、WeatherRequestJob 任務中使用 BeanFactoryAware 的方式獲取 Spring 容器中的 RestTemplate 實例,然後請求 http 地址.
4、BeanConfig 類是 @Configuration 配置類,使用 @Bean 將 RestTemplate 實例交由 Spring 容器管理.
5、控制層接口返回的數據使用 ResultData 對象進行封裝,包含 code、message、data 三部分,同時提供 ResultCode 枚舉提供常用的返回值類型。
本文介紹的是內存存儲,即所有的調度信息全部存儲在內存中,如果需要使用 jdbc 存儲到數據庫中,則可以參考《Spring Boot 2.1.3 集成 Quartz 定時器, jdbc 持久化調度信息》