1. 程式人生 > >Quartz框架入門學習(一)

Quartz框架入門學習(一)

一.Quartz的簡單使用

Scheduler在使用之前需要例項化。一般通過SchedulerFactory來建立一個例項。有些使用者將factory的例項儲存在JNDI中,但直接初始化,然後使用該例項也許更簡單(見下面的示例)。
scheduler例項化後,可以啟動(start)、暫停(stand-by)、停止(shutdown)。注意:scheduler被停止後,除非重新例項化,否則不能重新啟動;只有當scheduler啟動後,即使處於暫停狀態也不行,trigger才會被觸發(job才會被執行)。

  SchedulerFactory schedFact = new org.quartz
.impl.StdSchedulerFactory(); Scheduler sched = schedFact.getScheduler(); sched.start(); // define the job and tie it to our HelloJob class JobDetail job = newJob(HelloJob.class) .withIdentity("myJob", "group1") .build(); // Trigger the job to run now, and then every 40 seconds Trigger trigger = newTrigger() .withIdentity
("myTrigger", "group1") .startNow() .withSchedule(simpleSchedule() .withIntervalInSeconds(40) .repeatForever()) .build(); // Tell quartz to schedule the job using our trigger sched.scheduleJob(job, trigger);

二.Quartz的API,Job,Trigger

API

Quartz API核心介面有:
.scheduler->與scheduler互動的主要API
job:通過scheduler執行任務,你的任務類需要實現的介面。
JobDetail:定義Job的例項。
Trigger:觸發Job的執行。
JobBuilder:定義和建立JobDetail例項的介面。
TriggerBuilder:定義和建立Trigger例項的介面

Scheduler的生命週期,從SchedulerFactory建立它時開始,到Scheduler呼叫shutdown()方法時結束,Scheduler被建立後,可以增加,刪除,和列舉Job和Trigger,以及執行其他與排程相關的操作(如暫停Trigger),但是,Scheduler只有在呼叫start()方法後,才會真正地觸發trigger(即執行job)。
Quartz提供的“builder”類,可以認為是一種領域特定語言(DSL,Domain Specific Language)。教程一中有相關示例,這裡是其中的程式碼片段:(校對注:這種級聯的API非常方便使用者使用,大家以後寫對外介面時也可以使用這種方式)

   // define the job and tie it to our HelloJob class
    JobDetail job = newJob(HelloJob.class)
      .withIdentity("myJob", "group1") // name "myJob", group "group1"
      .build();

    // Trigger the job to run now, and then every 40 seconds
    Trigger trigger = newTrigger()
      .withIdentity("myTrigger", "group1")
      .startNow()
      .withSchedule(simpleSchedule()
          .withIntervalInSeconds(40)
          .repeatForever())            
      .build();

    // Tell quartz to schedule the job using our trigger
    sched.scheduleJob(job, trigger);

Job,Trigger

一個job就是一個實現了Job介面的類,該介面只有一個方法:
Job介面:

    package org.quartz;

    public interface Job {

    public void execute(JobExecutionContext context)
      throws JobExecutionException;
    }

當job的一個trigger被觸發後(稍後會講到),execute()方法會被scheduler的一個工作執行緒呼叫;傳遞給execute()方法的JobExecutionContext物件中儲存著該job執行時的一些資訊 ,執行job的scheduler的引用,觸發job的trigger的引用,JobDetail物件引用,以及一些其它資訊。

JobDetail物件是在將job加入scheduler時,由客戶端程式(你的程式)建立的。它包含job的各種屬性設定,以及用於儲存job例項狀態資訊的JobDataMap。本節是對job例項的簡單介紹,更多的細節將在下一節講到。

Trigger用於觸發Job的執行。當你準備排程一個job時,你建立一個Trigger的例項,然後設定排程相關的屬性。Trigger也有一個相關聯的JobDataMap,用於給Job傳遞一些觸發相關的引數。Quartz自帶了各種不同型別的Trigger,最常用的主要是SimpleTrigger和CronTrigger。

SimpleTrigger主要用於一次性執行的Job(只在某個特定的時間點執行一次),或者Job在特定的時間點執行,重複執行N次,每次執行間隔T個時間單位。CronTrigger在基於日曆的排程上非常有用,如“每個星期五的正午”,或者“每月的第十天的上午10:15”等。

為什麼既有Job,又有Trigger呢?很多工排程器並不區分Job和Trigger。有些排程器只是簡單地通過一個執行時間和一些job識別符號來定義一個Job;其它的一些排程器將Quartz的Job和Trigger物件合二為一。在開發Quartz的時候,我們認為將排程和要排程的任務分離是合理的。在我們看來,這可以帶來很多好處。

例如,Job被建立後,可以儲存在Scheduler中,與Trigger是獨立的,同一個Job可以有多個Trigger;這種鬆耦合的另一個好處是,當與Scheduler中的Job關聯的trigger都過期時,可以配置Job稍後被重新排程,而不用重新定義Job;還有,可以修改或者替換Trigger,而不用重新定義與之關聯的Job。

key

將Job和Trigger註冊到Scheduler時,可以為它們設定key,配置其身份屬性。Job和Trigger的key(JobKey和TriggerKey)可以用於將Job和Trigger放到不同的分組(group)裡,然後基於分組進行操作。同一個分組下的Job或Trigger的名稱必須唯一,即一個Job或Trigger的key由名稱(name)和分組(group)組成。

三.Quartz的Job和JobDetail的詳細介紹

你定義了一個實現Job介面的類,這個類僅僅表明該job需要完成什麼型別的任務,除此之外,Quartz還需要知道該Job例項所包含的屬性;這將由JobDetail類來完成。
JobDetail例項是通過JobBuilder類建立的

    // define the job and tie it to our HelloJob class
    JobDetail job = newJob(HelloJob.class)
      .withIdentity("myJob", "group1") // name "myJob", group "group1"
      .build();

    // Trigger the job to run now, and then every 40 seconds
    Trigger trigger = newTrigger()
      .withIdentity("myTrigger", "group1")
      .startNow()
      .withSchedule(simpleSchedule()
          .withIntervalInSeconds(40)
          .repeatForever())            
      .build();

    // Tell quartz to schedule the job using our trigger
    sched.scheduleJob(job, trigger);

“HelloJob”類可以如下定義:

    public class HelloJob implements Job {

        public HelloJob() {
        }

        public void execute(JobExecutionContext context)
          throws JobExecutionException
        {
          System.err.println("Hello!  HelloJob is executing.");
        }
    }

可以看到,我們傳給scheduler一個JobDetail例項,因為我們在建立JobDetail時,將要執行的job的類名傳給了JobDetail,所以scheduler就知道了要執行何種型別的job;每次當scheduler執行job時,在呼叫其execute(…)方法之前會建立該類的一個新的例項;執行完畢,對該例項的引用就被丟棄了,例項會被垃圾回收;這種執行策略帶來的一個後果是,job必須有一個無參的建構函式(當使用預設的JobFactory時);另一個後果是,在job類中,不應該定義有狀態的資料屬性,因為在job的多次執行中,這些屬性的值不會保留。
那麼如何給job例項增加屬性或配置呢?如何在job的多次執行中,跟蹤job的狀態呢?答案就是:JobDataMap,JobDetail物件的一部分。

JobDataMap

JobDataMap中可以包含不限量的(序列化的)資料物件,在job例項執行的時候,可以使用其中的資料;JobDataMap是Java Map介面的一個實現,額外增加了一些便於存取基本型別的資料的方法。
將job加入到scheduler之前,在構建JobDetail時,可以將資料放入JobDataMap,如下示例:

    JobDetail job = newJob(DumbJob.class)
      .withIdentity("myJob", "group1") // name "myJob", group "group1"
      .usingJobData("jobSays", "Hello World!")
      .usingJobData("myFloatValue", 3.141f)
      .build();

在job的執行過程中,可以從JobDataMap中取出資料,如下示例:

   public class DumbJob implements Job {

        public DumbJob() {
        }

        public void execute(JobExecutionContext context)
            throws JobExecutionException
        {
            JobKey key = context.getJobDetail().getKey();

            JobDataMap dataMap = context.getJobDetail().getJobDataMap();

            String jobSays = dataMap.getString("jobSays");
            float myFloatValue = dataMap.getFloat("myFloatValue");

            System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
        }
    }

如果你使用的是持久化的儲存機制(本教程的JobStore部分會講到),在決定JobDataMap中存放什麼資料的時候需要小心,因為JobDataMap中儲存的物件都會被序列化,因此很可能會導致類的版本不一致的問題;Java的標準型別都很安全,如果你已經有了一個類的序列化後的例項,某個時候,別人修改了該類的定義,此時你需要確保對類的修改沒有破壞相容性;更多細節,參考現實中的序列化問題。另外,你也可以配置JDBC-JobStore和JobDataMap,使得map中僅允許儲存基本型別和String型別的資料,這樣可以避免後續的序列化問題。
如果你在job類中,為JobDataMap中儲存的資料的key增加set方法(如在上面示例中,增加setJobSays(String val)方法),那麼Quartz的預設JobFactory實現在job被例項化的時候會自動呼叫這些set方法,這樣你就不需要在execute()方法中顯式地從map中取資料了。

在Job執行時,JobExecutionContext中的JobDataMap為我們提供了很多的便利。它是JobDetail中的JobDataMap和Trigger中的JobDataMap的並集,但是如果存在相同的資料,則後者會覆蓋前者的值。
下面的示例,在job執行時,從JobExecutionContext中獲取合併後的JobDataMap:

    public class DumbJob implements Job {

        public DumbJob() {
        }

        public void execute(JobExecutionContext context)
          throws JobExecutionException
        {
            JobKey key = context.getJobDetail().getKey();

            JobDataMap dataMap = context.getMergedJobDataMap();  // Note the difference from the previous example

            String jobSays = dataMap.getString("jobSays");
            float myFloatValue = dataMap.getFloat("myFloatValue");
            ArrayList state = (ArrayList)dataMap.get("myStateData");
            state.add(new Date());

            System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
        }
    }

如果你希望使用JobFactory實現資料的自動“注入”,則示例程式碼為:

    public class DumbJob implements Job {

        String jobSays;
        float myFloatValue;
        ArrayList state;

        public DumbJob() {
        }

        public void execute(JobExecutionContext context)
          throws JobExecutionException
        {
            JobKey key = context.getJobDetail().getKey();

            JobDataMap dataMap = context.getMergedJobDataMap();  // Note the difference from the previous example

            state.add(new Date());

            System.err.println("Instance " + key + " of DumbJob says: " + jobSays + ", and val is: " + myFloatValue);
        }

        public void setJobSays(String jobSays) {
            this.jobSays = jobSays;
        }

        public void setMyFloatValue(float myFloatValue) {
            myFloatValue = myFloatValue;
        }

        public void setState(ArrayList state) {
            state = state;
        }

    }

你也許發現,整體上看程式碼更多了,但是execute()方法中的程式碼更簡潔了。而且,雖然程式碼更多了,但如果你的IDE可以自動生成setter方法,你就不需要寫程式碼呼叫相應的方法從JobDataMap中獲取資料了,所以你實際需要編寫的程式碼更少了。當前,如何選擇,由你決定。

Job例項

很多使用者對於Job例項到底由什麼構成感到很迷惑。我們在這裡解釋一下,並在接下來的小節介紹job狀態和併發。

你可以只建立一個job類,然後建立多個與該job關聯的JobDetail例項,每一個例項都有自己的屬性集和JobDataMap,最後,將所有的例項都加到scheduler中。

比如,你建立了一個實現Job介面的類“SalesReportJob”。該job需要一個引數(通過JobdataMap傳入),表示負責該銷售報告的銷售員的名字。因此,你可以建立該job的多個例項(JobDetail),比如“SalesReportForJoe”、“SalesReportForMike”,將“joe”和“mike”作為JobDataMap的資料傳給對應的job例項。

當一個trigger被觸發時,與之關聯的JobDetail例項會被載入,JobDetail引用的job類通過配置在Scheduler上的JobFactory進行初始化。預設的JobFactory實現,僅僅是呼叫job類的newInstance()方法,然後嘗試呼叫JobDataMap中的key的setter方法。你也可以建立自己的JobFactory實現,比如讓你的IOC或DI容器可以建立/初始化job例項。

在Quartz的描述語言中,我們將儲存後的JobDetail稱為“job定義”或者“JobDetail例項”,將一個正在執行的job稱為“job例項”或者“job定義的例項”。當我們使用“job”時,一般指代的是job定義,或者JobDetail;當我們提到實現Job介面的類時,通常使用“job類”。

Job狀態與併發

關於job的狀態資料(即JobDataMap)和併發性,還有一些地方需要注意。在job類上可以加入一些註解,這些註解會影響job的狀態和併發性。

@DisallowConcurrentExecution:將該註解加到job類上,告訴Quartz不要併發地執行同一個job定義(這裡指特定的job類)的多個例項。請注意這裡的用詞。拿前一小節的例子來說,如果“SalesReportJob”類上有該註解,則同一時刻僅允許執行一個“SalesReportForJoe”例項,但可以併發地執行“SalesReportForMike”類的一個例項。所以該限制是針對JobDetail的,而不是job類的。但是我們認為(在設計Quartz的時候)應該將該註解放在job類上,因為job類的改變經常會導致其行為發生變化。

@PersistJobDataAfterExecution:將該註解加在job類上,告訴Quartz在成功執行了job類的execute方法後(沒有發生任何異常),更新JobDetail中JobDataMap的資料,使得該job(即JobDetail)在下一次執行的時候,JobDataMap中是更新後的資料,而不是更新前的舊資料。和 @DisallowConcurrentExecution註解一樣,儘管註解是加在job類上的,但其限制作用是針對job例項的,而不是job類的。由job類來承載註解,是因為job類的內容經常會影響其行為狀態(比如,job類的execute方法需要顯式地“理解”其”狀態“)。

如果你使用了@PersistJobDataAfterExecution註解,我們強烈建議你同時使用@DisallowConcurrentExecution註解,因為當同一個job(JobDetail)的兩個例項被併發執行時,由於競爭,JobDataMap中儲存的資料很可能是不確定的。

Job的其它特性

通過JobDetail物件,可以給job例項配置的其它屬性有:

Durability:如果一個job是非持久的,當沒有活躍的trigger與之關聯的時候,會被自動地從scheduler中刪除。也就是說,非持久的job的生命期是由trigger的存在與否決定的;
RequestsRecovery:如果一個job是可恢復的,並且在其執行的時候,scheduler發生硬關閉(hard shutdown)(比如執行的程序崩潰了,或者關機了),則當scheduler重新啟動的時候,該job會被重新執行。此時,該job的JobExecutionContext.isRecovering() 返回true。

JobExecutionException

最後,是關於Job.execute(..)方法的一些額外細節。execute方法中僅允許丟擲一種型別的異常(包括RuntimeExceptions),即JobExecutionException。因此,你應該將execute方法中的所有內容都放到一個”try-catch”塊中。你也應該花點時間看看JobExecutionException的文件,因為你的job可以使用該異常告訴scheduler,你希望如何來處理髮生的異常。