1.一個想法
最近項目中用到了Quartz框架,作爲定時任務的調度框架,無論是和Spring的完美融合還是直接使用Java代碼來進行使用,可以說都非常的簡單且穩定,某天突然想到如果沒有Quartz框架,我們應該如何實現一個純Java代碼的定時任務調度框架呢?
- 想法一:我立刻有了一個設計思路,需要一條單獨的調度線程,來不斷地輪詢當前時間點和任務時間點,到達任務出發時間就開啓一個線程,把需要交給的任務提交給那個線程。但是這樣做可能會導致線程數量過多,不斷輪旋線程一直進行循環佔用CPU資源也是不怎麼合理的設計。
- 想法二:基於第一版的思路,單獨的調度線程再等待下一次觸發任務的時候進入一個休眠,線程休眠時間會根據下次觸發的時間差來確定,任務提交到線程池,可以避免出現任務過多導致的線程暴增的現象。
於是我就忍不住去看了Quartz的源碼(多撈啊…),然後發現Quartz的核心設計思路竟然和我第二版的想法差不多,那麼廢話不多數,亮出源碼吧!
2.Quartz總體架構
先偷張圖:
Quartz框架的三件套:
- Job:需要執行的任務。
- Trigger:觸發器,可以指定特定的時間觸發Job
- Scheduler(核心):統一調度器,Job和Trigger作爲參數傳入,調度器完成任務的觸發和時間調度
可以說Quartz從任務的提交,到任務的定時執行,就是依靠這三件套來完成的,當然這裏我假設你已經掌握了Quartz的基本用法,下面我們對這些組件一一分析。
2.1 Job和JobDetail
我們知道,在想要在Quartz中提交一個任務,我們需要有一個自己實現Job
接口的類,只需要將定時觸發的代碼覆寫在其execute
方法中。
// 實現Job接口的類
public class MyJob implements Job {
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("this is a test job!");
}
}
使用Quartz的時候,我們通過實現Job接口來完成我們需要的業務,execute
方法是需要Job
接口需要實現的唯一方法。
實際上,Quartz內部需要將Job包裝的更爲複雜,就像Spring
會在內部包裝生成Bean
一樣,Job
最終會被包裝成爲一個JobDetailImpl
對象。
JobDetailImpl
是JobDetail
接口的默認實現類,我們可以看其中一些屬性。
JobDetailImpl
對象是給Quartz框架使用的,就像SpringBean
對象是給Spring框架使用的,額外在Job
基礎上進行封裝。
關於JobDetail
對象的生成,Quartz使用了建造者模式:
可以看到JobBuilder
類的結構,很明顯這是一個Builder
類。
public JobDetail build() {
JobDetailImpl job = new JobDetailImpl();
job.setJobClass(jobClass);
job.setDescription(description);
if(key == null)
key = new JobKey(Key.createUniqueName(null), null);
job.setKey(key);
job.setDurability(durability);
job.setRequestsRecovery(shouldRecover);
if(!jobDataMap.isEmpty())
job.setJobDataMap(jobDataMap);
return job;
}
**核心方法如上,Builder模式在安卓端比較常用,這裏的好處就是,可以比較方便的生成一個JobDetail
對象,交給Scheduler
使用。**ps:關於建造者模式-菜鳥教程
2.2 Trigger
Tigger
在Quartz作爲一個觸發器的角色,我們可以通過Trigger
對象來實現任務調度的頻率設置,時間設置等。
Trigger
在Quartz中有兩種常用的使用手段,一種是基於SimpleTrigger
接口實現的簡單觸發器,這種觸發器規則比較簡單,一般都是間隔特定時間執行,還有一個字是基於CronTrigger
接口實現的Cron表達式觸發器,Cron表達式能實現更爲強大的觸發規則,用過Quartz的小夥伴一定會接觸到Cron表達式(使用Linux肯定也接觸過)。這裏我們看更爲簡單的SimpleTrigger
Trigger
實例和JobDetail
實例一樣,都是採用了Builder模式創造,真正的實現類是SimpleTriggerImpl
類。
我們先來看一下SimpleTrigger
類的繼承關係圖:
其實關於Trigger作用的說明,我想Quartz自帶的JavaDoc應該比我表述的清楚:
/**
* A <code>{@link Trigger}</code> that is used to fire a <code>Job</code>
* at a given moment in time, and optionally repeated at a specified interval.
*
* `SimpleTrigger`類開頭的這段註釋很好的說明了`SimpleTrigger`的作用,fire這裏意爲執行。
* /
SimpleTriggerImpl
類裏面有很多屬性,包括Trigger名,屬組,開始時間,執行次數,執行間隔等等很多屬性,和JobDetailImpl
類一樣,這些屬性都是給Quartz框架用的。
這裏還要特別說明一個和Trigger
緊密相關的類,就是ScheduleBuilder
類,很明顯這也是一個Builder模式的類,其實就是爲了生成一個Trigger
使用的時間表,然後作爲參數傳入TriggerBuilder
類中,這裏註釋說的也很明白,ScheduleBuilder
會被用於定義Trigger的觸發時間。(當然CronTrigger
又是另一套轉換邏輯,需要把corn表達式轉換成時間表)
2.3 JobStore
下面進入一個經典問題:我現在有了咖啡,有了牛奶,請問我怎麼得到咖啡牛奶?
好吧只需要一個杯子。
就像現在我們有了Job
有了Trigger
,怎麼把他們建立對應關係並儲存起來呢?答案就是JobStore
類,JobStore
是建立JobDetail
和Trigger
關聯的關鍵類,同時也存儲JobDetail
和Tigger
的類。Quartz內部有兩個實現類:
- 存儲在內存中,對應類
RAMJobStore
,這個存儲類的特性是項目重啓以後會清空數據。 - 存儲到數據庫中,使用到了JDBC接口,對應
JobStoreTX
類和JobStoreCMT
類,這兩個類分別對應Quartz單機持久化部署和集羣持久化部署的數據庫操作實現。
這裏我們分析RAMJobStore
,這個類我們不需要手動創建,如果配置文件中不啓用數據庫配置的話,Quartz默認使用這個類。
在Quartz框架中,我們會使用scheduleJob
方法提交Job
和Trigger
,實現Job
和Trigger
關聯的關鍵步驟有兩個
Trigger
實例設置JobKey
,這裏需要注意Quartz框架中每個Job
都會有一個唯一的JobKey
來標識- 存儲
JobDetial
和Trigger
到JobStore
中,Quartz中有兩種方式存儲,一種時存儲到內存中,一種是使用JDBC
存儲到數據庫中。
下面是RAMJobStore
類的核心方法,storeJobAndTrigger
源碼:
public void storeJobAndTrigger(JobDetail newJob,
OperableTrigger newTrigger) throws JobPersistenceException {
storeJob(newJob, false);
storeTrigger(newTrigger, false);
}
簡單明瞭,先存Job
再存Trigger
,當然其實storeJob
和storeTrigger
方法纔是真正的實現了存儲對象方法。
下面問題來了, 如果讓你設計一種存儲Job
和Trigger
在內存中的結構,同時讓他具有關聯性,你會選擇Java中哪種容器實現?讓我想起來大二我們課程設計實現一個SQL型數據庫,我硬生生用幾層嵌套HashMap
創造了一種存儲結構。當然這裏RAMJobStore
中就是採用了嵌套HashMap
來實現Job
和Trigger
的映射關係。
// 核心存儲容器
protected Map<JobKey, List<TriggerWrapper>> triggersByJob =
new HashMap<JobKey, List<TriggerWrapper>>(1000);
沒錯,就是你想象的這麼簡單。JobKey
之前我們已經說過了,是Quartz框架中全局唯一的(即使分佈式部署Quartz也是全局唯一的,用到了UUID
類),List<TriggerWrapper>
是Trigger
的列表,也就是說一個Job
可以對應多個Trigger
觸發器。
我們再來看一下存儲方法:
public void storeTrigger(OperableTrigger newTrigger,
boolean replaceExisting) throws JobPersistenceException {
TriggerWrapper tw = new TriggerWrapper((OperableTrigger)newTrigger.clone());
synchronized (lock) {
// …… 省略部分代碼
// 核心功能代碼:add to triggers by job
List<TriggerWrapper> jobList = triggersByJob.get(tw.jobKey);
if(jobList == null) {
jobList = new ArrayList<TriggerWrapper>(1);
triggersByJob.put(tw.jobKey, jobList);
}
jobList.add(tw);
// ……省略部分代碼
}
}
synchronized
關鍵字在這裏是確保多線程環境下的數據安全,其實真的很簡單,就是單純的去存儲數據,不過JobStore
在我們之後要講到的Scheduler
核心代碼裏扮演着很重要的角色。
3.上篇小結
到現在爲止,我們還沒有分析到Quartz的核心工作代碼,只是瞭解了Job
和Trigger
的創建,包裝和存儲,當然這也是非常重要的,俗話說巧婦難爲無米之炊,Job
和Trigger
作爲原材料也是Quartz必不可少的一環,下篇我們會去分析Scheduler
源碼,也就是Quartz的核心代碼,其中涉及到的線程模型,設計思想,會進行一些分析。
關於我們的一個想法,Quartz是如何將它照進現實的,還請看下一篇。
參考資料:
- https://blog.csdn.net/qq_29166327/article/details/80502310 – Quartz使用介紹
- http://www.runoob.com/design-pattern/builder-pattern.html – Builder模式介紹
- https://www.jianshu.com/p/f466e02e2e94 – 這個大佬也是Quartz的源碼分析,質量很高很詳細,早知道就一開始就看他寫的了,慚愧。