quartz與timer


1、timer與quartz的比較

1)精確度和功能

   Quartz可以通過cron表達式精確到特定時間執行,而TimerTask不能。Quartz擁有TimerTask所有的功能,而TimerTask則沒有。

 

2)任務類的數量

    TimerTask和Quartz每次執行任務時,每次調用的是不是都是同一個任務類對象,還是每次都不一樣?現在做如下實驗,每次執行任務時,將任務類對象本身打印出來。

   Quartz每次執行任務都創建一個新的任務類對象,而TimerTask則每次使用同一個任務類對象。

3)對異常的處理

一個循環執行的任務,如果某一次執行任務時,因爲某些原因拋出異常,則定時器是否還會在下一個執行任務的時間點執行任務嗎?下面通過模擬在任務類中拋出異常,來模擬這種情況,並測試兩種定時器如何處理這種情況。

Quartz的某次執行任務過程中拋出異常,不影響下一次任務的執行,當下一次執行時間到來時,定時器會再次執行任務;而TimerTask則不同,一旦某個任務在執行過程中拋出異常,則整個定時器生命週期就結束,以後永遠不會再執行定時器任務



總結:其實timer實現定時任務是很簡單的,但是在想法開發是很少用到timer,而是用spring的Quartz。我也在網上找到了一些資料,現在總結一下。

1. Java定時器沒有持久化機制。
2. Java定時器的日程管理不夠靈活(只能設置開始時間、重複的間隔,設置特定的日期、時間等)//這點感同身受
3. Java定時器沒有使用線程池(每個Java定時器使用一個線程)//想必在用timer是遇到了吧。

4. Java定時器沒有切實的管理方案,你不得不自己完成存儲、組織、恢復任務的措施


2、各種定時器的實現(timer、quartz的程序直接啓動與web監聽兩種方式)

1定時器的作用
在實際的開發中,如果項目中需要定時執行或者需要重複執行一定的工作,定時器顯現的尤爲重要。
當然如果我們不瞭解定時器就會用線程去實現,例如:
package org.lzstone.action
public class FinanceAction extends Thread{
       private Date date;
       public void run{
       try{
       while(true){
       Thread.sleep((int)(Math.random()*1000));
       date = new Date();
       //定時執行任務
       }
       }catch(Exception e){
        e.printStackTrace();
       }
}
}
自己實現定時器的工作很複雜,如果實現不好佔用內存過多,系統就此Over,所以處理定時執行或者重複執行的任務,定時器是很好的選擇
2.java中常見的定時器
1)藉助Java.util.Timer來實現
2)OpenSymphony社區提供的Quartz來實現
3.介紹Timer
利用Timer開發定時任務是主要分爲兩個步驟:
1)創建定時任務類
示例代碼:
package org.lzstone.action
import java.util.TimeTask
public class LzstoneTimeTask extends TimeTask{
       public void run(){
              //執行的定時器任務
       }
}
2)運行定時任務,運行定時任務分爲兩種方式:
2.1)程序直接啓動
示例代碼:
package org.lzstone.action
public class LzstoneMain{
       .......
       public void run(){
        //執行定時器的任務
        //創建實例
        Timer timer = new Timer();
        參數:
        new LzstoneTimeTask()- 所要安排的任務。
        0- 執行任務前的延遲時間,單位是毫秒。
        1*1000- 執行各後續任務之間的時間間隔,單位是毫秒。
        timer.schedule(new LzstoneTimeTask(),0,1*1000);
       }
}
2.2)web監聽方式
示例代碼:
package org.lzstone.action
public class LzstoneMain implements ServletContextListener{
       private Timer timer = null;
       //初始化監聽器,創建實例,執行任務
       public void contextInitialized(ServletContextEvent event){
               timer = new Timer();
               timer.schedule(new LzstoneTimeTask(),0,1*1000);
       }
       //銷燬監聽器,停止執行任務
       public void contextDestroyed(ServletContextEvent event){
              //注意,在此計時器調用的計時器任務的 run 方法內調用此方法,就可以絕對確保正在執行的任務是此計時器所執行的最後一個任務。
              timer.cancel();
        }
}
web.xml配置
<listener>
   <listener-class>
        org.lzstone.action.LzstoneMain
   </listener-class>
</listener>
4. 介紹Quartz
Quartz是OpenSymphony開源組織在Job scheduling領域又一個開源項目,可以用來創建簡單或者複雜的定時任務,利用Quartz開發定時任務的步驟與Timer類

似。

利用Quartz開發定時任務是主要分爲兩個步驟:
1)創建定時任務類
示例代碼:
package org.lzstone.action
public class LzstoneTimeTask implements Job{
       public void execute(JobExecutionContext context) throws JobExecutionException{
              //執行的定時器任務
       }
}
2)運行定時任務,運行定時任務分爲兩種方式:
2.1)程序直接啓動,創建任務調度器及配置相應的任務計劃
示例代碼:
package org.lzstone.action
public class LzstoneMain{
       private static Scheduler sched;
       public static void run() throws Exception{
              //創建LzstoneTimeTask的定時任務
              JobDetail jobDetail = new JobDetail("lzstoneJob",sched.DEFAULT_GROUP,LzstoneTimeTask.class);
              //目標 創建任務計劃
              CronTrigger trigger = new CronTrigger("lzstoneTrigger","lzstone","0 0 12 * * ?");
              //0 0 12 * * ? 代表每天的中午12點觸發
              sched = new org.quartz.impl.StdSchedulerFactory().getScheduler();
              sched.scheduleJob(jobDetail,trigger);
              sched.start();
       }
       //停止
       public static void stop() throws Exception{
              sched.shutdown();
        }
}
//執行
public class Main{
       .............
       public void run(){
            LzstoneMain.run();
       }
       ............
}
2.2)web監聽方式
示例代碼:
package org.lzstone.action
public class LzstoneMainListener implements ServletContextListener{
       private Timer timer = null;
       //初始化監聽器,創建實例,執行任務
       public void contextInitialized(ServletContextEvent event){
               LzstoneMain.run();
       }
       //銷燬監聽器,停止執行任務
       public void contextDestroyed(ServletContextEvent event){
              LzstoneMain.stop();
        }
}
web.xml配置
<listener>
   <listener-class>
        org.lzstone.action.LzstoneMainListener
   </listener-class>
</listener>
5.對比
Timer方式實現定時器,原理簡單,實現方便,在執行簡單的任務比較方便,不足之處是無法確定執行時間,並且依賴性比較強,必須繼承指定的類
Quartz方式實現定時器,方便,清晰指定啓動時間,定時參數比較靈活,容易實現比較複雜的定時任務,不足之處是需要實現特定接口,加載其框架
兩種方式各有優缺點,在特定場合可以根據其特點選擇使用。
6.Spring定時任務
Spring定時任務對Timer與Quartz都提供了支持,並且實現步驟基本一樣
首先配置Spring對Timer的支持
1.1 創建定時任務類
package org.lzstone.action
import java.util.TimeTask
public class LzstoneTimeTask extends TimeTask{
       public void run(){
              //執行的定時器任務
       }
}
1.2 註冊定時任務類,配置任務計劃與任務調度器
    在項目的WEB-INF下面創建TimerConfig.xml文件(一般叫做applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean>
<!--註冊定時執行任務實體-->
<bean id="lzstoneTimeTask" class="org.lzstone.action.LzstoneTimeTask"/>
<!--註冊定時器信息-->
<bean id="taskInfo" class="org.springframework.scheduling.timer.ScheduledTimerTask">
<!--第一次執行任務前需要等待的時間,這裏設置爲3秒-->
<property name="delay">
<value>3000</value>
</property>
<!--設置任務的執行週期 這裏設置爲4秒-->
<property name="period">
  <value>4000</value>
</property>
<!--設置具體執行的任務 這裏設置爲lzstoneTimeTask-->
<property name="timerTask">
<ref local="lzstoneTimeTask"/>
</property>
</bean>
<!--配置定時器任務的調度器-->
<bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean">
<!--註冊定時器列表-->
<property name="scheduledTimerTasks">
    <list>
        <ref local="taskInfo"/>
        ........
    </list>
</property>
</bean>
</beans>
1.3 web項目中的啓動設置
    <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/TimerConfig.xml</param-value>
     </context-param>

     <listener>
         <listener-class>
                  org.springframework.web.context.ContextLoaderListener
         </listener-class>
     </listener>
配置Spring對Quartz的支持
2.1 創建定時任務類
package org.lzstone.action
public class LzstoneQuartzTask{
       public void execute(){
              //執行的定時器任務
       }
}
2.2 註冊定時任務類,配置任務計劃與任務調度器
    在項目的WEB-INF下面創建QuartzConfig.xml文件(一般叫做applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean>
<!--註冊定時執行任務實體-->
<bean id="lzstoneQuartzTask" class="org.lzstone.action.LzstoneQuartzTask"/>
<!--註冊定時器信息-->
<bean id="taskInfo" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<!--指定要執行的定時任務類  這裏是LzstoneQuartzTask-->
<property name="targetObject">
<ref local="lzstoneQuartzTask"/>
</property>
<!--指定定時器任務類要執行的方法名稱 這裏是execute-->
<property name="targetMethod">
<value>execute</value>
</property>
</bean>
<!--配置定時器任務的調度器-->
<bean id="quartzTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<!--聲明要運行的實體-->
<property name="jobDetail">
    <ref local="taskInfo"/>
</property>
<!--設置運行時間-->
<property name="cronExpression">
    <value>0 0 12 * * ?</value>
</property>
</bean>
<!--註冊監聽器-->
<bean id="registerQuartz" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<!--註冊定時器實體 集合-->
<property name="triggers">
    <list>
          <ref local="quartzTrigger"/>
    </list>
</property>
</bean>
</beans>
2.3 web項目中的啓動設置
    <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/QuartzConfig.xml</param-value>
     </context-param>

     <listener>
         <listener-class>
                  org.springframework.web.context.ContextLoaderListener
         </listener-class>
     </listener>


4、quartz原理

概述

各種企業應用幾乎都會碰到任務調度的需求,就拿論壇來說:每隔半個小時生成精華文章的RSS文件,每天凌晨統計論壇用戶的積分排名,每隔30分鐘執行鎖定用戶解鎖任務。

對於一個典型的MIS系統來說,在每月1號凌晨統計上個月各部門的業務數據生成月報表,每半個小時查詢用戶是否已經有快到期的待處理業務……,這樣的例子俯拾皆是,不勝枚舉。

任務調度本身涉及到多線程併發、運行時間規則制定和解析、場景保持與恢復、線程池維護等諸多方面的工作。如果直接使用自定義線程這種刀耕火種的原始辦法,開發任務調度程序是一項頗具挑戰性的工作。Java開源的好處就是:領域問題都能找到現成的解決方案。

OpenSymphony所提供的Quartz2001年發佈版本以來已經被衆多項目作爲任務調度的解決方案,Quartz在提供巨大靈活性的同時並未犧牲其簡單性,它所提供的強大功能使你可以應付絕大多數的調度需求。

Quartz 在開源任務調度框架中的翹首,它提供了強大任務調度機制,難能可貴的是它同時保持了使用的簡單性。Quartz 允許開發人員靈活地定義觸發器的調度時間表,並可以對觸發器和任務進行關聯映射。

此外,Quartz提供了調度運行環境的持久化機制,可以保存並恢復調度現場,即使系統因故障關閉,任務調度現場數據並不會丟失。此外,Quartz還提供了組件式的偵聽器、各種插件、線程池等功能。

瞭解Quartz體系結構

Quartz對任務調度的領域問題進行了高度的抽象,提出了調度器、任務和觸發器這3個核心的概念,並在org.quartz通過接口和類對重要的這些核心概念進行描述:

●Job:是一個接口,只有一個方法void execute(JobExecutionContext context),開發者實現該接口定義運行任務,JobExecutionContext類提供了調度上下文的各種信息。Job運行時的信息保存在JobExecutionContext實例中;

●JobDetailQuartz在每次執行Job時,都重新創建一個Job實例,所以它不直接接受一個Job的實例,相反它接收一個Job實現類,以便運行時通過newInstance()的反射機制實例化Job。因此需要通過一個類來描述Job的實現類及其它相關的靜態信息,如Job名字、描述、關聯監聽器等信息,JobDetail承擔了這一角色。

通過該類的構造函數可以更具體地瞭解它的功用:JobDetail(java.lang.String name, java.lang.String group, java.lang.Class jobClass),該構造函數要求指定Job的實現類,以及任務在Scheduler中的組名和Job名稱;

●Trigger:是一個類,描述觸發Job執行的時間觸發規則。主要有SimpleTriggerCronTrigger這兩個子類。當僅需觸發一次或者以固定時間間隔週期執行,SimpleTrigger是最適合的選擇;而CronTrigger則可以通過Cron表達式定義出各種複雜時間規則的調度方案:如每早晨9:00執行,週一、週三、週五下午5:00執行等;

●Calendarorg.quartz.Calendarjava.util.Calendar不同,它是一些日曆特定時間點的集合(可以簡單地將org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一個日曆時間點,無特殊說明後面的Calendar即指org.quartz.Calendar)。一個Trigger可以和多個Calendar關聯,以便排除或包含某些時間點。

假設,我們安排每週星期一早上10:00執行任務,但是如果碰到法定的節日,任務則不執行,這時就需要在Trigger觸發機制的基礎上使用Calendar進行定點排除。針對不同時間段類型,Quartzorg.quartz.impl.calendar包下提供了若干個Calendar的實現類,如AnnualCalendarMonthlyCalendarWeeklyCalendar分別針對每年、每月和每週進行定義;

●Scheduler:代表一個Quartz的獨立運行容器,TriggerJobDetail可以註冊到Scheduler中,兩者在Scheduler中擁有各自的組及名稱,組及名稱是Scheduler查找定位容器中某一對象的依據,Trigger的組及名稱必須唯一,JobDetail的組和名稱也必須唯一(但可以和Trigger的組和名稱相同,因爲它們是不同類型的)。Scheduler定義了多個接口方法,允許外部通過組及名稱訪問和控制容器中TriggerJobDetail

Scheduler可以將Trigger綁定到某一JobDetail中,這樣當Trigger觸發時,對應的Job就被執行。一個Job可以對應多個Trigger,但一個Trigger只能對應一個Job可以通過SchedulerFactory創建一個Scheduler實例。Scheduler擁有一個SchedulerContext,它類似於ServletContext,保存着Scheduler上下文信息,JobTrigger都可以訪問SchedulerContext內的信息。SchedulerContext內部通過一個Map,以鍵值對的方式維護這些上下文數據,SchedulerContext爲保存和獲取數據提供了多個put()getXxx()的方法。可以通過Scheduler# getContext()獲取對應的SchedulerContext實例;

●ThreadPoolScheduler使用一個線程池作爲任務運行的基礎設施,任務通過共享線程池中的線程提高運行效率。

Job有一個StatefulJob子接口,代表有狀態的任務,該接口是一個沒有方法的標籤接口,其目的是讓Quartz知道任務的類型,以便採用不同的執行方案。無狀態任務在執行時擁有自己的JobDataMap拷貝,對JobDataMap的更改不會影響下次的執行。而有狀態任務共享共享同一個JobDataMap實例,每次任務執行對JobDataMap所做的更改會保存下來,後面的執行可以看到這個更改,也即每次執行任務後都會對後面的執行發生影響。

正因爲這個原因,無狀態的Job可以併發執行,而有狀態的StatefulJob不能併發執行,這意味着如果前次的StatefulJob還沒有執行完畢,下一次的任務將阻塞等待,直到前次任務執行完畢。有狀態任務比無狀態任務需要考慮更多的因素,程序往往擁有更高的複雜度,因此除非必要,應該儘量使用無狀態的Job

如果Quartz使用了數據庫持久化任務調度信息,無狀態的JobDataMap僅會在Scheduler註冊任務時保持一次,而有狀態任務對應的JobDataMap在每次執行任務後都會進行保存。

Trigger自身也可以擁有一個JobDataMap,其關聯的Job可以通過JobExecutionContext#getTrigger().getJobDataMap()獲取Trigger中的JobDataMap(該JobDataMap和JobDetail所擁有的JobDataMap不是同一個哦不管是有狀態還是無狀態的任務,在任務執行期間對TriggerJobDataMap所做的更改都不會進行持久,也即不會對下次的執行產生影響。

Quartz擁有完善的事件和監聽體系,大部分組件都擁有事件,如任務執行前事件、任務執行後事件、觸發器觸發前事件、觸發後事件、調度器開始事件、關閉事件等等,可以註冊相應的監聽器處理感興趣的事件。

1描述了Scheduler的內部組件結構,SchedulerContext提供Scheduler全局可見的上下文信息,每一個任務都對應一個JobDataMap,虛線表達的JobDataMap表示對應有狀態的任務:

Scheduler結構圖

一個Scheduler可以擁有多個Triger組和多個JobDetail組,註冊TriggerJobDetail時,如果不顯式指定所屬的組,Scheduler將放入到默認組中,默認組的組名爲Scheduler.DEFAULT_GROUP。組名和名稱組成了對象的全名,同一類型對象的全名不能相同。

Scheduler本身就是一個容器,它維護着Quartz的各種組件並實施調度的規則。Scheduler還擁有一個線程池,線程池爲任務提供執行線程——這比執行任務時簡單地創建一個新線程要擁有更高的效率,同時通過共享節約資源的佔用。通過線程池組件的支持,對於繁忙度高、壓力大的任務調度,Quartz將可以提供良好的伸縮性。

提示: Quartz完整下載包examples目錄下擁有10多個實例,它們是快速掌握Quartz應用很好的實例。

使用SimpleTrigger

SimpleTrigger擁有多個重載的構造函數,用以在不同場合下構造出對應的實例:

●SimpleTrigger(String name, String group):通過該構造函數指定Trigger所屬組和名稱;

●SimpleTrigger(String name, String group, Date startTime):除指定Trigger所屬組和名稱外,還可以指定觸發的開發時間;

●SimpleTrigger(String name, String group, Date startTime, Date endTime, int repeatCount, long repeatInterval):除指定以上信息外,還可以指定結束時間、重複執行次數、時間間隔等參數;

●SimpleTrigger(String name, String group, String jobName, String jobGroup, Date startTime, Date endTime, int repeatCount, long repeatInterval):這是最複雜的一個構造函數,在指定觸發參數的同時,還通過jobGroupjobName,讓該TriggerScheduler中的某個任務關聯起來。


Cron表達式

Quartz使用類似於Linux下的Cron表達式定義時間規則,Cron表達式由67個由空格分隔的時間字段組成,如表1所示:

 Cron表達式時間字段

位置

時間域名

允許值

允許的特殊字符

1

0-59

, - * /

2

分鐘

0-59

, - * /

3

小時

0-23

, - * /

4

日期

1-31

, - * ? / L W C

5

月份

1-12

, - * /

6

星期

1-7

, - * ? / L C #

7

(可選)

空值1970-2099

, - * /

Cron表達式的時間字段除允許設置數值外,還可使用一些特殊的字符,提供列表、範圍、通配符等功能,細說如下:

星號(*):可用在所有字段中,表示對應時間域的每一個時刻,例如,*在分鐘字段時,表示每分鐘

問號(?):該字符只在日期和星期字段中使用,它通常指定爲無意義的值,相當於點位符;

減號(-):表達一個範圍,如在小時字段中使用“10-12”,則表示從1012點,即10,11,12

逗號(,):表達一個列表值,如在星期字段中使用“MON,WED,FRI”,則表示星期一,星期三和星期五;

斜槓(/)x/y表達一個等步長序列,x爲起始值,y爲增量步長值。如在分鐘字段中使用0/15,則表示爲0,15,3045秒,而5/15在分鐘字段中表示5,20,35,50,你也可以使用*/y,它等同於0/y

●L:該字符只在日期和星期字段中使用,代表“Last”的意思,但它在兩個字段中意思不同。L在日期字段中,表示這個月份的最後一天,如一月的31號,非閏年二月的28號;如果L用在星期中,則表示星期六,等同於7。但是,如果L出現在星期字段裏,而且在前面有一個數值X,則表示這個月的最後X,例如,6L表示該月的最後星期五;

●W:該字符只能出現在日期字段裏,是對前導日期的修飾,表示離該日期最近的工作日。例如15W表示離該月15號最近的工作日,如果該月15號是星期六,則匹配14號星期五;如果15日是星期日,則匹配16號星期一;如果15號是星期二,那結果就是15號星期二。但必須注意關聯的匹配日期不能夠跨月,如你指定1W,如果1號是星期六,結果匹配的是3號星期一,而非上個月最後的那天。W字符串只能指定單一日期,而不能指定日期範圍;

●LW組合:在日期字段可以組合使用LW,它的意思是當月的最後一個工作日;

井號(#):該字符只能在星期字段中使用,表示當月某個工作日。如6#3表示當月的第三個星期五(6表示星期五,#3表示當前的第三個),而4#5表示當月的第五個星期三,假設當月沒有第五個星期三,忽略不觸發;

● C:該字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是計劃所關聯的日期,如果日期沒有被關聯,則相當於日曆中所有日期。例如5C在日期字段中就相當於日曆5日以後的第一天。1C在星期字段中相當於星期日後的第一天。

Cron表達式對特殊字符的大小寫不敏感,對代表星期的縮寫英文大小寫也不敏感。

Cron表示式示例

表示式

說明

"0 0 12 * * ? "

每天12點運行

"0 15 10 ? * *"

每天10:15運行

"0 15 10 * * ?"

每天10:15運行

"0 15 10 * * ? *"

每天10:15運行

"0 15 10 * * ? 2008"

2008年的每天1015運行

"0 * 14 * * ?"

每天14點到15點之間每分鐘運行一次,開始於14:00,結束於14:59

"0 0/5 14 * * ?"

每天14點到15點每5分鐘運行一次,開始於14:00,結束於14:55

"0 0/5 14,18 * * ?"

每天14點到15點每5分鐘運行一次,此外每天18點到19點每5鍾也運行一次。

"0 0-5 14 * * ?"

每天14:00點到14:05,每分鐘運行一次。

"0 10,44 14 ? 3 WED"

3月每週三的14:10分到14:44,每分鐘運行一次。

"0 15 10 ? * MON-FRI"

每週一,二,三,四,五的10:15分運行。

"0 15 10 15 * ?"

每月1510:15分運行。

"0 15 10 L * ?"

每月最後一天10:15分運行。

"0 15 10 ? * 6L"

每月最後一個星期五10:15分運行。

"0 15 10 ? * 6L 2007-2009"

2007,2008,2009年每個月的最後一個星期五的10:15分運行。

"0 15 10 ? * 6#3"

每月第三個星期五的10:15分運行。


代碼示例

  1. package com.lxq.quartz.test;  
  2.   
  3. import java.util.Date;  
  4.   
  5. import org.quartz.Job;  
  6. import org.quartz.JobExecutionContext;  
  7. import org.quartz.JobExecutionException;  
  8.   
  9. public class BaseJob implements Job  
  10. {  
  11.   
  12.     @Override  
  13.     public void execute(JobExecutionContext JEContext) throws JobExecutionException  
  14.     {  
  15.         /*輸出當前job執行的時間*/  
  16.         System.out.println(new Date(System.currentTimeMillis()) + "我在執行");  
  17.         /*輸出當前jobDetail的name,groupName和JobClass*/  
  18.         System.out.println(JEContext.getJobDetail().getName() + "," + JEContext.getJobDetail().getGroup() + ","  
  19.                 + JEContext.getJobDetail().getJobClass());  
  20.         /*輸出當前jobDetail的JobDataMap中的數據*/  
  21.         System.out.println(JEContext.getJobDetail().getJobDataMap().get("type"));  
  22.         System.out.println(JEContext.getJobDetail().getJobDataMap().get("index"));  
  23.         /*輸出當前trigger的JobDataMap中的數據*/  
  24.         System.out.println(JEContext.getTrigger().getJobDataMap().get("type"));  
  25.         System.out.println(JEContext.getTrigger().getJobDataMap().get("index"));  
  26.     }  
  27. }  

  1. package com.lxq.quartz.test;  
  2.   
  3. import java.util.Date;  
  4. import java.util.GregorianCalendar;  
  5.   
  6. import org.quartz.CronTrigger;  
  7. import org.quartz.JobDetail;  
  8. import org.quartz.Scheduler;  
  9. import org.quartz.SchedulerException;  
  10. import org.quartz.SchedulerFactory;  
  11. import org.quartz.SimpleTrigger;  
  12. import org.quartz.TriggerUtils;  
  13. import org.quartz.impl.StdSchedulerFactory;  
  14. import org.quartz.impl.calendar.AnnualCalendar;  
  15.   
  16. public class TestQuartz  
  17. {  
  18.     public static void main(String[] args) throws SchedulerException  
  19.     {  
  20.         TestQuartz testQuartz=new TestQuartz();  
  21.         testQuartz.test3();  
  22.           
  23.     }  
  24.   
  25.     public void test1()  
  26.     {  
  27.         //通過SchedulerFactory來獲取一個調度器  
  28.         SchedulerFactory schedulerFactory = new StdSchedulerFactory();  
  29.         Scheduler scheduler;  
  30.         try  
  31.         {  
  32.             scheduler = schedulerFactory.getScheduler();  
  33.             //引進作業程序public JobDetail(String jobName, String jobGroupName, Class jobClass){}  
  34.             JobDetail jobDetail = new JobDetail("jobDetail-j1""jobGroup-g1", BaseJob.class);  
  35.             /*new一個觸發器public SimpleTrigger(String triggerName, String triggerGroupName){} 
  36.             在構造Trigger實例時,可以考慮使用org.quartz.TriggerUtils工具類,該工具類不但提供了衆多獲取特定時間的方法,還擁有衆多獲取常見Trigger的方法, 
  37.             如makeSecondlyTrigger(String trigName)方法將創建一個每秒執行一次的Trigger,而makeWeeklyTrigger(String trigName, int dayOfWeek, int hour, int minute) 
  38.             將創建一個每星期某一特定時間點執行一次的Trigger。而getEvenMinuteDate(Date date)方法將返回某一時間點一分鐘以後的時間。*/  
  39.             SimpleTrigger simpleTrigger = new SimpleTrigger("simpleTrigger""triggerGroup-tg1");  
  40.             //設置作業啓動時間,馬上啓動  
  41.             long ctime = System.currentTimeMillis();  
  42.             simpleTrigger.setStartTime(new Date(ctime));  
  43.             //設置作業執行間隔   
  44.             simpleTrigger.setRepeatInterval(1000);  
  45.             //設置作業執行次數  
  46.             simpleTrigger.setRepeatCount(10);  
  47.             //設置作業執行優先級默認爲5  
  48.             simpleTrigger.setPriority(10);  
  49.             //作業和觸發器註冊到調度器中,並建立Trigger和JobDetail的關聯  
  50.             scheduler.scheduleJob(jobDetail, simpleTrigger);  
  51.             /*上面的一句話,也可以換成下面的這幾句話 
  52.              * JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class); 
  53.             SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1"); 
  54.             simpleTrigger.setJobGroup("jGroup1");:指定關聯的Job組名 
  55.             simpleTrigger.setJobName("job1_1");指定關聯的Job名稱 
  56.             scheduler.addJob(jobDetail, true);註冊JobDetail 
  57.             scheduler.scheduleJob(simpleTrigger);註冊指定了關聯JobDetail的Trigger*/  
  58.             //啓動調度器  
  59.             scheduler.start();  
  60.         }  
  61.         catch (SchedulerException e)  
  62.         {  
  63.             e.printStackTrace();  
  64.         }  
  65.     }  
  66.   
  67.     public void test2()  
  68.     {  
  69.         try  
  70.         {  
  71.             //通過SchedulerFactory來獲取一個調度器  
  72.             SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();  
  73.               
  74.             Scheduler sched = schedFact.getScheduler();  
  75.             JobDetail jobDetail = new JobDetail("jobDetail-j2""jobGroup-g1", BaseJob.class);  
  76.             jobDetail.getJobDataMap().put("type""JobDetail");  
  77.             jobDetail.getJobDataMap().put("index"1002);  
  78.               
  79.             /*public CronTrigger(){} 
  80.             public CronTrigger(String name, String group){} 
  81.             public CronTrigger(String name, String group, String cronExpression){}*/  
  82.             CronTrigger trigger = new CronTrigger("cronTrigger""triggerGroup-tg2");  
  83.             /*CronTrigger 能夠提供比 SimpleTrigger 更有具體實際意義的調度方案,調度規則基於 Cron 表達式,CronTrigger  
  84.             支持日曆相關的重複時間間隔(比如每月第一個週一執行),而不是簡單的週期時間間隔。*/  
  85.             trigger.getJobDataMap().put("type""trigger");  
  86.             trigger.getJobDataMap().put("index"2002);  
  87.             /* 每半分鐘執行一次  */  
  88.             trigger.setCronExpression("30 * * * * ?");  
  89.             sched.scheduleJob(jobDetail, trigger);  
  90.             sched.start();  
  91.         }  
  92.         catch (Exception e)  
  93.         {  
  94.             e.printStackTrace();  
  95.         }  
  96.     }  
  97.   
  98.     /*在實際任務調度中,我們不可能一成不變地按照某個週期性的調度規則運行任務,必須考慮到實現生活中日曆上特定日期,就象習慣了大男人作風的人在2月14號也會有不同表現一樣。*/  
  99.     public void test3() throws SchedulerException  
  100.     {  
  101.         SchedulerFactory sf = new StdSchedulerFactory();  
  102.         Scheduler scheduler = sf.getScheduler();  
  103.         /*法定節日是以每年爲週期的,所以使用AnnualCalendar*/  
  104.         AnnualCalendar holidays = new AnnualCalendar();  
  105.         /*五一勞動節*/  
  106.         GregorianCalendar laborDay = new GregorianCalendar();  
  107.         laborDay.add(GregorianCalendar.MONTH,5);  
  108.         laborDay.add(GregorianCalendar.DATE,1);  
  109.         holidays.setDayExcluded(laborDay, true); //排除的日期,如果設置爲false則爲包含  
  110.         /*國慶節*/  
  111.         GregorianCalendar nationalDay = new GregorianCalendar();  
  112.         nationalDay.add(GregorianCalendar.MONTH,10);  
  113.         nationalDay.add(GregorianCalendar.DATE,1);  
  114.         holidays.setDayExcluded(nationalDay, true);//排除該日期  
  115.         /*Scheduler#addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers) 
  116.          進行註冊,如果updateTriggers爲true,Scheduler中已引用Calendar的Trigger將得到更新*/  
  117.         scheduler.addCalendar("holidays", holidays, falsefalse);//向Scheduler註冊日曆  
  118.         Date runDate = TriggerUtils.getDateOf(0,01017);//7月1號 上午10點  
  119.         JobDetail job = new JobDetail("job1""group1", BaseJob.class);  
  120.         SimpleTrigger trigger = new SimpleTrigger("trigger1""group1",runDate,null,SimpleTrigger.REPEAT_INDEFINITELY,60L * 60L * 1000L);  
  121.         trigger.setCalendarName("holidays");//讓Trigger應用指定的日曆規則  
  122.         scheduler.scheduleJob(job, trigger);  
  123.         scheduler.start();  
  124.   
  125.     }  
  126.   
  127. }  

任務調度信息存儲

在默認情況下Quartz將任務調度的運行信息保存在內存中,這種方法提供了最佳的性能,因爲內存中數據訪問最快。不足之處是缺乏數據的持久性,當程序路途停止或系統崩潰時,所有運行的信息都會丟失。比如我們希望安排一個執行100次的任務,如果執行到50次時系統崩潰了,系統重啓時任務的執行計數器將從0開始。在大多數實際的應用中,我們往往並不需要保存任務調度的現場數據,因爲很少需要規劃一個指定執行次數的任務。

對於僅執行一次的任務來說,其執行條件信息本身應該是已經持久化的業務數據(如鎖定到期解鎖任務,解鎖的時間應該是業務數據),當執行完成後,條件信息也會相應改變。當然調度現場信息不僅僅是記錄運行次數,還包括調度規則、JobDataMap中的數據等等。

如果確實需要持久化任務調度信息,Quartz允許你通過調整其屬性文件,將這些信息保存到數據庫中。使用數據庫保存任務調度信息後,即使系統崩潰後重新啓動,任務的調度信息將得到恢復。如前面所說的例子,執行50次崩潰後重新運行,計數器將從51開始計數。使用了數據庫保存信息的任務稱爲持久化任務。

通過配置文件調整任務調度信息的保存策略

其實Quartz JAR文件的org.quartz包下就包含了一個quartz.properties屬性配置文件並提供了默認設置。如果需要調整默認配置,可以在類路徑下建立一個新的quartz.properties,它將自動被Quartz加載並覆蓋默認的設置。

先來了解一下Quartz的默認屬性配置文件:

quartz.properties:默認配置

  1. ①集羣的配置,這裏不使用集羣  
  2. org.quartz.scheduler.instanceName = DefaultQuartzScheduler  
  3. org.quartz.scheduler.rmi.export = false  
  4. org.quartz.scheduler.rmi.proxy = false  
  5. org.quartz.scheduler.wrapJobExecutionInUserTransaction = false  
  6. ②配置調度器的線程池  
  7. org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool  
  8. org.quartz.threadPool.threadCount = 10  
  9. org.quartz.threadPool.threadPriority = 5  
  10. org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true  
  11. ③配置任務調度現場數據保存機制  
  12. org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore  

Quartz的屬性配置文件主要包括三方面的信息:

1)集羣信息;

2)調度器線程池;

3)任務調度現場數據的保存。

如果任務數目很大時,可以通過增大線程池的大小得到更好的性能。默認情況下,Quartz採用org.quartz.simpl.RAMJobStore保存任務的現場數據,顧名思義,信息保存在RAM內存中,我們可以通過以下設置將任務調度現場數據保存到數據庫中:

quartz.properties:使用數據庫保存任務調度現場數據

  1. org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX  
  2. org.quartz.jobStore.tablePrefix = QRTZ_①數據表前綴  
  3. org.quartz.jobStore.dataSource = qzDS②數據源名稱  
  4. ③定義數據源的具體屬性  
  5. org.quartz.dataSource.qzDS.driver = oracle.jdbc.driver.OracleDriver  
  6. org.quartz.dataSource.qzDS.URL = jdbc:oracle:thin:@localhost:1521:ora9i  
  7. org.quartz.dataSource.qzDS.user = stamen  
  8. org.quartz.dataSource.qzDS.password = abc  
  9. org.quartz.dataSource.qzDS.maxConnections = 10  

要將任務調度數據保存到數據庫中,就必須使用org.quartz.impl.jdbcjobstore.JobStoreTX代替原來的org.quartz.simpl.RAMJobStore並提供相應的數據庫配置信息。首先①處指定了Quartz數據庫表的前綴,在②處定義了一個數據源,在③處具體定義這個數據源的連接信息。

你必須事先在相應的數據庫中創建Quartz的數據表(共8張),在Quartz的完整發布包的docs/dbTables目錄下擁有對應不同數據庫的SQL腳本。

查詢數據庫中的運行信息

任務的現場保存對於上層的Quartz程序來說是完全透明的,我們在src目錄下編寫一個如上代碼所示的quartz.properties文件後,重新運行如下代碼示例的程序,在數據庫表中將可以看到對應的持久化信息。當調度程序運行過程中途停止後,任務調度的現場數據將記錄在數據表中,在系統重啓時就可以在此基礎上繼續進行任務的調度。

  1. package com.lxq.quartz.test;  
  2.   
  3. import java.util.Date;  
  4. import org.quartz.JobDetail;  
  5. import org.quartz.Scheduler;  
  6. import org.quartz.SchedulerFactory;  
  7. import org.quartz.SimpleTrigger;  
  8. import org.quartz.impl.StdSchedulerFactory;  
  9.   
  10. public class SimpleTriggerRunner  
  11. {  
  12.     public static void main(String args[])  
  13.     {  
  14.         try  
  15.         {  
  16.             JobDetail jobDetail = new JobDetail("job1_1""jGroup1", BaseJob.class);  
  17.             SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1""tgroup1");  
  18.             simpleTrigger.setStartTime(new Date());  
  19.             simpleTrigger.setRepeatInterval(2000);  
  20.             simpleTrigger.setRepeatCount(100);  
  21.             SchedulerFactory schedulerFactory = new StdSchedulerFactory();  
  22.             Scheduler scheduler = schedulerFactory.getScheduler();  
  23.             scheduler.scheduleJob(jobDetail, simpleTrigger);  
  24.             scheduler.start();  
  25.         }  
  26.         catch (Exception e)  
  27.         {  
  28.             e.printStackTrace();  
  29.         }  
  30.   
  31.     }  
  32.   
  33. }  
  1. package com.lxq.quartz.test;  
  2.   
  3. import java.util.Date;  
  4.   
  5. import org.quartz.Scheduler;  
  6. import org.quartz.SchedulerFactory;  
  7. import org.quartz.SimpleTrigger;  
  8. import org.quartz.Trigger;  
  9. import org.quartz.impl.StdSchedulerFactory;  
  10.   
  11. public class JDBCJobStoreRunner  
  12. {  
  13.     public static void main(String args[])  
  14.     {  
  15.         try  
  16.         {  
  17.             SchedulerFactory schedulerFactory = new StdSchedulerFactory();  
  18.             Scheduler scheduler = schedulerFactory.getScheduler();  
  19.             //獲取調度器中所有的觸發器組  
  20.             String[] triggerGroups = scheduler.getTriggerGroupNames();  
  21.             //重新恢復在tgroup1組中,名爲trigger1_1觸發器的運行  
  22.             for (int i = 0; i < triggerGroups.length; i++)  
  23.             {  
  24.                 String[] triggers = scheduler.getTriggerNames(triggerGroups[i]);  
  25.                 for (int j = 0; j < triggers.length; j++)  
  26.                 {  
  27.                     Trigger tg = scheduler.getTrigger(triggers[j], triggerGroups[i]);  
  28.                     if (tg instanceof SimpleTrigger && tg.getFullName().equals("tgroup1.trigger1_1"))//根據名稱判斷  
  29.                     {  
  30.                         /*恢復運行public Date rescheduleJob(String triggerName, String groupName, Trigger newTrigger){}*/  
  31.                         scheduler.rescheduleJob(triggers[j], triggerGroups[i], tg);  
  32.                     }  
  33.                 }  
  34.             }  
  35.             scheduler.start();  
  36.         }  
  37.         catch (Exception e)  
  38.         {  
  39.             e.printStackTrace();  
  40.         }  
  41.     }  
  42. }  


當代碼SimpleTriggerRunner執行到一段時間後非正常退出,我們就可以通過這個JDBCJobStoreRunner根據記錄在數據庫中的現場數據恢復任務的調度。Scheduler中的所有Trigger以及JobDetail的運行信息都會保存在數據庫中,這裏我們僅恢復tgroup1組中名稱爲trigger1_1的觸發器,這可以通過所示的代碼進行過濾,觸發器的採用GROUP.TRIGGER_NAME的全名格式。通過Scheduler#rescheduleJob(String triggerName,String groupName,Trigger newTrigger)即可重新調度關聯某個Trigger的任務。

下面我們來觀察一下不同時期qrtz_simple_triggers表的數據:

1.運行代碼SimpleTriggerRunner一小段時間後退出:


REPEAT_COUNT
表示需要運行的總次數,而TIMES_TRIGGER表示已經運行的次數。

2.運行代碼JDBCJobStoreRunner恢復trigger1_1的觸發器,運行一段時間後退出,這時qrtz_simple_triggers中的數據如下:


首先
Quartz會將原REPEAT_COUNT-TIMES_TRIGGER得到新的REPEAT_COUNT值,並記錄已經運行的次數(重新從0開始計算)。

3.重新啓動JDBCJobStoreRunner運行後,數據又將發生相應的變化:



4
.繼續運行直至完成所有剩餘的次數,再次查詢qrtz_simple_triggers表:


這時,該表中的記錄已經變空。

值得注意的是,如果你使用JDBC保存任務調度數據時,當你運行代碼SimpleTriggerRunner然後退出,當再次希望運行SimpleTriggerRunner時,系統將拋出JobDetail重名的異常:Unable to store Job with name: 'job1_1' and group: 'jGroup1', because one already exists with this identification.

因爲每次調用Scheduler#scheduleJob()時,Quartz都會將JobDetailTrigger的信息保存到數據庫中,如果數據表中已經同名的JobDetailTrigger,異常就產生了。

本文使用quartz 1.6版本,我們發現當後臺數據庫使用MySql時,數據保存不成功,該錯誤是Quartz的一個Bug,相信會在高版本中得到修復。因爲HSQLDB不支持SELECT * FROM TABLE_NAME FOR UPDATE的語法,所以不能使用HSQLDB數據庫。

附錄

·org.quartz.scheduler.instanceName 
每個 Scheduler 必須給定一個名稱來標識。當在同一個程序中有多個實例時,這個名稱作爲客戶代碼識別是哪個 Scheduler 而用。假如你用到了集羣特性,你就必須爲集羣中的每一個實例使用相同的名稱,以使它們成爲“邏輯上” 是同一個 Scheduler 。
·org.quartz.scheduler.instanceId
每個 Quartz Scheduler 必須指定一個唯一的 ID。這個值可以是任何字符串值,只要對於所有的 Scheduler 是唯一的。如果你想要自動生成的 ID,那你可以使用AUTO 作爲 instanceId 。從版本 1.5.1 開始,你能夠定製如何自動生成實例 ID。見instanceIDGenerator.class 屬性,會在接下來講到。
·org.quartz.scheduler.instanceIdGenerator.class
從版本 1.5.1 開始,這個屬性允許你定製instanceId 的生成,這個屬性僅被用於屬性org.quartz.scheduler.instanceId 設置爲AUTO 的情況下。默認是 org.quartz.simpl.SimpleInstanceIdGenerator ,它會基於主機名和時間戳來產生實例 ID 的。
·org.quartz.scheduler.threadName
可以是對於 Java 線程來說有效名稱的任何字符串。假如這個屬性未予指定,線程將會接受 Scheduler 名稱 (org.quartz.scheduler.instanceName ) 前附加上字符串 '_QuartzSchedulerThread' 作爲名稱。
·org.quartz.scheduler.idelWaitTime 
這個屬性設置了當 Scheduler 處於空閒時轉而再次查詢可用 Trigger 時所等待的毫秒數。通常,你無需調整這個參數,除非你正使用 XA 事物,遇到了 Trigger 本該立即觸發而發生延遲的問題。
·org.quartz.scheduler.dbFailureRetryInterval
這個屬性設置 Scheduler 在檢測到 JobStore 到某處的連接(比如到數據庫的連接) 斷開後,再次嘗試連接所等待的毫秒數。這個參數在使用 RamJobStore 無效。
·org.quartz.scheduler.classLoadHelper.class
對於多數健狀的應用,所使用的默認值爲 org.quartz.simpl.CascadingClassLoadHelper 類,它會依序使用其他的ClassLoadHelper 類,直到有一個能正常工作爲止。你大概沒必須爲這個屬性指定任何其他的類,除非有可能在應用服務器中時。當前所有可能的ClassLoadHelper 實現可在 org.quartz.simpl 包中找到。
·org.quartz.context.key.SOME_KEY 
這個屬性用於向 "Scheduler 上下文" 中置入一個 名-值 對錶示的字符串值。(見 Scheduler.getContext() )。因此,比如設置了org.quartz.context.key.MyEmail = [email protected]就相當於執行了scheduler.getContext().put("MyEmail", [email protected]) 
·org.quartz.scheduler.userTransactionURL
它設置了 Quartz 能在哪裏定位到應用服務器的 UserTransaction 管理器的 JNDI URL。默認值(未設定的話) 是java:comp/UserTransaction ,這幾乎能工作於所有的應用服務器中。Websphere 用戶也許需要設置這個屬性爲jta/usertransaction 。這個屬性僅用於 Quartz 配置使用 JobStoreCMT 的情況,並且org.quartz.scheduler.wrapJobExecutionInUserTransaction 被設定成了true 。
·org.quartz.scheduler.wrapJobExecutionInUserTransaction
如果你要 Quartz 在調用你的 Job 的 execute 之前啓動一個 UserTransaction 的話,設置這個屬性爲 true 。這個事物將在 Job 的execute 方法完成和 JobDataMap (假如是一個StatefulJob ) 更新後提交。默認值爲 false 。
·org.quartz.scheduler.jobFactory.class
這是所用的 JobFactory 的類名稱。默認爲 org.quartz.simpl.SimpleJobFactory 。你也可以試試org.quartz.simpl.PropertySettingJobFactory 。一個 Job 工廠負責產生 Job 類的實例。SimpleFactory 類是調用 Job 類的newInstance() 方法。PropertySettingJobFactory 也會調用newInstance() ,但還會使用 JobDataMap 中的內容以反射方式設置 Job Bean 的屬性。

小結

Quartz提供了最爲豐富的任務調度功能,不但可以制定週期性運行的任務調度方案,還可以讓你按照日曆相關的方式進行任務調度。Quartz框架的重要組件包括JobJobDetailTriggerScheduler以及輔助性的JobDataMapSchedulerContextQuartz擁有一個線程池,通過線程池爲任務提供執行線程,你可以通過配置文件對線程池進行參數定製。Quartz的另一個重要功能是可將任務調度信息持久化到數據庫中,以便系統重啓時能夠恢復已經安排的任務。此外,Quartz還擁有完善的事件體系,允許你註冊各種事件的監聽器。

5、quartz原理2

Quartz可以用來做什麼?

Quartz是一個任務調度框架。比如你遇到這樣的問題

  • 想每月25號,信用卡自動還款
  • 想每年4月1日自己給當年暗戀女神發一封匿名賀卡
  • 想每隔1小時,備份一下自己的愛情動作片 學習筆記到雲盤

這些問題總結起來就是:在某一個有規律的時間點幹某件事。並且時間的觸發的條件可以非常複雜(比如每月最後一個工作日的17:50),複雜到需要一個專門的框架來幹這個事。 Quartz就是來幹這樣的事,你給它一個觸發條件的定義,它負責到了時間點,觸發相應的Job起來幹活。

一個簡單的示例

這裏面的所有例子都是基於Quartz 2.2.1

package com.test.quartz;

import static org.quartz.DateBuilder.newDate;
import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;

import java.util.GregorianCalendar;

import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.impl.calendar.AnnualCalendar;

public class QuartzTest {

    public static void main(String[] args) {
        try {
            //創建scheduler
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            //定義一個Trigger
            Trigger trigger = newTrigger().withIdentity("trigger1", "group1") //定義name/group
                .startNow()//一旦加入scheduler,立即生效
                .withSchedule(simpleSchedule() //使用SimpleTrigger
                    .withIntervalInSeconds(1) //每隔一秒執行一次
                    .repeatForever()) //一直執行,奔騰到老不停歇
                .build();

            //定義一個JobDetail
            JobDetail job = newJob(HelloQuartz.class) //定義Job類爲HelloQuartz類,這是真正的執行邏輯所在
                .withIdentity("job1", "group1") //定義name/group
                .usingJobData("name", "quartz") //定義屬性
                .build();

            //加入這個調度
            scheduler.scheduleJob(job, trigger);

            //啓動之
            scheduler.start();

            //運行一段時間後關閉
            Thread.sleep(10000);
            scheduler.shutdown(true);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
package com.test.quartz;

import java.util.Date;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class HelloQuartz implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail detail = context.getJobDetail();
        String name = detail.getJobDataMap().getString("name");
        System.out.println("say hello to " + name + " at " + new Date());
    }
}

這個例子很好的覆蓋了Quartz最重要的3個基本要素:

  • Scheduler:調度器。所有的調度都是由它控制。
  • Trigger: 定義觸發的條件。例子中,它的類型是SimpleTrigger,每隔1秒中執行一次(什麼是SimpleTrigger下面會有詳述)。
  • JobDetail & Job: JobDetail 定義的是任務數據,而真正的執行邏輯是在Job中,例子中是HelloQuartz。 爲什麼設計成JobDetail + Job,不直接使用Job?這是因爲任務是有可能併發執行,如果Scheduler直接使用Job,就會存在對同一個Job實例併發訪問的問題。而JobDetail & Job 方式,sheduler每次執行,都會根據JobDetail創建一個新的Job實例,這樣就可以規避併發訪問的問題。

Quartz API

Quartz的API的風格在2.x以後,採用的是DSL風格(通常意味着fluent interface風格),就是示例中newTrigger()那一段東西。它是通過Builder實現的,就是以下幾個。(** 下面大部分代碼都要引用這些Builder ** )

//job相關的builder
import static org.quartz.JobBuilder.*;

//trigger相關的builder
import static org.quartz.TriggerBuilder.*;
import static org.quartz.SimpleScheduleBuilder.*;
import static org.quartz.CronScheduleBuilder.*;
import static org.quartz.DailyTimeIntervalScheduleBuilder.*;
import static org.quartz.CalendarIntervalScheduleBuilder.*;

//日期相關的builder
import static org.quartz.DateBuilder.*;

DSL風格寫起來會更加連貫,暢快,而且由於不是使用setter的風格,語義上會更容易理解一些。對比一下:

JobDetail jobDetail=new JobDetailImpl("jobDetail1","group1",HelloQuartz.class);
jobDetail.getJobDataMap().put("name", "quartz");

SimpleTriggerImpl trigger=new SimpleTriggerImpl("trigger1","group1");
trigger.setStartTime(new Date());
trigger.setRepeatInterval(1);
trigger.setRepeatCount(-1);

關於name和group

JobDetail和Trigger都有name和group。

name是它們在這個sheduler裏面的唯一標識。如果我們要更新一個JobDetail定義,只需要設置一個name相同的JobDetail實例即可。

group是一個組織單元,sheduler會提供一些對整組操作的API,比如 scheduler.resumeJobs()。

Trigger

在開始詳解每一種Trigger之前,需要先了解一下Trigger的一些共性。

StartTime & EndTime

startTime和endTime指定的Trigger會被觸發的時間區間。在這個區間之外,Trigger是不會被觸發的。

** 所有Trigger都會包含這兩個屬性 **

優先級(Priority)

當scheduler比較繁忙的時候,可能在同一個時刻,有多個Trigger被觸發了,但資源不足(比如線程池不足)。那麼這個時候比剪刀石頭布更好的方式,就是設置優先級。優先級高的先執行。

需要注意的是,優先級只有在同一時刻執行的Trigger之間纔會起作用,如果一個Trigger是9:00,另一個Trigger是9:30。那麼無論後一個優先級多高,前一個都是先執行。

優先級的值默認是5,當爲負數時使用默認值。最大值似乎沒有指定,但建議遵循Java的標準,使用1-10,不然鬼才知道看到【優先級爲10】是時,上頭還有沒有更大的值。

Misfire(錯失觸發)策略

類似的Scheduler資源不足的時候,或者機器崩潰重啓等,有可能某一些Trigger在應該觸發的時間點沒有被觸發,也就是Miss Fire了。這個時候Trigger需要一個策略來處理這種情況。每種Trigger可選的策略各不相同。

這裏有兩個點需要重點注意:

  • MisFire的觸發是有一個閥值,這個閥值是配置在JobStore的。比RAMJobStore是org.quartz.jobStore.misfireThreshold。只有超過這個閥值,纔會算MisFire。小於這個閥值,Quartz是會全部重新觸發。

所有MisFire的策略實際上都是解答兩個問題:

  1. 已經MisFire的任務還要重新觸發嗎?
  2. 如果發生MisFire,要調整現有的調度時間嗎?

比如SimpleTrigger的MisFire策略有:

  • MISFIRE_INSTRUCTION_IGNORE_MISFIRE_POLICY

    這個不是忽略已經錯失的觸發的意思,而是說忽略MisFire策略。它會在資源合適的時候,重新觸發所有的MisFire任務,並且不會影響現有的調度時間。

    比如,SimpleTrigger每15秒執行一次,而中間有5分鐘時間它都MisFire了,一共錯失了20個,5分鐘後,假設資源充足了,並且任務允許併發,它會被一次性觸發。

    這個屬性是所有Trigger都適用。

  • MISFIRE_INSTRUCTION_FIRE_NOW

    忽略已經MisFire的任務,並且立即執行調度。這通常只適用於只執行一次的任務。

  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT

    將startTime設置當前時間,立即重新調度任務,包括的MisFire的

  • MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT

    類似MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT,區別在於會忽略已經MisFire的任務

  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT

    在下一次調度時間點,重新開始調度任務,包括的MisFire的

  • MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT

    類似於MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT,區別在於會忽略已經MisFire的任務。

  • MISFIRE_INSTRUCTION_SMART_POLICY

    所有的Trigger的MisFire默認值都是這個,大致意思是“把處理邏輯交給聰明的Quartz去決定”。基本策略是,

    1. 如果是隻執行一次的調度,使用MISFIRE_INSTRUCTION_FIRE_NOW
    2. 如果是無限次的調度(repeatCount是無限的),使用MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
    3. 否則,使用MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT

MisFire的東西挺繁雜的,可以參考這篇

Calendar

這裏的Calendar不是jdk的java.util.Calendar,不是爲了計算日期的。它的作用是在於補充Trigger的時間。可以排除或加入某一些特定的時間點。

以”每月25日零點自動還卡債“爲例,我們想排除掉每年的2月25號零點這個時間點(因爲有2.14,所以2月一定會破產)。這個時間,就可以用Calendar來實現。

例子:

AnnualCalendar cal = new AnnualCalendar(); //定義一個每年執行Calendar,精度爲天,即不能定義到2.25號下午2:00
java.util.Calendar excludeDay = new GregorianCalendar();
excludeDay.setTime(newDate().inMonthOnDay(2, 25).build());
cal.setDayExcluded(excludeDay, true);  //設置排除2.25這個日期
scheduler.addCalendar("FebCal", cal, false, false); //scheduler加入這個Calendar

//定義一個Trigger
Trigger trigger = newTrigger().withIdentity("trigger1", "group1") 
    .startNow()//一旦加入scheduler,立即生效
    .modifiedByCalendar("FebCal") //使用Calendar !!
    .withSchedule(simpleSchedule()
        .withIntervalInSeconds(1) 
        .repeatForever()) 
    .build();

Quartz體貼地爲我們提供以下幾種Calendar,注意,所有的Calendar既可以是排除,也可以是包含,取決於:

  • HolidayCalendar。指定特定的日期,比如20140613。精度到天。
  • DailyCalendar。指定每天的時間段(rangeStartingTime, rangeEndingTime),格式是HH:MM[:SS[:mmm]]。也就是最大精度可以到毫秒。
  • WeeklyCalendar。指定每星期的星期幾,可選值比如爲java.util.Calendar.SUNDAY。精度是天。
  • MonthlyCalendar。指定每月的幾號。可選值爲1-31。精度是天
  • AnnualCalendar。 指定每年的哪一天。使用方式如上例。精度是天。
  • CronCalendar。指定Cron表達式。精度取決於Cron表達式,也就是最大精度可以到秒。

Trigger實現類

Quartz有以下幾種Trigger實現:

SimpleTrigger

指定從某一個時間開始,以一定的時間間隔(單位是毫秒)執行的任務。

它適合的任務類似於:9:00 開始,每隔1小時,執行一次。

它的屬性有:

  • repeatInterval 重複間隔
  • repeatCount 重複次數。實際執行次數是 repeatCount+1。因爲在startTime的時候一定會執行一次。** 下面有關repeatCount 屬性的都是同理。 **

例子:

simpleSchedule()
        .withIntervalInHours(1) //每小時執行一次
        .repeatForever() //次數不限
        .build();

simpleSchedule()
    .withIntervalInMinutes(1) //每分鐘執行一次
    .withRepeatCount(10) //次數爲10次
    .build();

CalendarIntervalTrigger

類似於SimpleTrigger,指定從某一個時間開始,以一定的時間間隔執行的任務。 但是不同的是SimpleTrigger指定的時間間隔爲毫秒,沒辦法指定每隔一個月執行一次(每月的時間間隔不是固定值),而CalendarIntervalTrigger支持的間隔單位有秒,分鐘,小時,天,月,年,星期

相較於SimpleTrigger有兩個優勢:1、更方便,比如每隔1小時執行,你不用自己去計算1小時等於多少毫秒。 2、支持不是固定長度的間隔,比如間隔爲月和年。但劣勢是精度只能到秒。

它適合的任務類似於:9:00 開始執行,並且以後每週 9:00 執行一次

它的屬性有:

  • interval 執行間隔
  • intervalUnit 執行間隔的單位(秒,分鐘,小時,天,月,年,星期)

例子:

calendarIntervalSchedule()
    .withIntervalInDays(1) //每天執行一次
    .build();

calendarIntervalSchedule()
    .withIntervalInWeeks(1) //每週執行一次
    .build();

DailyTimeIntervalTrigger

指定每天的某個時間段內,以一定的時間間隔執行任務。並且它可以支持指定星期。

它適合的任務類似於:指定每天9:00 至 18:00 ,每隔70秒執行一次,並且只要週一至週五執行。

它的屬性有:

  • startTimeOfDay 每天開始時間
  • endTimeOfDay 每天結束時間
  • daysOfWeek 需要執行的星期
  • interval 執行間隔
  • intervalUnit 執行間隔的單位(秒,分鐘,小時,天,月,年,星期)
  • repeatCount 重複次數

例子:

dailyTimeIntervalSchedule()
    .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) //第天9:00開始
    .endingDailyAt(TimeOfDay.hourAndMinuteOfDay(16, 0)) //16:00 結束 
    .onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY) //週一至週五執行
    .withIntervalInHours(1) //每間隔1小時執行一次
    .withRepeatCount(100) //最多重複100次(實際執行100+1次)
    .build();

dailyTimeIntervalSchedule()
    .startingDailyAt(TimeOfDay.hourAndMinuteOfDay(9, 0)) //第天9:00開始
    .endingDailyAfterCount(10) //每天執行10次,這個方法實際上根據 startTimeOfDay+interval*count 算出 endTimeOfDay
    .onDaysOfTheWeek(MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY) //週一至週五執行
    .withIntervalInHours(1) //每間隔1小時執行一次
    .build();

CronTrigger

適合於更復雜的任務,它支持類型於Linux Cron的語法(並且更強大)。基本上它覆蓋了以上三個Trigger的絕大部分能力(但不是全部)—— 當然,也更難理解。

它適合的任務類似於:每天0:00,9:00,18:00各執行一次。

它的屬性只有:

  • Cron表達式。但這個表示式本身就夠複雜了。下面會有說明。

例子:

cronSchedule("0 0/2 8-17 * * ?") // 每天8:00-17:00,每隔2分鐘執行一次
    .build();

cronSchedule("0 30 9 ? * MON") // 每週一,9:30執行一次
.build();

weeklyOnDayAndHourAndMinute(MONDAY,9, 30) //等同於 0 30 9 ? * MON 
    .build();

Cron表達式

位置 時間域 允許值 特殊值
1 0-59 , - * /
2 分鐘 0-59 , - * /
3 小時 0-23 , - * /
4 日期 1-31 , - * ? / L W C
5 月份 1-12 , - * /
6 星期 1-7 , - * ? / L C #
7 年份(可選) 1-31 , - * /

星號():可用在所有字段中,表示對應時間域的每一個時刻,例如, 在分鐘字段時,表示“每分鐘”;

問號(?):該字符只在日期和星期字段中使用,它通常指定爲“無意義的值”,相當於點位符;

減號(-):表達一個範圍,如在小時字段中使用“10-12”,則表示從10到12點,即10,11,12;

逗號(,):表達一個列表值,如在星期字段中使用“MON,WED,FRI”,則表示星期一,星期三和星期五;

斜槓(/):x/y表達一個等步長序列,x爲起始值,y爲增量步長值。如在分鐘字段中使用0/15,則表示爲0,15,30和45秒,而5/15在分鐘字段中表示5,20,35,50,你也可以使用*/y,它等同於0/y;

L:該字符只在日期和星期字段中使用,代表“Last”的意思,但它在兩個字段中意思不同。L在日期字段中,表示這個月份的最後一天,如一月的31號,非閏年二月的28號;如果L用在星期中,則表示星期六,等同於7。但是,如果L出現在星期字段裏,而且在前面有一個數值X,則表示“這個月的最後X天”,例如,6L表示該月的最後星期五;

W:該字符只能出現在日期字段裏,是對前導日期的修飾,表示離該日期最近的工作日。例如15W表示離該月15號最近的工作日,如果該月15號是星期六,則匹配14號星期五;如果15日是星期日,則匹配16號星期一;如果15號是星期二,那結果就是15號星期二。但必須注意關聯的匹配日期不能夠跨月,如你指定1W,如果1號是星期六,結果匹配的是3號星期一,而非上個月最後的那天。W字符串只能指定單一日期,而不能指定日期範圍;

LW組合:在日期字段可以組合使用LW,它的意思是當月的最後一個工作日;

井號(#):該字符只能在星期字段中使用,表示當月某個工作日。如6#3表示當月的第三個星期五(6表示星期五,#3表示當前的第三個),而4#5表示當月的第五個星期三,假設當月沒有第五個星期三,忽略不觸發;

C:該字符只在日期和星期字段中使用,代表“Calendar”的意思。它的意思是計劃所關聯的日期,如果日期沒有被關聯,則相當於日曆中所有日期。例如5C在日期字段中就相當於日曆5日以後的第一天。1C在星期字段中相當於星期日後的第一天。

Cron表達式對特殊字符的大小寫不敏感,對代表星期的縮寫英文大小寫也不敏感。

一些例子:

表示式 說明
0 0 12 * * ? 每天12點運行
0 15 10 ? * * 每天10:15運行
0 15 10 * * ? 每天10:15運行
0 15 10 * * ? * 每天10:15運行
0 15 10 * * ? 2008 在2008年的每天10:15運行
0 * 14 * * ? 每天14點到15點之間每分鐘運行一次,開始於14:00,結束於14:59。
0 0/5 14 * * ? 每天14點到15點每5分鐘運行一次,開始於14:00,結束於14:55。
0 0/5 14,18 * * ? 每天14點到15點每5分鐘運行一次,此外每天18點到19點每5鍾也運行一次。
0 0-5 14 * * ? 每天14:00點到14:05,每分鐘運行一次。
0 10,44 14 ? 3 WED 3月每週三的14:10分到14:44,每分鐘運行一次。
0 15 10 ? * MON-FRI 每週一,二,三,四,五的10:15分運行。
0 15 10 15 * ? 每月15日10:15分運行。
0 15 10 L * ? 每月最後一天10:15分運行。
0 15 10 ? * 6L 每月最後一個星期五10:15分運行。
0 15 10 ? * 6L 2007-2009 在2007,2008,2009年每個月的最後一個星期五的10:15分運行。
0 15 10 ? * 6#3 每月第三個星期五的10:15分運行。

JobDetail & Job

JobDetail是任務的定義,而Job是任務的執行邏輯。在JobDetail裏會引用一個Job Class定義。一個最簡單的例子

public class JobTest {
    public static void main(String[] args) throws SchedulerException, IOException {
           JobDetail job=newJob()
               .ofType(DoNothingJob.class) //引用Job Class
               .withIdentity("job1", "group1") //設置name/group
               .withDescription("this is a test job") //設置描述
               .usingJobData("age", 18) //加入屬性到ageJobDataMap
               .build();

           job.getJobDataMap().put("name", "quertz"); //加入屬性name到JobDataMap

           //定義一個每秒執行一次的SimpleTrigger
           Trigger trigger=newTrigger()
                   .startNow()
                   .withIdentity("trigger1")
                   .withSchedule(simpleSchedule()
                       .withIntervalInSeconds(1)
                       .repeatForever())
                   .build();

           Scheduler sche=StdSchedulerFactory.getDefaultScheduler();
           sche.scheduleJob(job, trigger);

           sche.start();

           System.in.read();

           sche.shutdown();
    }
}


public class DoNothingJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("do nothing");
    }
}

從上例我們可以看出,要定義一個任務,需要幹幾件事:

  1. 創建一個org.quartz.Job的實現類,並實現實現自己的業務邏輯。比如上面的DoNothingJob。
  2. 定義一個JobDetail,引用這個實現類
  3. 加入scheduleJob

Quartz調度一次任務,會幹如下的事:

  1. JobClass jobClass=JobDetail.getJobClass()
  2. Job jobInstance=jobClass.newInstance()。所以Job實現類,必須有一個public的無參構建方法。
  3. jobInstance.execute(JobExecutionContext context)。JobExecutionContext是Job運行的上下文,可以獲得Trigger、Scheduler、JobDetail的信息。

也就是說,每次調度都會創建一個新的Job實例,這樣的好處是有些任務併發執行的時候,不存在對臨界資源的訪問問題——當然,如果需要共享JobDataMap的時候,還是存在臨界資源的併發訪問的問題。

JobDataMap

Job都次都是newInstance的實例,那我怎麼傳值給它? 比如我現在有兩個發送郵件的任務,一個是發給"liLei",一個發給"hanmeimei",不能說我要寫兩個Job實現類LiLeiSendEmailJob和HanMeiMeiSendEmailJob。實現的辦法是通過JobDataMap。

每一個JobDetail都會有一個JobDataMap。JobDataMap本質就是一個Map的擴展類,只是提供了一些更便捷的方法,比如getString()之類的。

我們可以在定義JobDetail,加入屬性值,方式有二:

newJob().usingJobData("age", 18) //加入屬性到ageJobDataMap

 or

 job.getJobDataMap().put("name", "quertz"); //加入屬性name到JobDataMap

然後在Job中可以獲取這個JobDataMap的值,方式同樣有二:

public class HelloQuartz implements Job {
    private String name;

    public void execute(JobExecutionContext context) throws JobExecutionException {
        JobDetail detail = context.getJobDetail();
        JobDataMap map = detail.getJobDataMap(); //方法一:獲得JobDataMap
        System.out.println("say hello to " + name + "[" + map.getInt("age") + "]" + " at "
                           + new Date());
    }

    //方法二:屬性的setter方法,會將JobDataMap的屬性自動注入
    public void setName(String name) { 
        this.name = name;
    }
}

對於同一個JobDetail實例,執行的多個Job實例,是共享同樣的JobDataMap,也就是說,如果你在任務裏修改了裏面的值,會對其他Job實例(併發的或者後續的)造成影響。

除了JobDetail,Trigger同樣有一個JobDataMap,共享範圍是所有使用這個Trigger的Job實例。

Job併發

Job是有可能併發執行的,比如一個任務要執行10秒中,而調度算法是每秒中觸發1次,那麼就有可能多個任務被併發執行。

有時候我們並不想任務併發執行,比如這個任務要去”獲得數據庫中所有未發送郵件的名單“,如果是併發執行,就需要一個數據庫鎖去避免一個數據被多次處理。這個時候一個@DisallowConcurrentExecution解決這個問題。

就是這樣

public class DoNothingJob implements Job {
    @DisallowConcurrentExecution
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("do nothing");
    }
}

注意,@DisallowConcurrentExecution是對JobDetail實例生效,也就是如果你定義兩個JobDetail,引用同一個Job類,是可以併發執行的。

JobExecutionException

Job.execute()方法是不允許拋出除JobExecutionException之外的所有異常的(包括RuntimeException),所以編碼的時候,最好是try-catch住所有的Throwable,小心處理。

其他屬性

  • Durability(耐久性?)

    如果一個任務不是durable,那麼當沒有Trigger關聯它的時候,它就會被自動刪除。

  • RequestsRecovery

    如果一個任務是"requests recovery",那麼當任務運行過程非正常退出時(比如進程崩潰,機器斷電,但不包括拋出異常這種情況),Quartz再次啓動時,會重新運行一次這個任務實例。

    可以通過JobExecutionContext.isRecovering()查詢任務是否是被恢復的。

Scheduler

Scheduler就是Quartz的大腦,所有任務都是由它來設施。

Schduelr包含一個兩個重要組件: JobStore和ThreadPool。

JobStore是會來存儲運行時信息的,包括Trigger,Schduler,JobDetail,業務鎖等。它有多種實現RAMJob(內存實現),JobStoreTX(JDBC,事務由Quartz管理),JobStoreCMT(JDBC,使用容器事務),ClusteredJobStore(集羣實現)、TerracottaJobStore(什麼是Terractta)。

ThreadPool就是線程池,Quartz有自己的線程池實現。所有任務的都會由線程池執行。

SchedulerFactory

SchdulerFactory,顧名思義就是來用創建Schduler了,有兩個實現:DirectSchedulerFactory和 StdSchdulerFactory。前者可以用來在代碼裏定製你自己的Schduler參數。後者是直接讀取classpath下的quartz.properties(不存在就都使用默認值)配置來實例化Schduler。通常來講,我們使用StdSchdulerFactory也就足夠了。

SchdulerFactory本身是支持創建RMI stub的,可以用來管理遠程的Scheduler,功能與本地一樣,可以遠程提交個Job什麼的。

DirectSchedulerFactory的創建接口

    /**
     * Same as
     * {@link DirectSchedulerFactory#createScheduler(ThreadPool threadPool, JobStore jobStore)},
     * with the addition of specifying the scheduler name and instance ID. This
     * scheduler can only be retrieved via
     * {@link DirectSchedulerFactory#getScheduler(String)}
     *
     * @param schedulerName
     *          The name for the scheduler.
     * @param schedulerInstanceId
     *          The instance ID for the scheduler.
     * @param threadPool
     *          The thread pool for executing jobs
     * @param jobStore
     *          The type of job store
     * @throws SchedulerException
     *           if initialization failed
     */
    public void createScheduler(String schedulerName,
            String schedulerInstanceId, ThreadPool threadPool, JobStore jobStore)
        throws SchedulerException;

StdSchdulerFactory的配置例子, 更多配置,參考Quartz配置指南

org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10 
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

這裏未講的稍微高級的主題


發佈了63 篇原創文章 · 獲贊 67 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章