Quartz源碼分析(上)

1.一個想法

最近項目中用到了Quartz框架,作爲定時任務的調度框架,無論是和Spring的完美融合還是直接使用Java代碼來進行使用,可以說都非常的簡單且穩定,某天突然想到如果沒有Quartz框架,我們應該如何實現一個純Java代碼的定時任務調度框架呢?

  • 想法一:我立刻有了一個設計思路,需要一條單獨的調度線程,來不斷地輪詢當前時間點和任務時間點,到達任務出發時間就開啓一個線程,把需要交給的任務提交給那個線程。但是這樣做可能會導致線程數量過多,不斷輪旋線程一直進行循環佔用CPU資源也是不怎麼合理的設計。
  • 想法二:基於第一版的思路,單獨的調度線程再等待下一次觸發任務的時候進入一個休眠,線程休眠時間會根據下次觸發的時間差來確定,任務提交到線程池,可以避免出現任務過多導致的線程暴增的現象。

於是我就忍不住去看了Quartz的源碼(多撈啊…),然後發現Quartz的核心設計思路竟然和我第二版的想法差不多,那麼廢話不多數,亮出源碼吧!

2.Quartz總體架構

先偷張圖:
在這裏插入圖片描述
Quartz框架的三件套:

  1. Job:需要執行的任務。
  2. Trigger:觸發器,可以指定特定的時間觸發Job
  3. 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對象。

JobDetailImplJobDetail接口的默認實現類,我們可以看其中一些屬性。
在這裏插入圖片描述
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是建立JobDetailTrigger關聯的關鍵類,同時也存儲JobDetailTigger的類。Quartz內部有兩個實現類:

  • 存儲在內存中,對應類RAMJobStore,這個存儲類的特性是項目重啓以後會清空數據。
  • 存儲到數據庫中,使用到了JDBC接口,對應JobStoreTX類和JobStoreCMT類,這兩個類分別對應Quartz單機持久化部署和集羣持久化部署的數據庫操作實現。

這裏我們分析RAMJobStore,這個類我們不需要手動創建,如果配置文件中不啓用數據庫配置的話,Quartz默認使用這個類。

在Quartz框架中,我們會使用scheduleJob方法提交JobTrigger,實現JobTrigger關聯的關鍵步驟有兩個

  1. Trigger實例設置JobKey,這裏需要注意Quartz框架中每個Job都會有一個唯一的JobKey來標識
  2. 存儲JobDetialTriggerJobStore中,Quartz中有兩種方式存儲,一種時存儲到內存中,一種是使用JDBC存儲到數據庫中。

在這裏插入圖片描述
下面是RAMJobStore類的核心方法,storeJobAndTrigger源碼:

public void storeJobAndTrigger(JobDetail newJob,
        OperableTrigger newTrigger) throws JobPersistenceException {
    storeJob(newJob, false);
    storeTrigger(newTrigger, false);
}

簡單明瞭,先存Job再存Trigger,當然其實storeJobstoreTrigger方法纔是真正的實現了存儲對象方法。

下面問題來了, 如果讓你設計一種存儲JobTrigger在內存中的結構,同時讓他具有關聯性,你會選擇Java中哪種容器實現?讓我想起來大二我們課程設計實現一個SQL型數據庫,我硬生生用幾層嵌套HashMap創造了一種存儲結構。當然這裏RAMJobStore中就是採用了嵌套HashMap來實現JobTrigger的映射關係。

// 核心存儲容器
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的核心工作代碼,只是瞭解了JobTrigger的創建,包裝和存儲,當然這也是非常重要的,俗話說巧婦難爲無米之炊,JobTrigger作爲原材料也是Quartz必不可少的一環,下篇我們會去分析Scheduler源碼,也就是Quartz的核心代碼,其中涉及到的線程模型,設計思想,會進行一些分析。

關於我們的一個想法,Quartz是如何將它照進現實的,還請看下一篇。


參考資料:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章