1. 程式人生 > >Quartz實現定時任務

Quartz實現定時任務

在開發過程中,我們經常會遇到一些需要非同步定期執行的批處理任務。比如夜裡低峰時段的備份、統計,或者是每週、每月對資料庫表進行整理,這時就需要通過使用定時工作管理員來輔助我們完成這些任務的定時觸發。常見的定時工作管理員多分為三類,分別是:

  1. 作業系統(OS)級別的定時工作管理員,例如linux的crontab、windows自帶的計劃任務。OS級不用專門開啟監聽器,佔用系統資源較少,而且操作簡便,是定時任務首選的實現方式,但是但是當任務數量非常大,而且任務與任務之間有因果關係、先後順序、競爭條件的話,OS級別的定時工作管理員就很難滿足需求了;
  2. 程式語言自帶的定時工作管理員,例如Java的timer和TimeTask。但是這些API提供的介面功能簡單,往往不能滿足使用者定時任務設定需要,所以在專案開發過程中很少使用;
  3. 第三方專門開發的定時任務管理元件,例如Java的quartz,python的celery等。這些元件往往既可以單獨部署,也可以與當前的專案整合在一起統一部署管理,關鍵是他們有著強大的功能,能夠滿足我們對定時任務管理的各種需求,所以這些第三方元件往往在專案中應用廣泛。

一、Quartz的體系結構

 Quartz對任務排程的領域問題進行了高度的抽象,提出了排程器、作業任務和觸發器這3個核心的概念,並在org.quartz通過介面和類對重要的這些核心概念進行描述:
  1. Job:是一個介面,只有一個方法void execute(JobExecutionContext
    context),開發者實現該介面定義執行任務,JobExecutionContext類提供了排程上下文的各種資訊。Job執行時的資訊儲存在JobDataMap例項中;
  2. JobDetail:Quartz在每次執行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名稱;
  3. Trigger:是一個類,描述觸發Job執行的時間觸發規則。主要有SimpleTrigger和CronTrigger這兩個子類。當僅需觸發一次或者以固定時間間隔週期執行,SimpleTrigger是最適合的選擇;而CronTrigger則可以通過Cron表示式定義出各種複雜時間規則的排程方案:如每早晨9:00執行,週一、週三、週五下午5:00執行等;
  4. Scheduler:代表一個Quartz的獨立執行容器,Trigger和JobDetail可以註冊到Scheduler中,兩者在Scheduler中擁有各自的組及名稱,組及名稱是Scheduler查詢定位容器中某一物件的依據,Trigger的組及名稱必須唯一,JobDetail的組和名稱也必須唯一(但可以和Trigger的組和名稱相同,因為它們是不同型別的)。Scheduler定義了多個介面方法,允許外部通過組及名稱訪問和控制容器中Trigger和JobDetail。

    Scheduler可以將Trigger繫結到某一JobDetail中,這樣當Trigger觸發時,對應的Job就被執行。一個Job可以對應多個Trigger,但一個Trigger只能對應一個Job。可以通過SchedulerFactory建立一個Scheduler例項。Scheduler擁有一個SchedulerContext,它類似於ServletContext,儲存著Scheduler上下文資訊,Job和Trigger都可以訪問SchedulerContext內的資訊。SchedulerContext內部通過一個Map,以鍵值對的方式維護這些上下文資料,SchedulerContext為儲存和獲取資料提供了多個put()和getXxx()的方法。可以通過Scheduler# getContext()獲取對應的SchedulerContext例項;

  5. ThreadPool:Scheduler使用一個執行緒池作為任務執行的基礎設施,任務通過共享執行緒池中的執行緒提高執行效率。

二、程式碼實踐

通過上面的介紹,我們可以親自動手用Java實現一個定時任務了,下面例子中我使用的Quartz版本為2.2.1,不同版本API可能會有些不同。
首先需要增加Quartz Maven依賴,如下:

<dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>

同時還需要增加slf4j依賴,如下:

<dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.2</version>
        </dependency>

完整的pom.xml如下:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.ricky.maven</groupId>
    <artifactId>quartz</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>QuartzDemo</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
            <version>1.7.2</version>
        </dependency>
    </dependencies>
</project>

1.作業任務
通過實現 org.quartz.job 介面,可以使 Java 類變成可執行的。下面的例子就提供了 Quartz 作業的一個示例。這個類用一條非常簡單的輸出語句覆蓋了 execute(JobExecutionContext context) 方法。這個方法可以包含我們想要執行的任何程式碼。

package com.ricky.maven.quartz.job;

import java.util.Date;

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

public class SimpleQuartzJob implements Job {

    public SimpleQuartzJob() {}

    public void execute(JobExecutionContext context) throws JobExecutionException {

        System.out.println("SimpleQuartzJob - executing its JOB at "     
                + new Date() + " by " + context.getTrigger().getClass().getName()+" in thread:"+Thread.currentThread().getName());   
    }    
}  

execute 方法接受一個 JobExecutionContext 物件作為引數。這個物件提供了作業例項的執行時上下文。特別地,它提供了對排程器和觸發器的訪問,這兩者協作來啟動作業以及作業的 JobDetail 物件的執行。Quartz 通過把作業的狀態放在 JobDetail 物件中並讓 JobDetail 建構函式啟動一個作業的例項,分離了作業的執行和作業周圍的狀態。JobDetail 物件儲存作業的偵聽器、群組、資料對映、描述以及作業的其他屬性。
2.簡單觸發器
觸發器可以實現對任務執行的排程。Quartz 提供了幾種不同的觸發器,複雜程度各不相同。下面的例子中的 SimpleTrigger 展示了觸發器的基礎:

package com.ricky.maven.quartz;

import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.SimpleTrigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

import com.ricky.maven.quartz.job.SimpleQuartzJob;

public class SimpleQuartzDemo {

    public static void main(String[] args) {

        try {
            SchedulerFactory schedulerFactory = new StdSchedulerFactory();  
            Scheduler scheduler = schedulerFactory.getScheduler();
            JobDetail jobDetail = JobBuilder.newJob(SimpleQuartzJob.class).build();
            SimpleTrigger trigger = TriggerBuilder.newTrigger().withIdentity("myJob", "mygroup")
                        .withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInSeconds(2)
                            .withRepeatCount(100))
//                          .startAt(new Date(Calendar.getInstance().getTimeInMillis()+ 3000))
                            .startNow()
                            .build(); 
            scheduler.scheduleJob(jobDetail, trigger);
            scheduler.start();

        } catch (SchedulerException e) {
            e.printStackTrace();
        }  
    }

}

開始時例項化一個 SchedulerFactory,獲得此排程器。就像前面討論過的,建立 JobDetail 物件時,它的建構函式要接受一個 Job 作為引數。顧名思義,SimpleTrigger 例項相當原始。在建立物件之後,設定幾個基本屬性以立即排程任務,然後每 2 秒重複一次,直到作業被執行100次。
3.定時觸發器
CronTrigger 支援比 SimpleTrigger 更具體的排程,而且也不是很複雜。基於 cron 表示式,CronTrigger 支援類似日曆的重複間隔,而不是單一的時間間隔 —— 這相對 SimpleTrigger 而言是一大改進。

package com.ricky.maven.quartz;

import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerFactory;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

import com.ricky.maven.quartz.job.SimpleQuartzJob;

public class CronQuartzDemo {

    public static void main(String[] args) {

        try {
            SchedulerFactory schedulerFactory = new StdSchedulerFactory();  
            Scheduler scheduler = schedulerFactory.getScheduler();
            JobDetail jobDetail = JobBuilder.newJob(SimpleQuartzJob.class).build();
            Trigger trigger = TriggerBuilder.newTrigger().withIdentity("simple")
                    .withSchedule(CronScheduleBuilder.cronSchedule("*/5 * * * * ?"))    //每隔5秒執行一次
                    .startNow()
                    .build();
            scheduler.scheduleJob(jobDetail, trigger);
            scheduler.start();

        } catch (SchedulerException e) {
            e.printStackTrace();
        }  
    }

}

CronTriggers往往比SimpleTrigger更有用,如果您需要基於日曆的概念,而非SimpleTrigger完全指定的時間間隔,復發的發射工作的時間表。
CronTrigger,你可以指定觸發的時間表如“每星期五中午”,或“每個工作日9:30時”,甚至“每5分鐘一班9:00和10:00逢星期一上午,星期三星期五“。
即便如此,SimpleTrigger一樣,CronTrigger擁有的startTime指定的時間表時生效,指定的時間表時,應停止(可選)結束時間。
Quartz使用類似於Linux下的cron表示式定義時間規則,cron的表示式是字串,實際上是由七子表示式,描述個別細節的時間表。這些子表示式是分開的空白,代表:
位置 時間單位
1 Seconds
2 Minutes
3 Hours
4 Day-of-Month
5 Month
6 Day-of-Week
7 Year (可選欄位)

例 “0 0 12 ? * WED” 在每星期三下午12:00 執行,
每一個欄位都有一套可以指定有效值,如
Seconds (秒) :可以用數字0-59 表示,
Minutes(分) :可以用數字0-59 表示,
Hours(時) :可以用數字0-23表示,
Day-of-Month(天) :可以用數字1-31 中的任一一個值,但要注意一些特別的月份
Month(月) :可以用0-11 或用字串 “JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and DEC” 表示
Day-of-Week(每週):可以用數字1-7表示(1 = 星期日)或用字元口串“SUN, MON, TUE, WED, THU, FRI and SAT”表示
“/”:為特別單位,表示為“每”如“0/15”表示每隔15分鐘執行一次,“0”表示為從“0”分開始, “3/20”表示表示每隔20分鐘執行一次,“3”表示從第3分鐘開始執行
“?”:表示每月的某一天,或第周的某一天
“L”:用於每月,或每週,表示為每月的最後一天,或每個月的最後星期幾如“6L”表示“每月的最後一個星期五”
“W”:表示為最近工作日,如“15W”放在每月(day-of-month)欄位上表示為“到本月15日最近的工作日”
“#”:是用來指定“的”每月第n個工作日,例 在每週(day-of-week)這個欄位中內容為”6#3” or “FRI#3” 則表示“每月第三個星期五”