欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Introduction to Quartz(1)

程序员文章站 2022-06-15 18:44:20
...
1 Overview
Quartz是一个开源的作业调度框架,可被集成到任何的Java EE 或者Java SE程序中。

2 Scheduler
通过SchedulerFactory接口创建Scheduler对象。当 Scheduler 实例被创建之后,就会被保存到一个仓库中(org.quartz.impl.SchedulerRepository)。Scheduler对象可以被start、standby和shutdown。一旦Scheduler被shutdown,那么不应该重新启动它。Scheduler对象被启动后,Triggers才会被触发。在Trigger被触发时,Scheduler会创建 Job 类的实例,执行实例的 execute() 方法并传入JobExecutionContext 上下文变量,JobExecutionContext封装了 Quartz 的运行时环境。

2.1 StdScheduler
可以通过调用以java.util.Properties为参数的initialize方法对StdSchedulerFactory实例进行配置。StdSchedulerFactory 还提供了一个静态方法 getDefaultScheduler()用来获得默认的Scheduler,假如之前未调用过任何一个 initialize() 方法,那么无参的 initialize() 方法会被调用。如果使用无参的 initialize 方法,StdSchedulerFactory 会执行以下几个步骤去尝试为工厂加载属性:
1. 检查System.getProperty("org.quartz.properties") 中是否设置了别的文件名;否则使用 quartz.properties 作为要加载的文件名。
2. 试图从当前工作目录中加载这个文件。
3. 试图从系统 classpath 下加载这个文件。这一步总是能成功的,因为在 Quartz Jar 包中有一个默认的 quartz.properties 文件。

以下是个quartz.properties 文件的例子:

#===============================================================
#Configure Main Scheduler Properties
#===============================================================
org.quartz.scheduler.instanceName = QuartzScheduler
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false

#===============================================================
#Configure ThreadPool
#===============================================================
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

#===============================================================
#Configure JobStore
#===============================================================
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
org.quartz.jobStore.misfireThreshold = 60000

org.quartz.scheduler.instanceName用于指定Scheduler的实例名,可以是任意字符串。org.quartz.scheduler.instanceId用于指定实例ID,也可以是任意字符串。在集群环境中instanceId必须唯一。如果希望 Quartz 自动生成这个值,那么可以设置为 AUTO。如果 Quartz 框架是运行在非集群环境中,那么自动产生的值将会是 NON_CLUSTERED;如果是在集群环境下使用 Quartz,这个值将会是主机名加上当前的日期和时间。
org.quartz.threadPool.class属性值是一个实现了 org.quartz.spi.ThreadPool 接口的全限定类名。Quartz 自带的实现类是 org.quartz.smpl.SimpleThreadPool,它提供了固定大小的线程池,并经很好的测试过,能满足绝大多数情况下的需求。你也可以指定其它的线程池实现,例如需要一个可伸缩的线程池。
org.quartz.jobStore.class属性指定了在Scheduler的生命周期中,Job 和 Trigger 信息是如何被存储的。如果使用RAMJobStore,那么一旦调度器进程被终止,所有的 Job 和 Trigger 的状态就丢失了。除了RAMJobStore,还可以指定JDBCJobStore。虽然JDBCJobStore的性能不如RAMJobStore,但是可以持久地保存Job 和 Trigger 信息。
以下是个简单的例子:

public class DummyJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(sdf.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start();
JobDetail jobDetail = new JobDetail("myJob", null, DummyJob.class);
Trigger trigger = TriggerUtils.makeSecondlyTrigger(2);
trigger.setName("myTrigger");
scheduler.scheduleJob(jobDetail, trigger);
Thread.sleep(10000);
scheduler.shutdown();
}
}


3 Job
一个 Quartz Job 就是一个执行任务的 Java 类。Job 的实例要到该执行它们的时候才会实例化出来。这意味着 Job 不必担心线程安全性,因为同一时刻仅有一个线程去执行给定 Job 类的实例。Job有以下三个主要的属性:
易失性(volatility)。Job在程序关闭之后是否被持久化。 通过调用 JobDetail 的 setVolatility(boolean flag) 进行设置,默认值是 false。RamJobStore 使用的是非永久性存储器,所有关于 Job 和 Trigger 的信息会在程序关闭之后丢失。保存 Job 到 RamJobStore使得它们是易失性的。假如你需要让你的 Job 信息在程序重启之后仍能保留下来,你就该使用JDBCJobStore。
持久性(Durability)。 在所有的触发器触发之后,是否将Job从 JobStore 中移除。通过调用 JobDetail 的 setDurability(boolean flag) 方法设置,默认值是 false。假设设置了一个单次触发的 Trigger,触发之后它就变成了 STATE_COMPLETE 状态。这个 Trigger 指向的 Job 现在成了一个孤儿 Job,因为不再有任何 Trigger 与之相关联了。假如你设置这个Job为持久的,那么即使它成了孤儿 Job 也不会从 JobStore 移除掉。这样可以保证在将来,无论何时你的程序决定为这个 Job 增加另一个 Trigger 都是可用的。
可恢复性(Recovery)。当一个 Job 还在执行中,Scheduler 经历了一次非预期的关闭,在 Scheduler 重启之后该 Job 是否会重头开始再次被执行。通过调用 JobDetail 的 setRequestsRecovery(boolean flag)方法设置,默认值是 false。

3.1 JobDetail
实际上,并不是直接把 Job 对象注册到 Scheduler,而是注册一个 JobDetail 实例。JobDetail用于对Job进行配置。每当Trigger被触发时,Scheduler会创建相对应的Job的实例并调用其execute方法。传入的JobExecutionContext参数包含了该Job的运行时环境,例如得到相关的Scheduler、Trigger和JobDetail。

3.2 JobDataMap
由于每次调度Job的时候都有一个新的Job实例被创建,因此每次Job被调度后状态都丢失了。通过使用JobDataMap,可以向Job传递配置信息,或者保存多次调度之间的作业状态。JobDataMap 通过它的超类 org.quartz.util.DirtyFlagMap 实现了java.util.Map 接口。如果你使用的是持久化的JobStore(例如JDBCJobStore),那么JobDataMap中存放的数据必须能够被序列化。
可以在JobDetail和Trigger上设置JobDataMap。如果向 JobDataMap 中存入键/值对,那么这些数据可以在Job实例被执行时,通过传入参数JobExecutionContext中包含的Trigger和JobDetail实例访问到,因此这是一个向 Job实例传送配置的信息便捷方法。JobExecutionContext提供了一个方法getMergedJobDataMap,用于得到JobDetail和Trigger中包含的JobDataMap的合并后版本。需要注意的是,如果键/值对冲突,那么Trigger中包含的JobDataMap的值会覆盖掉JobDetail中包含的JobDataMap的值。以下是个简单的例子:

public class DummyJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String name = context.getMergedJobDataMap().getString("name");
System.out.println(df.format(new Date()) + " Job executed, name: " + name);
}

public static void main(String args[]) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
scheduler.start();
JobDetail jobDetail = new JobDetail("myJob", null, DummyJob.class);
jobDetail.getJobDataMap().put("name", "job1");
Trigger trigger = TriggerUtils.makeSecondlyTrigger(2);
trigger.setName("myTrigger");
scheduler.scheduleJob(jobDetail, trigger);
Thread.sleep(10000);
scheduler.shutdown();
}
}


3.3 Job
Job定义如下:

public interface Job {
execute(JobExecutionContext context) throws JobExecutionException;
}

每当Scheduler调度Job的时候,Scheduler根据配置的JobFactory创建一个新的Job实例。默认的JobFactory(org.quartz.simpl.SimpleJobFactory)只是通过反射,即调用Job类的newInstance方法构造Job实例,因此Job类需要有个可访问的无参构造函数。

3.4 StatefulJob
StatefulJob扩展Job,没有添加新的方法,其定义如下:

public interface StatefulJob extends Job {
}

当你需要在两次 Job 执行间维护状态的话,Quartz 框架为此提供了 org.quartz.StatefulJob 接口。Job 和 StatefulJob 存在以下两个关键差异:
JobDataMap 在每次执行之后重新持久化到 JobStore 中。这样就确保对stateful Job执行时对JobDataMap的修改都会保存,下次执行时也仍然可见。对于non-stateful Job,JobDataMap只是在JobDetail被注册到Scheduler的时候持久化到JobStore 中,这意味着每次non-stateful Job执行时对JobDataMap的修改都会被丢弃。
两个或多个有状态的 JobDetail 实例不能并发执行。如果有两个Trigger试图同时触发一个相同的stateful Job,那么其中一个Trigger会被阻塞,直到另外一个Trigger触发的Job执行完毕。这确保了不会对JobDataMap进行并发的修改,从而确保了线程安全。
下面是个使用StatefulJob接口的例子:

public class DummyStatefulJob implements StatefulJob {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
JobDataMap map = context.getJobDetail().getJobDataMap();
Integer round = (Integer)map.get("round");
if(round == null) {
round = new Integer(1);
} else {
round = new Integer(round.intValue() + 1);
}
map.put("round", round);
System.out.println(df.format(new Date()) + " Job executed, round: " + round);
}

public static void main(String args[]) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
scheduler.start();
JobDetail jd = new JobDetail("myJob", null, DummyStatefulJob.class);
Trigger trigger = TriggerUtils.makeSecondlyTrigger(2);
trigger.setName("myTrigger");
scheduler.scheduleJob(jd, trigger);
Thread.sleep(10000);
scheduler.shutdown();
}
}

以上程序的输出如下:

2013-04-26 11:20:50.357 Job executed, round: 1
2013-04-26 11:20:52.357 Job executed, round: 2
2013-04-26 11:20:54.342 Job executed, round: 3
2013-04-26 11:20:56.342 Job executed, round: 4
2013-04-26 11:20:58.342 Job executed, round: 5
2013-04-26 11:21:00.342 Job executed, round: 6


3.5 InterruptableJob
InterruptableJob扩展Job,其定义如下:

public interface InterruptableJob extends Job {
void interrupt() throws UnableToInterruptJobException;
}

可以通过调用Scheduler 接口的interrupt(String jobName, String groupName) 方法来中断特定的Job。Scheduler会负责调用InterruptableJob的interrupt()方法。至于Job如何被中断,这要取决于Job的具体实现。下面是个使用InterruptableJob接口的例子:

public class DummyInterruptableJob implements InterruptableJob {
//
private final AtomicBoolean interrupted = new AtomicBoolean(false);


@Override
public void interrupt()
throws UnableToInterruptJobException {
interrupted.compareAndSet(false, true);
}

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
while(!interrupted.get()) {
System.out.println(df.format(new Date()) + " Job executing");
try {
Thread.sleep(1000);
} catch(Exception e) {
}
}
System.out.println(df.format(new Date()) + " Job interrupted");
}

public static void main(String args[]) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
scheduler.start();
JobDetail jd = new JobDetail("job1", null, DummyInterruptableJob.class);
SimpleTrigger trigger = new SimpleTrigger("trigger1", null);
trigger.setRepeatCount(0);
scheduler.scheduleJob(jd, trigger);
Thread.sleep(5000);
scheduler.interrupt("job1", null);
scheduler.shutdown();
}
}

以上程序的输出如下:

2010-08-09 08:58:54.248 Job executing
2010-08-09 08:58:55.248 Job executing
2010-08-09 08:58:56.248 Job executing
2010-08-09 08:58:57.248 Job executing
2010-08-09 08:58:58.248 Job executing
2010-08-09 08:58:59.248 Job interrupted

需要注意的是,以上例子中构造JobDetail的第二个参数group为null(即使用默认的group),因此在调用Scheduler的interrupt()方法时的第二个参数(group)也必须为null。

4 Trigger
Job 包含了要执行任务的逻辑,Trigger 决定了Job 何时被执行。可以为单个Job 使用多个 Trigger,但一个 Trigger 只能被指派给一个 Job。Trigger上也可以配置JobDataMap,当Trigger触发时,被执行的Job实例可以获得Trigger所关联的JobDataMap。特别是当多个Trigger被关联到一个JobDetail时,可以通过Trigger所关联的JobDataMap向Job提供额外的配置信息。Quartz提供了多种Trigger实现,最常用的有SimpleTrigger、NthIncludeDayTrigger和CronTrigger。
某一时刻,假设有m个trigger可能被触发,但是只有n(m > n)个工作线程可用,那么具有最高优先级的n个trigger会优先被触发。通过Trigger的setPriority(int priority)方法可以调整Trigger的优先级,默认优先级是5。
Trigger的另外一个比较重要的属性是misfire instruction。由于scheduler被shutdown,trigger被pause或者没有可用的工作线程等原因,都会导致trigger没有被触发。不同类型的Trigger可以设置不同的misfire instruction。

4.1 SimpleTrigger
SimpleTrigger是最简单的一种 Trigger。 如果你需要某个Job在特定时间执行一次,或者在某个时间执行一次后再以特定的间隔重复执行 n 次,那么可以使用SimpleTrigger。SimpleTrigger包含以下属性:开始时间、结束时间、重复次数和重复间隔。重复次数可以是0、正整数或者SimpleTrigger.REPEAT_INDEFINITELY。重复间隔的单位是毫秒。如果设置了结束时间,那么它会覆盖重复次数,例如你设置了开始时间、结束时间和重复间隔,那么Quartz会自动计算重复次数。下面是个使用SimpleTrigger的例子:

public class SimpleTriggerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
String startedTime = df.format(new Date());
try {
System.out.println("Job started, started time: " + startedTime);
Thread.sleep(3000);
} catch(Exception e) {
throw new JobExecutionException(e);
} finally {
System.out.println("Job stopped, started time: " + startedTime);
}
}

public static void main(String args[]) throws Exception {
//
Properties props = new Properties();
props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool");
props.put("org.quartz.threadPool.threadCount", "5");
StdSchedulerFactory factory = new StdSchedulerFactory();
factory.initialize(props);
Scheduler scheduler = factory.getScheduler();
scheduler.start();

//
JobDetail jd = new JobDetail("job1", "group1", SimpleTriggerJob.class);
SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1");
trigger.setRepeatCount(2);
trigger.setRepeatInterval(1000);
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上程序的输出如下:

Job started, started time: 2013-04-26 09:29:37.765
Job started, started time: 2013-04-26 09:29:38.750
Job started, started time: 2013-04-26 09:29:39.750
Job stopped, started time: 2013-04-26 09:29:37.765
Job stopped, started time: 2013-04-26 09:29:38.750
Job stopped, started time: 2013-04-26 09:29:39.750

如果将以上例子中props的org.quartz.threadPool.threadCount属性设置为2(也就是说最多同时能执行2个Job),那么程序的输出如下:

Job started, started time: 2013-04-26 09:31:10.156
Job started, started time: 2013-04-26 09:31:11.125
Job stopped, started time: 2013-04-26 09:31:10.156
Job started, started time: 2013-04-26 09:31:13.156
Job stopped, started time: 2013-04-26 09:31:11.125
Job stopped, started time: 2013-04-26 09:31:13.156

从以上输出可以看出,本应该在09:31:12左右调度的第三个任务,由于没有可用的线程而被阻塞,直到第一个任务结束之后(09:31:13)才被调度。需要注意的是,如果给trigger设置不同的misfire instruction,可能会有不同的行为。

4.2 NthIncludeDayTrigger
NthIncludedDayTrigger用于在每一间隔类型的第几天执行 Job,例如要在每个月的 15 号执行特定的 Job。目前Quartz 支持的间隔类型有:INTERVAL_TYPE_WEEKLY、INTERVAL_TYPE_MONTHLY和INTERVAL_TYPE_YEARLY。下面是个使用NthIncludeDayTrigger的例子:

public class NthIncludedDayTriggerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
JobDataMap map = context.getJobDetail().getJobDataMap();
String name = map.getString("name");
System.out.println(df.format(new Date()) + " Job executed, name: " + name);
}

public static void main(String args[]) throws Exception {
//
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

//
JobDetail jd = new JobDetail("job1", "group1", NthIncludedDayTriggerJob.class);
NthIncludedDayTrigger trigger = new NthIncludedDayTrigger("trigger1", "group1");
trigger.setN(15);
trigger.setIntervalType(NthIncludedDayTrigger.INTERVAL_TYPE_MONTHLY);
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,任务被配置成在每月15日执行。执行时间可以通过NthIncludedDayTrigger的setFireAtTime(String fireAtTime)方法设置,默认是12:00:00。

4.3 CronTrigger
SimpleTrigger 对于需要在指定的时间(毫秒级)执行的Job还是不错的,但是假如你的Job需要更复杂的执行计划时,可能需要使用CronTrigger 提供的更灵活的功能。
CronExpression用于配置CronTrigger,它封装了一个字符串,这个字符串由空格分割的7个子表达式构成,它们分别代表:

1. Seconds
2. Minutes
3. Hours
4. Day-of-Month
5. Month
6. Day-of-Week
7. Year (可选)

例如"0 0 12 ? * WED"代表每周三的12点,"* * * ? * *"则代表每秒都执行。每个子表达式都有可选值的集合:Seconds和Minutes的可选值是0到59;Hours的可选值是0到23;Day-of-Month的可选值是1到31;Months的可选值是0到11,或者使用JAN、FEB、MAR、APR、MAY、JUN、JUL、AUG、SEP、OCT、NOV和DEC;Days-of-Week的可选值是1到7(1代表周日),或者是SUN、MON、TUE、WED、THU、FRI和SAT。每个子表达式也可以包含范围和列表。例如"MON-FRI"代表一个范围,即周一到周五;"MON, WED, FRI"代表一个列表,即周一、周三和周五;"MON-WED,SAT"代表周一、周二、周三和周六。
CronExpression表达式支持用特殊字符来创建更为复杂的执行计划。下面是可以在CronExpression中使用的特殊字符:
通配符"*"代表每一个可能的值。
"/"用于指定值的增量,例如Minutes字段上的"0/15"代表从0分钟起每隔15分钟。
"?"可以放在Day-of-Month和Day-of-Week中。"?"表示这个字段不包含具体值。所以如果指定Day-of-Month,那么可以在Day-of-Week字段中设置"?",表示Day-of-Week值无关紧要。
"L"可以放在Day-of-Month和Day-of-Week中。Day-of-Month表示在当月最后一天执行。在Day-of-Week中,如果"L"单独存在,就等于7(星期六),否则代表当月内或者周内日期的最后一个实例。例如"* * * L 1 ? 2008"匹配2008-01-31(星期四);"* * * ? 1 L 2008"匹配2008-01-05(星期六)、2008-01-12(星期六)、2008-01-19(星期六)和2008-01-26(星期六)。
"W"可以放在Day-of-Month中。W 字符代表着工作日 (周一到周五),用来指定离指定日的最近的一个平日。例如,Day-of-Month中的 15W 意味着 "离该月15日的最近一个工作日"。如果15日是工作日,那么就在15日执行; 假如15日是星期六,那么会在14日(周五)执行;如果15日是周日,那么会在16日(周一)执行。
"#" 字符仅能用于Day-of-Week中。它用于指定月份中的第几周的哪一天。例如,如果你指定周域的值为 6#3,它意思是某月的第三个周五 (6=星期五,#3意味着月份中的第三周)。假如你指定 #5,然而月份中没有第 5 周,那么该月不会触发。
下面是个关于CronExpression的例子程序:

public class CronExpressionTest {

public static void main(String args[]) throws Exception {
//
String expression[] = new String[]{
"* * * 4W 1 ? 2008",
"* * * 5W 1 ? 2008",
"* * * 6W 1 ? 2008",
"* * * 7W 1 ? 2008",
"* * * LW 1 ? 2008",
"* * * L 1 ? 2008",
"* * * ? 1 L 2008",
"* * * ? 1 0L 2008",
"* * * ? 1 1L 2008",
"* * * ? 1 2L 2008",
"* * * ? 1 3L 2008",
"* * * ? 1 4L 2008",
"* * * ? 1 5L 2008",
"* * * ? 1 6L 2008",
"* * * ? 1 7L 2008",
"* * * ? 1 1#1 2008",
"* * * ? 1 1#2 2008",
"* * * ? 1 1#3 2008",
"* * * ? 1 1#4 2008",
"* * * ? 1 1#5 2008"
};

//
for(int i = 0; i < expression.length; i++) {
System.out.println("######################");
CronExpression cp = new CronExpression(expression[i]);
for(int j = 0; j <= 31; j++) {
Date d = getDate(2008, 1, j);
if(cp.isSatisfiedBy(d)) {
System.out.println(cp.getCronExpression() + " matches " + print(d));
}
}
}
}

private static String print(Date date) {
SimpleDateFormat d = new SimpleDateFormat("yyyy-MM-dd(E)");
return d.format(date);
}

private static Date getDate(int year, int month, int dayOfMonth) {
Calendar c = Calendar.getInstance();
c.set(Calendar.YEAR, year);
c.set(Calendar.MONTH, month - 1);
c.set(Calendar.DAY_OF_MONTH, dayOfMonth);
return c.getTime();
}
}

以上程序输出如下:

######################
* * * 4W 1 ? 2008 matches 2008-01-04(星期五)
######################
* * * 5W 1 ? 2008 matches 2008-01-04(星期五)
######################
* * * 6W 1 ? 2008 matches 2008-01-07(星期一)
######################
* * * 7W 1 ? 2008 matches 2008-01-07(星期一)
######################
* * * LW 1 ? 2008 matches 2008-01-31(星期四)
######################
* * * L 1 ? 2008 matches 2008-01-31(星期四)
######################
* * * ? 1 L 2008 matches 2008-01-05(星期六)
* * * ? 1 L 2008 matches 2008-01-12(星期六)
* * * ? 1 L 2008 matches 2008-01-19(星期六)
* * * ? 1 L 2008 matches 2008-01-26(星期六)
######################
* * * ? 1 0L 2008 matches 2008-01-26(星期六)
######################
* * * ? 1 1L 2008 matches 2008-01-27(星期日)
######################
* * * ? 1 2L 2008 matches 2008-01-28(星期一)
######################
* * * ? 1 3L 2008 matches 2008-01-29(星期二)
######################
* * * ? 1 4L 2008 matches 2008-01-30(星期三)
######################
* * * ? 1 5L 2008 matches 2008-01-31(星期四)
######################
* * * ? 1 6L 2008 matches 2008-01-25(星期五)
######################
* * * ? 1 7L 2008 matches 2008-01-26(星期六)
######################
* * * ? 1 1#1 2008 matches 2008-01-06(星期日)
######################
* * * ? 1 1#2 2008 matches 2008-01-13(星期日)
######################
* * * ? 1 1#3 2008 matches 2008-01-20(星期日)
######################
* * * ? 1 1#4 2008 matches 2008-01-27(星期日)
######################

下面是个使用CronTrigger的例子:

public class CronTriggerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
//
Properties props = new Properties();
props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool");
props.put("org.quartz.threadPool.threadCount", "2");
StdSchedulerFactory factory = new StdSchedulerFactory();
factory.initialize(props);
Scheduler scheduler = factory.getScheduler();
scheduler.start();

//
Date startTime = new Date();
Date endTime = new Date(startTime.getTime() + 20 * 1000);

//
JobDetail jd = new JobDetail("job1", "group1", CronTriggerJob.class);
CronTrigger trigger = new CronTrigger("trigger1", "group1");
CronExpression cronExpression = new CronExpression("0/5 * * * * ?");
trigger.setCronExpression(cronExpression);
trigger.setStartTime(startTime);
trigger.setEndTime(endTime);
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

在以上例子中,指定从每分钟的0秒起,每个5秒调度一次。此外还使用 CronTrigger 的 setStartTime方法 和 setEndTime方法限制了执行的时间范围。
CronTrigger支持的misfire instruction有MISFIRE_INSTRUCTION_SMART_POLICY(0),MISFIRE_INSTRUCTION_FIRE_ONCE_NOW(1)和MISFIRE_INSTRUCTION_DO_NOTHING(2)。其中MISFIRE_INSTRUCTION_SMART_POLICY等效于MISFIRE_INSTRUCTION_FIRE_ONCE_NOW。以下是关于misfire instruction的简单例子:

public class SimpleTriggerListener implements TriggerListener {
//
private String name;
private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

public SimpleTriggerListener(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void triggerFired(Trigger trigger, JobExecutionContext context) {
System.out.println(dateFormat.format(new Date()) + " triggerFired, trigger name: " + trigger.getName());
}

public void triggerMisfired(Trigger trigger) {
System.out.println(dateFormat.format(new Date()) + " triggerMisfired, trigger name: " + trigger.getName());
}

public void triggerComplete(Trigger trigger, JobExecutionContext context, int triggerInstructionCode) {
System.out.println(dateFormat.format(new Date()) + " triggerComplete, trigger name: " + trigger.getName());
}

public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
return false;
}
}

public class CronTriggerJob implements StatefulJob {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
Integer round = (Integer)context.getJobDetail().getJobDataMap().get("round");
if(round == null) {
round = new Integer(1);
} else {
round = new Integer(round.intValue() + 1);
}
context.getJobDetail().getJobDataMap().put("round", round);

System.out.println(df.format(new Date()) + " Job executed, round: " + round);
}

public static void main(String args[]) throws Exception {
//
Properties props = new Properties();
props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS, "org.quartz.simpl.SimpleThreadPool");
props.put("org.quartz.threadPool.threadCount", "5");
props.put("org.quartz.jobStore.misfireThreshold", "1000");
StdSchedulerFactory factory = new StdSchedulerFactory();
factory.initialize(props);
Scheduler scheduler = factory.getScheduler();
SimpleTriggerListener stl = new SimpleTriggerListener("triggerListener1");
scheduler.addTriggerListener(stl);
scheduler.start();

//
Date startTime = new Date();
Date endTime = new Date(startTime.getTime() + 6 * 1000);

//
JobDetail jd = new JobDetail("job1", "group1", CronTriggerJob.class);
CronTrigger trigger = new CronTrigger("trigger1", "group1");
CronExpression cronExpression = new CronExpression("0/1 * * * * ?");
trigger.setCronExpression(cronExpression);
trigger.setStartTime(startTime);
trigger.setEndTime(endTime);
trigger.addTriggerListener(stl.getName());
trigger.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_SMART_POLICY);
scheduler.scheduleJob(jd, trigger);

//
Thread.sleep(2000);
System.out.println("start to pause trigger");
scheduler.pauseTrigger("trigger1", "group1");
Thread.sleep(2500);
System.out.println("start to resume trigger");
scheduler.resumeTrigger("trigger1", "group1");
Thread.sleep(2000);
System.out.println("start to shutdown scheduler");
scheduler.shutdown();
}
}

以上例子中,如果将trigger的miscfire instruction设置为MISFIRE_INSTRUCTION_DO_NOTHING,那么输出可能如下:

2010-08-09 14:34:50.890 triggerFired, trigger name: trigger1
2010-08-09 14:34:50.890 Job executed, round: 1
2010-08-09 14:34:50.890 triggerComplete, trigger name: trigger1
2010-08-09 14:34:51.015 triggerFired, trigger name: trigger1
2010-08-09 14:34:51.015 Job executed, round: 2
2010-08-09 14:34:51.015 triggerComplete, trigger name: trigger1
2010-08-09 14:34:52.015 triggerFired, trigger name: trigger1
2010-08-09 14:34:52.015 Job executed, round: 3
2010-08-09 14:34:52.015 triggerComplete, trigger name: trigger1
start to pause trigger
start to resume trigger
2010-08-09 14:34:55.390 triggerMisfired, trigger name: trigger1
2010-08-09 14:34:56.000 triggerFired, trigger name: trigger1
2010-08-09 14:34:56.000 Job executed, round: 4
2010-08-09 14:34:56.000 triggerComplete, trigger name: trigger1
start to shutdown scheduler
如果将trigger的miscfire instruction设置为MISFIRE_INSTRUCTION_SMART_POLICY或者MISFIRE_INSTRUCTION_FIRE_ONCE_NOW,那么输出可能如下:
2010-08-09 14:35:20.921 triggerFired, trigger name: trigger1
2010-08-09 14:35:20.921 Job executed, round: 1
2010-08-09 14:35:20.921 triggerComplete, trigger name: trigger1
2010-08-09 14:35:21.015 triggerFired, trigger name: trigger1
2010-08-09 14:35:21.015 Job executed, round: 2
2010-08-09 14:35:21.015 triggerComplete, trigger name: trigger1
2010-08-09 14:35:22.015 triggerFired, trigger name: trigger1
2010-08-09 14:35:22.015 Job executed, round: 3
2010-08-09 14:35:22.015 triggerComplete, trigger name: trigger1
start to pause trigger
start to resume trigger
2010-08-09 14:35:25.421 triggerMisfired, trigger name: trigger1
2010-08-09 14:35:25.421 triggerFired, trigger name: trigger1
2010-08-09 14:35:25.421 Job executed, round: 4
2010-08-09 14:35:25.421 triggerComplete, trigger name: trigger1
2010-08-09 14:35:26.000 triggerFired, trigger name: trigger1
2010-08-09 14:35:26.000 Job executed, round: 5
2010-08-09 14:35:26.000 triggerComplete, trigger name: trigger1
start to shutdown scheduler


5 Calendar
跟 java.util.Calendar不同,Quartz Calendar用于指定一个时间区间,使 Trigger 在这个区间中不被触发。org.quartz.Calendar 接口中两个最重要的方法如下:
public long getNextIncludedTime(long timeStamp);
public boolean isTimeIncluded(long timeStamp);
Calendar 接口方法参数的类型是 long,这说明 Quartz Calender 能够排除的时间可以精确到毫秒级。要使用 Quartz Calendar,首先要把Calendar实例添加到Scheduler 中,然后在 Trigger 实例中指定关联的Calendar名字。

5.1 Builtin Calendars
Quartz 提供了以下常用的 Calender:
BaseCalender。为其它的Calender 实现了基本的功能。
WeeklyCalendar。 排除星期中的一天或多天。
MonthlyCalendar。 排除月的数天。
AnnualCalendar。排除年中一天或多天。
HolidayCalendar。特别用于排除特定节假日
下面是个使用AnnualCalendar的例子:

public class AnnualCalendarJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
//
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

//
JobDetail jd = new JobDetail("job1", "group1", AnnualCalendarJob.class);

//
Calendar c = Calendar.getInstance();
c.set(Calendar.MONTH, Calendar.JANUARY);
c.set(Calendar.DATE, 1);

AnnualCalendar ac = new AnnualCalendar();
ac.setDayExcluded(c, true);
scheduler.addCalendar("calendar1", ac, true, true);

//
CronTrigger trigger = new CronTrigger("trigger1", "group1");
CronExpression cronExpression = new CronExpression("0/5 * * * * ?");
trigger.setCronExpression(cronExpression);
trigger.setCalendarName("calendar1");
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,任务在每年的一月一日都不会被调度。

5.2 Customized Calendar
除了使用Quartz提供的Calendar,我们也可以创建定制的Calendar,例如创建一个 HourlyCalendar用于排除小时中的特定分钟。

public class HourlyCalendar extends BaseCalendar {
//
private static final long serialVersionUID = -4438834879438520769L;

//
private CopyOnWriteArrayList<Integer> excludedMinutes = new CopyOnWriteArrayList<Integer>();

/**
*
*/
public HourlyCalendar() {
super();
}

public HourlyCalendar(Calendar baseCalendar) {
super(baseCalendar);
}

/**
*
*/
public boolean isTimeIncluded(long msec) {
//
if (super.isTimeIncluded(msec) == false) {
return false;
}

//
final java.util.Calendar cal = createJavaCalendar(msec);
int minute = cal.get(java.util.Calendar.MINUTE);
return !(isMinuteExcluded(minute));
}

public long getNextIncludedTime(long msec) {
//
long baseTime = super.getNextIncludedTime(msec);
if ((baseTime > 0) && (baseTime > msec)) {
msec = baseTime;
}

//
final java.util.Calendar cal = getStartOfSecond(msec);
int minute = cal.get(java.util.Calendar.MINUTE);
if (isMinuteExcluded(minute) == false) {
return msec;
}

//
while (isMinuteExcluded(minute) == true) {
cal.add(java.util.Calendar.MINUTE, 1);
minute = cal.get(java.util.Calendar.MINUTE);
}
return cal.getTime().getTime();
}

public List<Integer> getMinutesExcluded() {
return excludedMinutes;
}

public void setMinuteExcluded(int minute) {
if (isMinuteExcluded(minute)) {
return;
}
excludedMinutes.add(new Integer(minute));
}

public void setMinutesExcluded(List<Integer> minutes) {
if (minutes == null) {
this.excludedMinutes.clear();
} else {
this.excludedMinutes.addAll(minutes);
}
}

public boolean isMinuteExcluded(int minute) {
for(Integer excludedMinute : this.excludedMinutes) {
if (minute == excludedMinute.intValue()) {
return true;
}
}
return false;
}

/**
*
*/
private java.util.Calendar getStartOfSecond(long msec) {
final java.util.Calendar c = (super.getTimeZone() == null) ? java.util.Calendar.getInstance() : java.util.Calendar.getInstance(getTimeZone());
c.setTimeInMillis(msec);
c.set(java.util.Calendar.SECOND, 0);
c.set(java.util.Calendar.MILLISECOND, 0);
return c;
}
}

public class HourlyCalendarJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
//
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

//
JobDetail jd = new JobDetail("job1", "group1", HourlyCalendarJob.class);

//
HourlyCalendar cal = new HourlyCalendar();
for (int i = 0; i < 60; i++) {
if (i % 2 == 0) {
cal.setMinuteExcluded(i);
}
}

//
long now = System.currentTimeMillis();
for(int i = 0; i < 100; i++) {
long next = cal.getNextIncludedTime(now);
System.out.println(new Date(next));
}
scheduler.addCalendar("hourlyCalendar1", cal, true, true);

//
CronTrigger trigger = new CronTrigger("trigger1", "group1");
CronExpression cronExpression = new CronExpression("0/5 * * * * ?");
trigger.setCronExpression(cronExpression);
trigger.setCalendarName("hourlyCalendar1");
scheduler.scheduleJob(jd, trigger);

//
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while (true) {
String line = br.readLine();
if (line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,只有奇数分钟内,任务才会被调度。

6 Listener
6.1 JobListener
JobListener 的定义如下:

public interface JobListener{
String getName();
void jobToBeExecuted(JobExecutionContext context);
void jobExecutionVetoed(JobExecutionContext context);
void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException);
}

getName() 方法返回一个字符串用以说明 JobListener 的名称。
Scheduler 在 JobDetail 将要被执行时调用jobToBeExecuted()方法。
Scheduler 在 JobDetail 即将被执行,却又被 TriggerListener 否决了时调用jobExecutionVetoed()方法。
Scheduler 在 JobDetail 被执行之后调用jobWasExecuted()方法。
下面是个关于JobListener的例子:

public class SimpleJobListener implements JobListener {
//
private String name;

public SimpleJobListener(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void jobToBeExecuted(JobExecutionContext context) {
System.out.println("jobToBeExecuted, job name: " + context.getJobDetail().getName());
}

public void jobExecutionVetoed(JobExecutionContext context) {
System.out.println("jobExecutionVetoed, job name: " + context.getJobDetail().getName());
}

public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
System.out.println("jobWasExecuted, job name: " + context.getJobDetail().getName());
}
}

public class ListenerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

SimpleJobListener sjl = new SimpleJobListener("listener1");
scheduler.addJobListener(sjl);

JobDetail jd = new JobDetail("job1", "group1", ListenerJob.class);
jd.addJobListener(sjl.getName());

SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1");
trigger.setRepeatCount(3);
trigger.setRepeatInterval(1000);
scheduler.scheduleJob(jd, trigger);

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,SimpleJobListener是作为一个非全局的JobListener注册到Scheduler的。首先调用Scheduler的addJobListener()方法,方法的参数是JobListener实例。然后对于任何引用到它的JobDetail,调用addJobListener()方法,方法的参数是JobListener的名称(也就是JobListener的getName()方法返回的名称)。如果需要注册一个全局的JobListener,只需要调用Scheduler的addGlobalJobListener()方法即可,这个JobListener就会和所有Job相关联。

6.2 TriggerListener
TriggerListener接口的定义如下:

public interface TriggerListener {
String getName();
void triggerFired(Trigger trigger, JobExecutionContext context);
boolean vetoJobExecution(Trigger trigger, JobExecutidonContext context);
void triggerMisfired(Trigger trigger);
void triggerComplete(Trigger trigger, JobExecutionContext context, int triggerInstructionCode);
}

和前面提到的 JobListener 一样,TriggerListner 接口的 getName() 返回一个字符串用以说明监听器的名称。
当与监听器相关联的 Trigger 被触发,Job 上的 execute() 方法将要被执行时,Scheduler调用triggerFired()方法。
Scheduler在 Trigger 触发后,Job 将要被执行时调用vetoJobExecution()方法。vetoJobExecution()方法给了TriggerListener 否决 Job 的执行的权力,如果返回 true,那么这个 Job 将不会为此次 Trigger的触发而得到执行。
Scheduler在 Trigger 错过触发时调用triggerMisfired() 方法。
Trigger 被触发并且完成了 Job 的执行时,Scheduler 调用triggerComplete()方法。
以下是个关于TriggerListener的例子:

public class SimpleTriggerListener implements TriggerListener {
//
private String name;

public SimpleTriggerListener(String name) {
this.name = name;
}

public String getName() {
return name;
}

public void triggerFired(Trigger trigger, JobExecutionContext context) {
System.out.println("triggerFired, trigger name: " + trigger.getName());
}

public void triggerMisfired(Trigger trigger) {
System.out.println("triggerMisfired, trigger name: " + trigger.getName());
}

public void triggerComplete(Trigger trigger, JobExecutionContext context, int triggerInstructionCode) {
System.out.println("triggerComplete, trigger name: " + trigger.getName());
}

public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
boolean r = (System.currentTimeMillis() / 1000) % 2 == 0;
System.out.println("vetoJobExecution, trigger name: " + trigger.getName() + ", voted: " + r);
return r;
}
}

public class ListenerJob implements Job {

@Override
public void execute(JobExecutionContext context)
throws JobExecutionException {
DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
System.out.println(df.format(new Date()) + " Job executed");
}

public static void main(String args[]) throws Exception {
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();

SimpleJobListener sjl = new SimpleJobListener("jobListener1");
scheduler.addJobListener(sjl);

SimpleTriggerListener stl = new SimpleTriggerListener("triggerListener1");
scheduler.addTriggerListener(stl);

JobDetail jd = new JobDetail("job1", "group1", ListenerJob.class);
jd.addJobListener(sjl.getName());

SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1");
trigger.setRepeatCount(100);
trigger.setRepeatInterval(500);
trigger.addTriggerListener(stl.getName());
scheduler.scheduleJob(jd, trigger);

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
while(true) {
String line = br.readLine();
if(line.equalsIgnoreCase("quit")) {
break;
}
}
br.close();
scheduler.shutdown();
}
}

以上例子中,SimpleTriggerListener是作为一个非全局的TriggerListener注册到Scheduler的。首先调用Scheduler的addTriggerListener()方法,方法的参数是TriggerListener实例。然后对于任何引用到它的Trigger,调用addTriggerListener ()方法,方法的参数是TriggerListener的名称(也就是TriggerListener的getName()方法返回的名称)。如果需要注册一个全局的TriggerListener,只需要调用Scheduler的addGlobalTriggerListener()方法即可,这个TriggerListener就会和所有Trigger相关联。以上程序的输出如下:

triggerFired, trigger name: trigger1
vetoJobExecution, trigger name: trigger1, voted: false
jobToBeExecuted, job name: job1
2010-08-09 11:39:25.546 Job executed
jobWasExecuted, job name: job1
triggerComplete, trigger name: trigger1
triggerFired, trigger name: trigger1
vetoJobExecution, trigger name: trigger1, voted: true
jobExecutionVetoed, job name: job1

以上例子中,在偶数秒内调度的任务都被否决了。需要注意的是,如果TriggerListener中否决了某个任务,那么相应的triggerComplete()方法不会被调用。

6.3 SchedulerListener
SchedulerListener 接口包含了一系列的回调方法,它们会在 Scheduler 的生命周期中有关键事件发生时被调用。

public interface SchedulerListener {
void jobScheduled(Trigger trigger);
void jobUnscheduled(String triggerName, String triggerGroup);
void triggerFinalized(Trigger trigger);
void triggersPaused(String triggerName, String triggerGroup);
void triggersResumed(String triggerName,String triggerGroup);
void jobsPaused(String jobName, String jobGroup);
void jobsResumed(String jobName, String jobGroup);
void schedulerError(String msg, SchedulerException cause);
void schedulerShutdown();
}

Scheduler 在有新的 JobDetail 部署或卸载时调用jobScheduled() 和 jobUnscheduled()方法。
当一个 Trigger 到了再也不会触发的状态时调用triggerFinalized() 方法,除非与之关联 Job 已设置成了持久性,否则它就会从 Scheduler 中移除。
Scheduler 在发生在一个 Trigger 或 Trigger 组被暂停时调用triggersPaused() 方法。如果是 Trigger 组,那么triggerName 参数将为 null。
Scheduler 在发生成一个 Trigger 或 Trigger 组从暂停中恢复时调用triggersResumed() 方法。如果是 Trigger 组,那么triggerName 参数将为 null。
当一个或一组 JobDetail 暂停时调用jobsPaused() 方法方法。
当一个或一组 Job 从暂停上恢复时调用jobsResumed() 方法。如果是一个 Job 组,那么jobName 参数将为 null。
在 Scheduler 的正常运行期间产生一个严重错误时调用schedulerError() 方法。你可以使用 SchedulerException 的 getErrorCode() 或者 getUnderlyingException() 方法或获取到特定错误的更详尽的信息。
Scheduler 调用schedulerShutdown() 方法用来通知 SchedulerListener Scheduler 将要被关闭。
相关标签: Quartz介绍