Introduction to Quartz(1)

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介绍)