了解Quartz体系结构数据库
Quartz对任务调度的领域问题进行了高度的抽象,提出了调度器、任务和触发器这3个核心的概念,并在org.quartz经过接口和类对重要的这些核心概念进行描述:编程
●Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各类信息。Job运行时的信息保存在JobDataMap实例中;服务器
●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名称;架构
●Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则能够经过Cron表达式定义出各类复杂时间规则的调度方案:如每早晨9:00执行,周1、周3、周五下午5:00执行等;并发
●Calendar:org.quartz.Calendar和java.util.Calendar不一样,它是一些日历特定时间点的集合(能够简单地将org.quartz.Calendar看做java.util.Calendar的集合——java.util.Calendar表明一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个Trigger能够和多个Calendar关联,以便排除或包含某些时间点。oracle
假设,咱们安排每周星期一早上10:00执行任务,可是若是碰到法定的节日,任务则不执行,这时就须要在Trigger触发机制的基础上使用Calendar进行定点排除。针对不一样时间段类型,Quartz在org.quartz.impl.calendar包下提供了若干个Calendar的实现类,如AnnualCalendar、MonthlyCalendar、WeeklyCalendar分别针对每一年、每个月和每周进行定义;负载均衡
●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实例;
●ThreadPool:Scheduler使用一个线程池做为任务运行的基础设施,任务经过共享线程池中的线程提升运行效率。
Job有一个StatefulJob子接口,表明有状态的任务,该接口是一个没有方法的标签接口,其目的是让Quartz知道任务的类型,以便采用不一样的执行方案。无状态任务在执行时拥有本身的JobDataMap拷贝,对JobDataMap的更改不会影响下次的执行。而有状态任务共享共享同一个JobDataMap实例,每次任务执行对JobDataMap所作的更改会保存下来,后面的执行能够看到这个更改,也即每次执行任务后都会对后面的执行发生影响。
正由于这个缘由,无状态的Job能够并发执行,而有状态的StatefulJob不能并发执行,这意味着若是前次的StatefulJob尚未执行完毕,下一次的任务将阻塞等待,直到前次任务执行完毕。有状态任务比无状态任务须要考虑更多的因素,程序每每拥有更高的复杂度,所以除非必要,应该尽可能使用无状态的Job。
若是Quartz使用了数据库持久化任务调度信息,无状态的JobDataMap仅会在Scheduler注册任务时保持一次,而有状态任务对应的JobDataMap在每次执行任务后都会进行保存。
Trigger自身也能够拥有一个JobDataMap,其关联的Job能够经过JobExecutionContext#getTrigger().getJobDataMap()获取Trigger中的JobDataMap。无论是有状态仍是无状态的任务,在任务执行期间对Trigger的JobDataMap所作的更改都不会进行持久,也即不会对下次的执行产生影响。
Quartz拥有完善的事件和监听体系,大部分组件都拥有事件,如任务执行前事件、任务执行后事件、触发器触发前事件、触发后事件、调度器开始事件、关闭事件等等,能够注册相应的监听器处理感兴趣的事件。
图1描述了Scheduler的内部组件结构,SchedulerContext提供Scheduler全局可见的上下文信息,每个任务都对应一个JobDataMap,虚线表达的JobDataMap表示对应有状态的任务:
图1 Scheduler结构图
一个Scheduler能够拥有多个Triger组和多个JobDetail组,注册Trigger和JobDetail时,若是不显式指定所属的组,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):这是最复杂的一个构造函数,在指定触发参数的同时,还经过jobGroup和jobName,让该Trigger和Scheduler中的某个任务关联起来。
经过实现 org.quartz..Job 接口,可使 Java 类化身为可调度的任务。代码清单1提供了 Quartz 任务的一个示例:
代码清单1 SimpleJob:简单的Job实现类
package com.baobaotao.basic.quartz;
import java.util.Date;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public class SimpleJob implements Job {
①实例Job接口方法
public void execute(JobExecutionContext jobCtx)throws JobExecutionException {
System.out.println(jobCtx.getTrigger().getName()+ " triggered. time is:" + (new Date()));
}
}
这个类用一条很是简单的输出语句实现了Job接口的execute(JobExecutionContext context) 方法,这个方法能够包含想要执行的任何代码。下面,咱们经过SimpleTrigger对SimpleJob进行调度:
代码清单2 SimpleTriggerRunner:使用SimpleTrigger进行调度
package com.baobaotao.basic.quartz;
import java.util.Date;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.impl.StdSchedulerFactory;
public class SimpleTriggerRunner {
public static void main(String args[]) {
try {
①建立一个JobDetail实例,指定SimpleJob
JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class);
②经过SimpleTrigger定义调度规则:立刻启动,每2秒运行一次,共运行100次
SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");
simpleTrigger.setStartTime(new Date());
simpleTrigger.setRepeatInterval(2000);
simpleTrigger.setRepeatCount(100);
③经过SchedulerFactory获取一个调度器实例
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail, simpleTrigger);④ 注册并进行调度
scheduler.start();⑤调度启动
} catch (Exception e) {
e.printStackTrace();
}
}
}
首先在①处经过JobDetail封装SimpleJob,同时指定Job在Scheduler中所属组及名称,这里,组名为jGroup1,而名称为job1_1。
在②处建立一个SimpleTrigger实例,指定该Trigger在Scheduler中所属组及名称。接着设置调度的时间规则。
最后,须要建立Scheduler实例,并将JobDetail和Trigger实例注册到Scheduler中。这里,咱们经过StdSchedulerFactory获取一个Scheduler实例,并经过scheduleJob(JobDetail jobDetail, Trigger trigger)完成两件事:
1)将JobDetail和Trigger注册到Scheduler中;
2)将Trigger指派给JobDetail,将二者关联起来。
当Scheduler启动后,Trigger将按期触发并执行SimpleJob的execute(JobExecutionContext jobCtx)方法,而后每 10 秒重复一次,直到任务被执行 100 次后中止。
还能够经过SimpleTrigger的setStartTime(java.util.Date startTime)和setEndTime(java.util.Date endTime)指定运行的时间范围,当运行次数和时间范围冲突时,超过期间范围的任务运行不被执行。如能够经过simpleTrigger.setStartTime(new Date(System.currentTimeMillis() + 60000L))指定60秒钟之后开始。
除了经过scheduleJob(jobDetail, simpleTrigger)创建Trigger和JobDetail的关联,还有另一种关联Trigger和JobDetail的方式:
JobDetail jobDetail = new JobDetail("job1_1","jGroup1", SimpleJob.class);
SimpleTrigger simpleTrigger = new SimpleTrigger("trigger1_1","tgroup1");
…
simpleTrigger.setJobGroup("jGroup1");①-1:指定关联的Job组名
simpleTrigger.setJobName("job1_1");①-2:指定关联的Job名称
scheduler.addJob(jobDetail, true);② 注册JobDetail
scheduler.scheduleJob(simpleTrigger);③ 注册指定了关联JobDetail的Trigger
在这种方式中,Trigger经过指定Job所属组及Job名称,而后使用Scheduler的scheduleJob(Trigger trigger)方法注册Trigger。有两个值得注意的地方:
经过这种方式注册的Trigger实例必须已经指定Job组和Job名称,不然调用注册Trigger的方法将抛出异常;
引用的JobDetail对象必须已经存在于Scheduler中。也即,代码中①、②和③的前后顺序不能互换。
在构造Trigger实例时,能够考虑使用org.quartz.TriggerUtils工具类,该工具类不但提供了众多获取特定时间的方法,还拥有众多获取常见Trigger的方法,如makeSecondlyTrigger(String trigName)方法将建立一个每秒执行一次的Trigger,而makeWeeklyTrigger(String trigName, int dayOfWeek, int hour, int minute)将建立一个每星期某一特定时间点执行一次的Trigger。而getEvenMinuteDate(Date date)方法将返回某一时间点一分钟之后的时间。
使用CronTrigger
CronTrigger 可以提供比 SimpleTrigger 更有具体实际意义的调度方案,调度规则基于 Cron 表达式,CronTrigger 支持日历相关的重复时间间隔(好比每个月第一个周一执行),而不是简单的周期时间间隔。所以,相对于SimpleTrigger而言,CronTrigger在使用上也要复杂一些。
Cron表达式
Quartz使用相似于Linux下的Cron表达式定义时间规则,Cron表达式由6或7个由空格分隔的时间字段组成,如表1所示:
表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”,则表示从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表达式对特殊字符的大小写不敏感,对表明星期的缩写英文大小写也不敏感。表2下面给出一些完整的Cron表示式的实例:表2 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分运行。 |
CronTrigger实例
下面,咱们使用CronTrigger对SimpleJob进行调度,经过Cron表达式制定调度规则,让它每5秒钟运行一次:
代码清单3 CronTriggerRunner:使用CronTrigger进行调度
package com.baobaotao.basic.quartz;
import org.quartz.CronExpression;
import org.quartz.CronTrigger;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
public class CronTriggerRunner {
public static void main(String args[]) {
try {
JobDetail jobDetail = new JobDetail("job1_2", "jGroup1",SimpleJob.class);
①-1:建立CronTrigger,指定组及名称
CronTrigger cronTrigger = new CronTrigger("trigger1_2", "tgroup1");
CronExpression cexp = new CronExpression("0/5 * * * * ?");①-2:定义Cron表达式
cronTrigger.setCronExpression(cexp);①-3:设置Cron表达式
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.scheduleJob(jobDetail, cronTrigger);
scheduler.start();
//②
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行CronTriggerRunner,每5秒钟将触发运行SimpleJob一次。默认状况下Cron表达式对应当前的时区,能够经过CronTriggerRunner的setTimeZone(java.util.TimeZone timeZone)方法显式指定时区。你还也能够经过setStartTime(java.util.Date startTime)和setEndTime(java.util.Date endTime)指定开始和结束的时间。
在代码清单3的②处须要经过Thread.currentThread.sleep()的方式让主线程睡眠,以便调度器能够继续工做执行任务调度。不然在调度器启动后,由于主线程立刻退出,也将同时引发调度器关闭,调度器中的任务都将相应销毁,这将致使看不到实际的运行效果。在单元测试的时候,让主线程睡眠常用的办法。对于某些长周期任务调度的测试,你能够简单地调整操做系统时间进行模拟。
使用Calendar
在实际任务调度中,咱们不可能一成不变地按照某个周期性的调度规则运行任务,必须考虑到实现生活中日历上特定日期,就象习惯了大男人做风的人在2月14号也会有不一样表现同样。
下面,咱们安排一个任务,每小时运行一次,并将五一节和国际节排除在外,其代码如代码清单4所示:
代码清单4 CalendarExample:使用Calendar
package com.baobaotao.basic.quartz;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.quartz.impl.calendar.AnnualCalendar;
import org.quartz.TriggerUtils;
…
public class CalendarExample {
public static void main(String[] args) throws Exception {
SchedulerFactory sf = new StdSchedulerFactory();
Scheduler scheduler = sf.getScheduler();
①法定节日是以每一年为周期的,因此使用AnnualCalendar
AnnualCalendar holidays = new AnnualCalendar();
②五一劳动节
Calendar laborDay = new GregorianCalendar();
laborDay.add(Calendar.MONTH,5);
laborDay.add(Calendar.DATE,1);
holidays.setDayExcluded(laborDay, true); ②-1:排除的日期,若是设置为false则为包含
③国庆节
Calendar nationalDay = new GregorianCalendar();
nationalDay.add(Calendar.MONTH,10);
nationalDay.add(Calendar.DATE,1);
holidays.setDayExcluded(nationalDay, true);③-1:排除该日期
scheduler.addCalendar("holidays", holidays, false, false);④向Scheduler注册日历
Date runDate = TriggerUtils.getDateOf(0,0, 10, 1, 4);⑤4月1号 上午10点
JobDetail job = new JobDetail("job1", "group1", SimpleJob.class);
SimpleTrigger trigger = new SimpleTrigger("trigger1", "group1",
runDate,
null,
SimpleTrigger.REPEAT_INDEFINITELY,
60L * 60L * 1000L);
trigger.setCalendarName("holidays");⑥让Trigger应用指定的日历规则
scheduler.scheduleJob(job, trigger);
scheduler.start();
//实际应用中主线程不能中止,不然Scheduler得不到执行,此处从略
}
}
因为节日是每一年重复的,因此使用org.quartz.Calendar的AnnualCalendar实现类,经过②、③的代码,指定五一和国庆两个节日并经过AnnualCalendar#setDayExcluded(Calendar day, boolean exclude)方法添加这两个日期。exclude为true时表示排除指定的日期,若是为false时表示包含指定的日期。
在定制好org.quartz.Calendar后,还须要经过Scheduler#addCalendar(String calName, Calendar calendar, boolean replace, boolean updateTriggers)进行注册,若是updateTriggers为true,Scheduler中已引用Calendar的Trigger将获得更新,如④所示。
在⑥处,咱们让一个Trigger指定使用Scheduler中表明节日的Calendar,这样Trigger就会避开五一和国庆这两个特殊日子了。
任务调度信息存储
在默认状况下Quartz将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,由于内存中数据访问最快。不足之处是缺少数据的持久性,当程序路途中止或系统崩溃时,全部运行的信息都会丢失。
好比咱们但愿安排一个执行100次的任务,若是执行到50次时系统崩溃了,系统重启时任务的执行计数器将从0开始。在大多数实际的应用中,咱们每每并不须要保存任务调度的现场数据,由于不多须要规划一个指定执行次数的任务。
对于仅执行一次的任务来讲,其执行条件信息自己应该是已经持久化的业务数据(如锁定到期解锁任务,解锁的时间应该是业务数据),当执行完成后,条件信息也会相应改变。固然调度现场信息不只仅是记录运行次数,还包括调度规则、JobDataMap中的数据等等。
若是确实须要持久化任务调度信息,Quartz容许你经过调整其属性文件,将这些信息保存到数据库中。使用数据库保存任务调度信息后,即便系统崩溃后从新启动,任务的调度信息将获得恢复。如前面所说的例子,执行50次崩溃后从新运行,计数器将从51开始计数。使用了数据库保存信息的任务称为持久化任务。
经过配置文件调整任务调度信息的保存策略
其实Quartz JAR文件的org.quartz包下就包含了一个quartz.properties属性配置文件并提供了默认设置。若是须要调整默认配置,能够在类路径下创建一个新的quartz.properties,它将自动被Quartz加载并覆盖默认的设置。
先来了解一下Quartz的默认属性配置文件:
代码清单5 quartz.properties:默认配置
①集群的配置,这里不使用集群
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
②配置调度器的线程池
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
Quartz的属性配置文件主要包括三方面的信息:
1)集群信息;
2)调度器线程池;
3)任务调度现场数据的保存。
若是任务数目很大时,能够经过增大线程池的大小获得更好的性能。默认状况下,Quartz采用org.quartz.simpl.RAMJobStore保存任务的现场数据,顾名思义,信息保存在RAM内存中,咱们能够经过如下设置将任务调度现场数据保存到数据库中:
代码清单6 quartz.properties:使用数据库保存任务调度现场数据
…
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix = QRTZ_①数据表前缀
org.quartz.jobStore.dataSource = qzDS②数据源名称
③定义数据源的具体属性
org.quartz.dataSource.qzDS.driver = oracle.jdbc.driver.OracleDriver
org.quartz.dataSource.qzDS.URL = jdbc:oracle:thin:@localhost:1521:ora9i
org.quartz.dataSource.qzDS.user = stamen
org.quartz.dataSource.qzDS.password = abc
org.quartz.dataSource.qzDS.maxConnections = 10
要将任务调度数据保存到数据库中,就必须使用org.quartz.impl.jdbcjobstore.JobStoreTX代替原来的org.quartz.simpl.RAMJobStore并提供相应的数据库配置信息。首先①处指定了Quartz数据库表的前缀,在②处定义了一个数据源,在③处具体定义这个数据源的链接信息。
你必须事先在相应的数据库中建立Quartz的数据表(共8张),在Quartz的完整发布包的docs/dbTables目录下拥有对应不一样数据库的SQL脚本。
查询数据库中的运行信息
任务的现场保存对于上层的Quartz程序来讲是彻底透明的,咱们在src目录下编写一个如代码清单6所示的quartz.properties文件后,从新运行代码清单2或代码清单3的程序,在数据库表中将能够看到对应的持久化信息。当调度程序运行过程当中途中止后,任务调度的现场数据将记录在数据表中,在系统重启时就能够在此基础上继续进行任务的调度。
代码清单7 JDBCJobStoreRunner:从数据库中恢复任务的调度
package com.baobaotao.basic.quartz;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.impl.StdSchedulerFactory;
public class JDBCJobStoreRunner {
public static void main(String args[]) {
try {
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
Scheduler scheduler = schedulerFactory.getScheduler();
①获取调度器中全部的触发器组
String[] triggerGroups = scheduler.getTriggerGroupNames();
②从新恢复在tgroup1组中,名为trigger1_1触发器的运行
for (int i = 0; i < triggerGroups.length; i++) {
String[] triggers = scheduler.getTriggerNames(triggerGroups[i]);
for (int j = 0; j < triggers.length; j++) {
Trigger tg = scheduler.getTrigger(triggers[j],triggerGroups[i]);
if (tg instanceof SimpleTrigger
&& tg.getFullName().equals("tgroup1.trigger1_1")) {②-1:根据名称判断
②-1:恢复运行
scheduler.rescheduleJob(triggers[j], triggerGroups[i],tg);
}
}
}
scheduler.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
当代码清单2中的SimpleTriggerRunner执行到一段时间后非正常退出,咱们就能够经过这个JDBCJobStoreRunner根据记录在数据库中的现场数据恢复任务的调度。Scheduler中的全部Trigger以及JobDetail的运行信息都会保存在数据库中,这里咱们仅恢复tgroup1组中名称为trigger1_1的触发器,这能够经过如②-1所示的代码进行过滤,触发器的采用GROUP.TRIGGER_NAME的全名格式。经过Scheduler#rescheduleJob(String triggerName,String groupName,Trigger newTrigger)便可从新调度关联某个Trigger的任务。
下面咱们来观察一下不一样时期qrtz_simple_triggers表的数据:
1.运行代码清单2的SimpleTriggerRunner一小段时间后退出:
REPEAT_COUNT表示须要运行的总次数,而TIMES_TRIGGER表示已经运行的次数。
2.运行代码清单7的JDBCJobStoreRunner恢复trigger1_1的触发器,运行一段时间后退出,这时qrtz_simple_triggers中的数据以下:
首先Quartz会将原REPEAT_COUNT-TIMES_TRIGGER获得新的REPEAT_COUNT值,并记录已经运行的次数(从新从0开始计算)。
3.从新启动JDBCJobStoreRunner运行后,数据又将发生相应的变化:
4.继续运行直至完成全部剩余的次数,再次查询qrtz_simple_triggers表:
这时,该表中的记录已经变空。
值得注意的是,若是你使用JDBC保存任务调度数据时,当你运行代码清单2的SimpleTriggerRunner而后退出,当再次但愿运行SimpleTriggerRunner时,系统将抛出JobDetail重名的异常:
Unable to store Job with name: 'job1_1' and group: 'jGroup1', because one already exists with this identification.
由于每次调用Scheduler#scheduleJob()时,Quartz都会将JobDetail和Trigger的信息保存到数据库中,若是数据表中已经同名的JobDetail或Trigger,异常就产生了。
本文使用quartz 1.6版本,咱们发现当后台数据库使用MySql时,数据保存不成功,该错误是Quartz的一个Bug,相信会在高版本中获得修复。由于HSQLDB不支持SELECT * FROM TABLE_NAME FOR UPDATE的语法,因此不能使用HSQLDB数据库。
小结
Quartz提供了最为丰富的任务调度功能,不但能够制定周期性运行的任务调度方案,还可让你按照日历相关的方式进行任务调度。Quartz框架的重要组件包括Job、JobDetail、Trigger、Scheduler以及辅助性的JobDataMap和SchedulerContext。
Quartz拥有一个线程池,经过线程池为任务提供执行线程,你能够经过配置文件对线程池进行参数定制。Quartz的另外一个重要功能是可将任务调度信息持久化到数据库中,以便系统重启时可以恢复已经安排的任务。此外,Quartz还拥有完善的事件体系,容许你注册各类事件的监听器。
Quartz是一个开源的做业调度框架,它彻底由java写成,并设计用于J2SE和J2EE应用中。它提供了巨大的灵活性而不牺牲简单性。你可以用它来为执行一个做业而建立简单的或复杂的调度。它有不少特征,如:数据库支持,集群,插件,EJB做业预构建,JavaMail及其它,支持cron-like表达式等等。
本文内容
1. Quartz让任务调度简单
2. Quartz的发展史
3. 上手Quartz
4. Quartz内部架构
5. 做业
6. 做业管理和存储
7. 有效做业存储
8. 做业和触发器
9. 调度一个做业
10. 用调度器(Scheduler)调用你的做业
11. 编程调度同声明性调度
12. 有状态和无状态做业
13. Quartz框架的其余特征
14. Quartz下一步计划
15. 了解更多Quartz特征
你曾经须要应用执行一个任务吗?这个任务天天或每周星期二晚上11:30,或许仅仅每月的最后一天执行。一个自动执行而无须干预的任务在执行过程当中若是发生一个严重错误,应用可以知到其执行失败并尝试从新执行吗?你和你的团队是用java编程吗?若是这些问题中任何一个你回答是,那么你应该使用Quartz调度器。
旁注:Matrix目前就大量使用到了Quartz。好比,排名统计功能的实现,在Jmatrix里经过Quartz定义了一个定时调度做业,在天天凌晨一点,做业开始工做,从新统计你们的Karma和排名等。
还有,RSS文件的生成,也是经过Quartz定义做业,每隔半个小时生成一次RSS XML文件。
因此Quartz使用的地方不少,本文无疑是一篇很好的入门和进阶的文章,在此,感谢David w Johnson的努力!
Quartz让做业调度简单
Quartz是一个彻底由java编写的开源做业调度框架。不要让做业调度这个术语吓着你。尽管Quartz框架整合了许多额外功能, 但就其简易形式看,你会发现它易用得简直让人受不了!。简单地建立一个实现org.quartz.Job接口的java类。Job接口包含惟一的方法:
public void execute(JobExecutionContext context)
throws JobExecutionException;
在你的Job接口实现类里面,添加一些逻辑到execute()方法。一旦你配置好Job实现类并设定好调度时间表,Quartz将密切注意剩余时间。当调度程序肯定该是通知你的做业的时候,Quartz框架将调用你Job实现类(做业类)上的execute()方法并容许作它该作的事情。无需报告任何东西给调度器或调用任何特定的东西。仅仅执行任务和结束任务便可。若是配置你的做业在随后再次被调用,Quartz框架将在恰当的时间再次调用它。
若是你使用了其它流行的开源框架象struts,你会对Quartz的设计和部件感到温馨。虽然两个开源工程是解决彻底不一样的问题,仍是有不少类似的之处,就是开源软件用户天天感受很温馨。Quartz能用在单机J2SE应用中,做为一个RMI服务器,也能够用在web应用中,甚至也能够用在J2EE应用服务器中。
Quartz的发展史
尽管Quartz今年开始受到人们注意,但仍是暂时流行。Quartz由James House建立并最初于2001年春天被加入sourceforge工程。接下来的几年里,有许多新特征和版本出现,可是直到项目迁移到新的站点并成为OpenSymphony项目家族的一员,才开始真正启动并受到应有的关注。
James House仍然和几个协助他的业余开发者参与大量开发工做。Quartz开发团队今年能发布几个新版本,包括当前正处在候选发布阶段的1.5版。
上手Quartz
Quartz工程驻留在OpenSymphony站点上。在Quartz站点上能够找到许多有用的资源:JavaDocs,包含指南的文档,CVS访问,用户和开发者论坛的链接,固然也有下载。
从下载链接取得Quartz的发布版本,而且解压到到本地目录。这个下载文件包含了一个预先构建好的Quartz二进制文件(quartz.jar),你能够将它放进本身的应用中。Quartz框架只须要少数的第三方库,而且这些三方库是必需的,你极可能已经在使用这些库了。
你要把Quartz的安装目录的<quartz- install>/lib/core 和 <quartz-install>/lib/optional目录中的第三方库加进你本身的工程中。大多数第三方库是咱们所熟知和喜欢的标准Jakarta Commons库,像Commons Logging, Commons BeantUtils等等。
quartz.properties文件
Quartz有一个叫作quartz.properties的配置文件,它容许你修改框架运行时环境。缺省是使用Quartz.jar里面的quartz.properties文件。固然,你应该建立一个quartz.properties文件的副本而且把它放入你工程的classes目录中以便类装载器找到它。quartz.properties样本文件如例1所示。
例1.quartz.properties文件容许修改Quartz运行环境:
#===============================================================
# Configure Main Scheduler Properties
#===============================================================
org.quartz.scheduler.instanceName = QuartzScheduler
org.quartz.scheduler.instanceId = AUTO
#===============================================================
# Configure ThreadPool
#===============================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
#===============================================================
# Configure JobStore
#===============================================================
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
一旦将Quartz.jar文件和第三方库加到本身的工程里面而且quartz.properties文件在工程的classes目录中,就能够建立做业了。然而,在作这以前,咱们暂且回避一下先简短讨论一下Quartz架构。
Quartz内部架构
在规模方面,Quartz跟大多数开源框架相似。大约有300个java类和接口,并被组织到12个包中。这能够和Apache Struts把大约325个类和接口以及组织到11个包中相比。尽管规模几乎不会用来做为衡量框架质量的一个特性,但这里的关键是quarts内含不少功能,这些功能和特性集是否成为、或者应该成为评判一个开源或非开源框架质量的因素。
Quartz调度器
Quartz框架的核心是调度器。调度器负责管理Quartz应用运行时环境。调度器不是靠本身作全部的工做,而是依赖框架内一些很是重要的部件。Quartz不只仅是线程和线程管理。为确保可伸缩性,Quartz采用了基于多线程的架构。启动时,框架初始化一套worker线程,这套线程被调度器用来执行预约的做业。这就是Quartz怎样能并发运行多个做业的原理。Quartz依赖一套松耦合的线程池管理部件来管理线程环境。本片文障中,咱们会屡次提到线程池管理,但Quartz里面的每一个对象是可配置的或者是可定制的。因此,例如,若是你想要插进本身线程池管理设施,我猜你必定能!
做业
用Quartz的行话讲,做业是一个执行任务的简单java类。任务能够是任何java代码。只需你实现org.quartz.Job接口而且在出现严重错误状况下抛出JobExecutionException异常便可。Job接口包含惟一的一个方法execute(),做业从这里开始执行。一旦实现了Job接口和execute()方法,当Quartz肯定该是做业运行的时候,它将调用你的做业。Execute()方法内就彻底是你要作的事情。下面有一些你要在做业里面作事情的例子:
· 用JavaMail(或者用其余的像Commons Net同样的邮件框架)发送邮件
· 建立远程接口而且调用在EJB上的方法
· 获取Hibernate Session,查询和更新关系数据库里的数据
· 使用OSWorkflow而且从做业调用一个工做流
· 使用FTP和处处移动文件
· 调用Ant构建脚本开始预约构建
这种可能性是无穷的,正事这种无限可能性使得框架功能如此强大。Quartz给你提供了一个机制来创建具备不一样粒度的、可重复的调度表,因而,你只需建立一个java类,这个类被调用而执行任务。
做业管理和存储
做业一旦被调度,调度器须要记住而且跟踪做业和它们的执行次数。若是你的做业是30分钟后或每30秒调用,这不是颇有用。事实上,做业执行须要很是准确和即时调用在被调度做业上的execute()方法。Quartz经过一个称之为做业存储(JobStore)的概念来作做业存储和管理。
有效做业存储
Quartz提供两种基本做业存储类型。第一种类型叫作RAMJobStore,它利用一般的内存来持久化调度程序信息。这种做业存储类型最容易配置、构造和运行。对许多应用来讲,这种做业存储已经足够了。然而,由于调度程序信息是存储在被分配给JVM的内存里面,因此,当应用程序中止运行时,全部调度信息将被丢失。若是你须要在从新启动之间持久化调度信息,则将须要第二种类型的做业存储。
第二种类型的做业存储实际上提供两种不一样的实现,但两种实现通常都称为JDBC做业存储。两种JDBC做业存储都须要JDBC驱动程序和后台数据库来持久化调度程序信息。这两种类型的不一样在于你是否想要控制数据库事务或这释放控制给应用服务器例如BEA's WebLogic或Jboss。(这相似于J2EE领域中,Bean管理的事务和和容器管理事务之间的区别)
这两种JDBC做业存储是:
· JobStoreTX:当你想要控制事务或工做在非应用服务器环境中是使用
· JobStoreCMT:当你工做在应用服务器环境中和想要容器控制事务时使用。
JDBC做业存储为须要调度程序维护调度信息的用户而设计。
做业和触发器
Quartz设计者作了一个设计选择来从调度分离开做业。Quartz中的触发器用来告诉调度程序做业何时触发。框架提供了一把触发器类型,但两个最经常使用的是SimpleTrigger和CronTrigger。SimpleTrigger为须要简单打火调度而设计。典型地,若是你须要在给定的时间和重复次数或者两次打火之间等待的秒数打火一个做业,那么SimpleTrigger适合你。另外一方面,若是你有许多复杂的做业调度,那么或许须要CronTrigger。
CronTrigger是基于Calendar-like调度的。当你须要在除星期六和星期天外的天天上午10点半执行做业时,那么应该使用CronTrigger。正如它的名字所暗示的那样,CronTrigger是基于Unix克隆表达式的。
做为一个例子,下面的Quartz克隆表达式将在星期一到星期五的天天上午10点15分执行一个做业。
0 15 10 ? * MON-FRI
下面的表达式
0 15 10 ? * 6L 2002-2005
将在2002年到2005年的每月的最后一个星期五上午10点15分执行做业。
你不可能用SimpleTrigger来作这些事情。你能够用二者之中的任何一个,但哪一个跟合适则取决于你的调度须要。
调度一个做业
让咱们经过看一个例子来进入实际讨论。现假定你管理一个部门,不管什么时候候客户在它的FTP服务器上存储一个文件,都得用电子邮件通知它。咱们的做业将用FTP登录到远程服务器并下载全部找到的文件。而后,它将发送一封含有找到和下载的文件数量的电子邮件。这个做业很容易就帮助人们成天从手工执行这个任务中解脱出来,甚至连晚上都无须考虑。咱们能够设置做业循环不断地每60秒检查一次,并且工做在7×24模式下。这就是Quartz框架彻底的用途。
首先建立一个Job类,将执行FTP和Email逻辑。下例展现了Quartz的Job类,它实现了org.quartz.Job接口。
例2.从FTP站点下载文件和发送email的Quartz做业
public class ScanFTPSiteJob implements Job {
private static Log logger = LogFactory.getLog(ScanFTPSiteJob.class);
/*
* Called the scheduler framework at the right time
*/
public void execute(JobExecutionContext context)
throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDataMap();
try {
// Check the ftp site for files
File[] files = JobUtil.checkForFiles(jobDataMap);
JobUtil.sendEmail(jobDataMap, files);
} catch (Exception ex) {
throw new JobExecutionException(ex.getMessage());
}
}
}
咱们故意让ScanFTPSiteJob保持很简单。咱们为这个例子建立了一个叫作JobUtil的实用类。它不是Quartz的组成部分,但对构建各类做业能重用的实用程序库来讲是有意义的。咱们能够轻易将那种代码组织进做业类中,quarts 调度器同样好用,由于咱们一直在使用quarts,因此那些代码可继续重用。
JobUtil.checkForFiles() and JobUtil.sendEmail()方法使用的参数是Quartz建立的JobDataMap的实例。实例为每一个做业的执行而建立,它是向做业类传递配置参数的方法。
这里并无展现JobUtil的实现,但咱们能用Jakarta上的Commons Net轻易地实现FTP和Email功能。
用调度器调用做业
首先建立一个做业,但为使做业能被调度器调用,你得向调度程序说明你的做业的调用时间和频率。这个事情由与做业相关的触发器来完成。由于咱们仅仅对大约每60秒循环调用做业感兴趣,因此打算使用SimpleTrigger。
做业和触发器经过Quartz调度器接口而被调度。咱们须要从调度器工厂类取得一个调度器的实例。最容易的办法是调用StdSchedulerFactory这个类上的静态方法getDefaultScheduler()。
使用Quartz框架,你须要调用start()方法来启动调度器。例3的代码遵循了大多数Quartz应用的通常模式:建立一个或多个做业,建立和设置触发器,用调度器调度做业和触发器,启动调度器。
例3.Quartz做业经过Quartz调度器而被调度
public class MyQuartzServer {
public static void main(String[] args) {
MyQuartzServer server = new MyQuartzServer();
try {
server.startScheduler();
} catch (SchedulerException ex) {
ex.printStackTrace();
}
}
protected void startScheduler() throws SchedulerException {
// Use the factory to create a Scheduler instance
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
// JobDetail holds the definition for Jobs
JobDetail jobDetail =
new JobDetail("ScanFTPJob", Scheduler.DEFAULT_GROUP,
ScanFTPSiteJob.class);
// Store job parameters to be used within execute()
jobDetail.getJobDataMap().put(
"FTP_HOST",
"//home//cavaness//inbound");
// Other neccessary Job parameters here
// Create a Trigger that fires every 60 seconds
Trigger trigger = TriggerUtils.makeSecondlyTrigger(60);
// Setup the Job and Trigger with the Scheduler
scheduler.scheduleJob(jobDetail, trigger );
// Start the Scheduler running
scheduler.start();
}
}
编程调度同声明性调度
例3中,咱们经过编程的方法调度咱们的ScanFTPSiteJob做业。就是说,咱们用java代码来设置做业和触发器。Quartz框架也支持在xml文件里面申明性的设置做业调度。申明性方法容许咱们更快速地修改哪一个做业何时被执行。
Quartz框架有一个插件,这个插件负责读取xml配置文件。xml配置文件包含了关于启动Quartz应用的做业和触发器信息。全部xml文件中的做业连同相关的触发器都被加进调度器。你仍然须要编写做业类,但配置那些做业类的调度器则很是动态化。例4展现了一个用申明性方式执行与例3代码相同的逻辑的xml配置文件。
例4.能使用xml文件调度的做业
ScanFTPSiteJob
DEFAULT
A job that scans an ftp site for files
ScanFTPSiteJob
FTP_HOST
/home/cavaness/inbound
ScanFTPSiteJobTrigger
DEFAULT
ScanFTPSiteJob
DEFAULT
2005-09-11 6:10:00 PM
-1
60000
你能够将xml文件中的元素跟例3代码做个比较,它们从概念上来看是相同的。使用例4式的申明性方法的好处是维护变得极其简单,只需改变xml配置文件和从新启动Quartz应用便可。无须修改代码,无须从新编译,无须从新部署。
有状态和无状态做业
在本文中你所看到的做业到是无状态的。这意味着在两次做业执行之间,不会去维护做业执行时JobDataMap的状态改变。若是你须要能增、删,改JobDataMap的值,并且能让做业在下次执行时能看到这个状态改变,则须要用Quartz有状态做业。
若是你是一个有经验的EJB开发者的话,深信你会当即退缩,由于有状态带有负面含义。这主要是因为EJB带来的伸缩性问题。Quartz有状态做业实现了org.quartz.StatefulJob接口。无状态和有状态做业的关键不一样是有状态做业在每次执行时只有一个实例。大多数状况下,有状态的做业不回带来大的问题。然而,若是你有一个须要频繁执行的做业或者须要很长时间才能完成的做业,那么有状态做业可能给你带来伸缩性问题。
Quartz框架的其余特征
Quartz框架有一个丰富的特征集。事实上,quarts有太多特性以至不能在一种状况中所有领会,下面列出了一些有意思的特征,但没时间在此详细讨论。
监听器和插件
每一个人都喜欢监听和插件。今天,几乎下载任何开源框架,你一定会发现支持这两个概念。监听是你建立的java类,当关键事件发生时会收到框架的回调。例如,当一个做业被调度、没有调度或触发器终止和再也不打火时,这些均可以经过设置来来通知你的监听器。Quartz框架包含了调度器监听、做业和触发器监听。你能够配置做业和触发器监听为全局监听或者是特定于做业和触发器的监听。
一旦你的一个具体监听被调用,你就能使用这个技术来作一些你想要在监听类里面作的事情。例如,你若是想要在每次做业完成时发送一个电子邮件,你能够将这个逻辑写进做业里面,也能够JobListener里面。写进JobListener的方式强制使用松耦合有利于设计上作到更好。
Quartz插件是一个新的功能特性,无须修改Quartz源码即可被建立和添加进Quartz框架。他为想要扩展Quartz框架又没有时间提交改变给Quartz开发团队和等待新版本的开发人员而设计。若是你熟悉Struts插件的话,那么彻底能够理解Quartz插件的使用。
与其Quartz提供一个不能知足你须要的有限扩展点,还不如经过使用插件来拥有可修整的扩展点。
集群Quartz应用
Quartz应用能被集群,是水平集群仍是垂直集群取决于你本身的须要。集群提供如下好处:
· 伸缩性
· 搞可用性
· 负载均衡
目前,Quartz只能借助关系数据库和JDBC做业存储支持集群。未来的版本这个制约将消失而且用RAMJobStore集群将是可能的并且将不须要数据库的支持。
Quartz web应用
使用框架几个星期或几个月后,Quartz用户所显示的需求之一是须要集成Quartz到图形用户界面中。目前Quartz框架已经有一些工具容许你使用Java servlet来初始化和启动Quartz。一旦你能够访问调度器实例,你就能够把它存储在web容器的servlet上下文中(ServletContext中)而且能够经过调度器接口管理调度环境。
幸运的是一些开发者已正影响着单机Quartz web应用,它用来更好地管理调度器环境。构建在若干个流行开源框架如Struts和Spring之上的图形用户界面支持不少功能,这些功能都被包装进一个简单接口。GUI的一个画面如图1所示:
图1.Quartz Web应用容许比较容易地管理Quartz环境。
Quartz的下一步计划
Quartz是一个活动中的工程。Quartz开发团队明确表示不会停留在已有的荣誉上。Quartz下一个主要版本已经在启动中。你能够在OpenSymphony的 wiki上体验一下Quartz 2.0的设计和特征。
总之,Quartz用户天天都自由地添加特性建议和设计创意以便能被核心框架考虑(看重)。
了解更多Quartz特征
当你开始使用Quartz框架的更多特性时,User and Developer Forum论坛变成一个回答问题和跟其余Quartz用户沟通的极其有用的资源。常常去逛逛这个论坛时颇有好处的,你也能够依靠James House来共享与你的须要相关的知识和意见。