一:简单使用
Quartz是一个开源的任务调度服务,它可以独立使用,也可与其它的Java EE,Java SE应用整合使用。无论是执行十个,一百个工作的简单任务,还是执行成百上千个工作的复杂调度任务都可以使用Quartz来实现,此外,Quartz还提供了很多企业级应用的特色,如JTA事务支持及集群。
如果你的应用中有任务需要在特定的时间执行,亦或周期循环执行,则Quartz也许是你理想的选择。
Quartz的使用
作为一个Javaer,自然知道第一步是下载Quartz的jar包,Quartz API中一些关键的接口有:
- Scheduler – 这是与调度器交互的主要API,通过它来调度任务的执行。
- Job – 你的任务需要实现的接口,执行调度任务时,Scheduler会执行此接口的execute方法;
- JobDetail – 定义Jobs的实例,里面包含了Job的所有信息:名称、所属的组,Job的类等;
- Trigger – 任务的触发器,由它来定义计划,规定Job何时被触发执行。
因此,可以这么理解,在我们应用Quartz时,完成具体工作的组件(ExportJob)需要实现Job接口,并在execute方法中调用具体的业务逻辑代码。而对于Quartz来说,每个Job的实例由JobDetail来描述,里面包括Job的实现类(ExportJob.class)等属性,Trigger则与调度器关联,决定何时触发Job的执行。最后,由Scheduler来管理所有的Jobs与Trigger。
Scheduler的生命周期从它被创建开始,一直到调用其shutdown()方法结束。一旦创建以后,便可通过Scheduler接口执行添加、删除、显示Jobs和Triggers,此外,还可执行其它相关的调度操作,如暂停Trigger。但是,所有Triggers相关的操作都需要在执行start()方法以后才能进行。
一个简单的实例如:
1. 执行业务逻辑的Job
public class SimpleJob implements Job{ public void execute(JobExecutionContext paramJobExecutionContext) throws JobExecutionException { System.out.println("在后台解析日志信息"); System.out.println("开始!"); //…… System.out.println("结束!"); } }
2. 任务调度
public class QuartzSimpleExample { public static void main(String args[]) { try { // 创建Scheduler Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); // 创建Job的实例 JobDetail jobIns = JobBuilder.newJob(SimpleJob.class).withIdentity("simpleJob", "group1").build(); // 创建Trigger SimpleScheduleBuilder buider = SimpleScheduleBuilder .simpleSchedule().withIntervalInMinutes(1).repeatForever(); Trigger trigger = TriggerBuilder.newTrigger().withIdentity( "simpleTrigger", "group1").startNow().withSchedule( buider).build(); //调度执行 scheduler.scheduleJob(jobIns, trigger); try { Thread.sleep(60000); } catch (InterruptedException e) { e.printStackTrace(); } scheduler.shutdown(); } catch (SchedulerException e) { e.printStackTrace(); } } }
如果你也看了Quartz的参考文档,可能会发现有些不一样,文档上面是简化的,这里加了一些东西,才能正常运行,特别是文档上坑新人,通过静态import,直接就newJob****。
二:深入理解Trigger和cron
Quartz提供了几个Trigger类型供用户使用,对应的实现可以在org.quartz.impl.triggers中看到,一般情况下,用户用得最多的是SimpleTrigger与CronTrigger。对于最简单的情况,如只需Job在某一个特定的时间执行一次,亦或在某个时间之后按一定的时间间隔重复执行,则可使用简单的SimpleTrigger,至于CronTrigger,如果你了解cron表达式(对于linux牛,应该知道,因为linux的定时任务cron也是如此这般),便知道它几乎可以适应所有复杂的调度情况。
对于SimpleTrigger,可通过SimpleScheduleBuilder来创建,在前面的示例中已经用到,不过SimpleScheduleBuilder里还有很多方法没有使用到,可以自己看看,了解自己可以设置哪些属性。
CronTrigger的魔力在于cron表达式,cron表达式是一个由7个子表达式组成的字符串,各个子表达式通过空白符分隔,分别描述了调度的详细信息。
- 秒
- 分
- 小时
- 每月的第几天
- 月
- 每周的第几天
- 年(可选的)
如”0 0 12 ? * WED”,表示每个礼拜三的12点。
子表达式中可以包含范围或列表,还可以同时包含两者,如上面例子中,WED可以是MON-FRI, MON,WED,FRI, 或MON-WED,SAT。
通配符”*”表示这个域的所有可能取值,如上面月部分用了*表示每个月,同理,如果在“每周的第几天”用*则表示每个礼拜的任意一天。
字符“/”可以用来指定增长值,如你在”分钟”中指定”0/15”则表示从零开始,每隔15分钟,”3/20”则是从第三分钟开始,每隔20分钟,这跟直接指定”3,23,43”意思是一样的。
字符”?”可以在“每月的第几天”和“每周的第几天”中使用,表示不指定任何的值。当你需要指定这两个子表达式中一个的值,而不指定另一个的值时,它便派上用处了。(其实想想,如果同时指定两个值貌似有点那个=。=)
字符”L”同样只可以在“每月第几天”和“每周第几天”中使用,L是”last”的缩写,因为表示的是最后,所以它在这两个子表达式中的含义也是变化的,如果是一月则表示31号,2月则表示28号,如何与“每周第几天”搭配,则表示礼拜天,等等。
字符”#”用于表示每个月的第几个礼拜几,如”6#3”或”FRI#3”表示每个月的第三个礼拜五。
下面是一些相关的例子:
“0 0/5 * * * ?” : 每隔5分钟触发一次
“10 0/5 * * * ?” : 从每分钟的第10秒开始,每隔五分钟触发
“0 30 10-13 ? * WED,FRI” : 每个周三,周五的10:30,11:30,12:30和13:30触发
了解了cron表达式,接下来看看如何创建CronTrigger?
创建SimpleTrigger时使用了SimpleScheduleBuilder,自然创建CronTrigger将使用CronScheduleBuilder,你可以自己编写cron表达式,然后通过cronSchedule()函数来创建,如果CronScheduleBuilder提供的一些调度函数(如dailyAtHourAndMinute)正好能满足你的需求,也可是直接使用,免去编写cron表达式的需要。
示例代码:
public class QuartzCronExample { public static void main(String[] args) { try { // 创建Scheduler Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); // 创建Job的实例 JobDetail jobIns = JobBuilder.newJob(SimpleJob.class).withIdentity("simpleJob", "group1").build(); // 创建Trigger CronScheduleBuilder builder = CronScheduleBuilder.cronSchedule("0 0/1 * * * ?"); /** builder = CronScheduleBuilder.dailyAtHourAndMinute(12, 30); **/ Trigger trigger = TriggerBuilder.newTrigger().withIdentity( "simpleTrigger", "group1").startNow().withSchedule( builder).build(); //调度执行 scheduler.scheduleJob(jobIns, trigger); try { Thread.sleep(600000); } catch (InterruptedException e) { e.printStackTrace(); } scheduler.shutdown(); } catch (SchedulerException e) { e.printStackTrace(); } } }
有点坑,跟上一篇的示例是一样的,除了CronScheduleBuilder这个点。
(1) Scheduler
在这之前,我们创建Scheduler都是通过下面的语句:
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
这使用的是默认的调度器,它会依据quartz.properties配置文件为我们设置好相关的属性,包括使用的调度器、线程池、线程池中线程数、采用的JobStore等等。如果我们在自己应用的classpath中写了quartz.properties,则SchedulerFactory会使用它的配置,否则它会使用Quartz的jar包中的配置文件。
Jar包中提供的quartz.properties如:
# Default Properties file for use by StdSchedulerFactory # to create a Quartz Scheduler Instance, if a different # properties file is not explicitly specified. # 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.misfireThreshold: 60000 org.quartz.jobStore.class: org.quartz.simpl.RAMJobStore
通过查看前面部分的示例程序,查看其输出的日志信息,也可说明这一点:
288 [main] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor 293 [main] INFO org.quartz.simpl.SimpleThreadPool - Job execution threads will use class loader of thread: main 345 [main] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl 347 [main] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.1.7 created. 358 [main] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized. 359 [main] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.1.7) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED' Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally. NOT STARTED. Currently in standby mode. Number of jobs executed: 0 Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads. Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered. 359 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties' 360 [main] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.1.7 362 [main] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.
如果要使用特殊的配置文件(如命名不同等),可通过StdSchedulerFactory的initialize()函数进行初始化,如:
StdSchedulerFactory factory = new StdSchedulerFactory(); factory.initialize("jmatrix_quartz.properties"); Scheduler scheduler = factory.getScheduler();
(2) 事件机制
Quartz为Job,Trigger和Scheduler都提供了对应的事件接口,它们分别为:
public abstract interface JobListener { public abstract String getName(); public abstract void jobToBeExecuted(JobExecutionContext paramJobExecutionContext); public abstract void jobExecutionVetoed(JobExecutionContext paramJobExecutionContext); public abstract void jobWasExecuted(JobExecutionContext paramJobExecutionContext, JobExecutionException paramJobExecutionException); }
可监听的事件有,Job开始执行、执行完成、停止。
public abstract interface TriggerListener { public abstract String getName(); public abstract void triggerFired(Trigger paramTrigger, JobExecutionContext paramJobExecutionContext); public abstract boolean vetoJobExecution(Trigger paramTrigger, JobExecutionContext paramJobExecutionContext); public abstract void triggerMisfired(Trigger paramTrigger); public abstract void triggerComplete(Trigger paramTrigger, JobExecutionContext paramJobExecutionContext, Trigger.CompletedExecutionInstruction paramCompletedExecutionInstruction); }
可监听的事件有:trigger触发,trigger错过执行,trigger执行完成和停止。
public abstract interface SchedulerListener { public abstract void jobScheduled(Trigger paramTrigger); public abstract void jobUnscheduled(TriggerKey paramTriggerKey); public abstract void triggerFinalized(Trigger paramTrigger); public abstract void triggerPaused(TriggerKey paramTriggerKey); public abstract void triggersPaused(String paramString); public abstract void triggerResumed(TriggerKey paramTriggerKey); public abstract void triggersResumed(String paramString); public abstract void jobAdded(JobDetail paramJobDetail); public abstract void jobDeleted(JobKey paramJobKey); public abstract void jobPaused(JobKey paramJobKey); public abstract void jobsPaused(String paramString); public abstract void jobResumed(JobKey paramJobKey); public abstract void jobsResumed(String paramString); public abstract void schedulerError(String paramString, SchedulerException paramSchedulerException); public abstract void schedulerInStandbyMode(); public abstract void schedulerStarted(); public abstract void schedulerShutdown(); public abstract void schedulerShuttingdown(); public abstract void schedulingDataCleared(); }
Scheduler可监听的事件就多了:Job/Trigger的添加与移除、Scheduler错误的发生、Scheduler的关闭等等。
想要创建一个自己的监听器,只需创建一个类并让它实现对应的Listener接口即可,不过由于直接实现接口需要实现所有的接口方法,所以更推荐让自己的监听器类继承各个Listener接口对应的Support类:JobListenerSupport,TriggerListenerSupport和SchedulerListenerSupport,这些Support类提供所有接口方法的默认实现(即不做任何事情),因此我们创建的监听器只需实现自己感兴趣的事件相关方法即可。
一个简单的listener示例:
public class SimpleJobListener extends JobListenerSupport { public String getName() { return SimpleJobListener.class.getName(); } public void jobToBeExecuted(JobExecutionContext context) { System.out.println("开始执行SimpleJob"); } public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { System.out.println("SimpleJob执行完成"); } }
创建好自己的listener后,我们需要将它添加到Schedule中,这个可以通过ListenerManager的addJobListener()函数进行:
// 创建Scheduler Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler(); scheduler.start(); //添加Listener SimpleJobListener listener = new SimpleJobListener(); scheduler.getListenerManager().addJobListener(listener, KeyMatcher.keyEquals(JobKey.jobKey("simpleJob","group1")));
需要注意的是,这里addJobListener()函数的第二个参数是Matcher接口,通过它来决定此Listener是与那个Job,那个group,亦或所有的Job关联,上面的例子是直接关联到simpleJob。关于Quartz提供的一系列Matcher实现,可以查看org.quartz.impl.matchers包。
四:深入Job和JobDetail
在前面部分,我们知道Job中定义了实际的业务逻辑,而JobDetail包含Job相关的配置信息。在Quartz中,每次Scheduler执行Job时,在调用其execute()方法之前,它需要先根据JobDetail提供的Job类型创建一个Job class的实例,在任务执行完以后,Job class的实例会被丢弃,Jvm的垃圾回收器会将它们回收。
因此编写Job的具体实现时,需要注意:(1) 它必须具有一个无参数的构造函数;(2) 它不应该有静态数据类型,因为每次Job执行完以后便被回收,因此在多次执行时静态数据没法被维护。
Keep moving,在JobDetail中有这么一个成员JobDataMap,JobDataMap是Java Map接口的具体实现,并添加了一些便利的方法用于存储与读取原生类型数据,里面包含了当Job实例运行时,你希望提供给它的所有数据对象。
可以借助JobDataMap为Job实例提供属性/配置,可以通过它来追踪Job的执行状态等等。对于第一种情况,可以在创建Job时,添加JobDataMap数据,在Job的execute()中获取数据,第二种,则可以在Listener中通过获取JobDataMap中存储的状态数据追踪Job的执行状态。
按例,一个简单的例子:
// 创建Job的实例 JobDetail jobIns = JobBuilder.newJob(SimpleJob.class).withIdentity( "simpleJob", "group1").usingJobData("domain", "www.jmatrix.org").usingJobData("rank", "求别提~~~").build();
Job实现:
public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("开始!"); //……JobDataMap JobDataMap dataMap = context.getJobDetail().getJobDataMap(); System.out.println("域名 : "+dataMap.getString("domain")); System.out.println("排名 : "+dataMap.getString("rank")); System.out.println("结束!"); }
完成了这些工作,还需决定如何存储Job的数据,Quartz提供了JobStore接口来做这件事,如果你决定将Job数据保存在内存中,则可以使用RAMJobStore,它的优点是速度快,缺点是一旦机器挂了,Job相关的数据也丢失了,如果要采用数据库来存储Job数据,可以使用JobStoreTX或JobStoreCMT,这取决于你采用的事务管理方式,使用RAMJobStore的话配置很简单,只需配置org.quartz.jobStore.class即可,如果使用数据库存储,则还需要配置driverDelegate,tablePrefix及dataSource,driverDelegate一般情况下使用StdJDBCDelegate(MySQL便可使用这个),特殊的可以使用Quartz提供的相关delegate,请查看jar包,一般命名就说明了一切。TablePrefix是你的数据库表前缀,创建数据库的sql文件可以在docs\dbTables目录下找到。最后的数据源dataSource就有点麻烦,Quartz为用户提供了三种创建dataSource的方式:
- 配置相关的数据库属性(driverClass,url,username,password等),让Quartz为你创建dataSource。
- 通过jndi使用你应用服务器管理的dataSource。
- 通过实现org.quartz.utils.ConnectionProvider定制自己的datasource。
前面两种都是依据datasource的名称为其配置相关的属性,具体有哪些属性可直接参考Quartz的文档。
下面说说最后一种,我们需要配置:
org.quartz.dataSource.dbcpDS.connectionProvider.class=cn.ds.vertical.core.service.mail.DbcpConnectionProvider
这样Quartz才能找到自定义的ConnectionProvider实现,DbcpConnectionProvider是我模仿Quartz提供的默认ConnectionProvider实现实现的,只是提供一种思路,Quartz使用的c3p0,而DbcpConnectionProvider使用DBCP作为DataSource实现。
public class DbcpConnectionProvider implements ConnectionProvider { private BasicDataSource basicDataSource; public DbcpConnectionProvider(){ initialize(); } public Connection getConnection() throws SQLException { return this.basicDataSource.getConnection(); } public void shutdown() throws SQLException { this.basicDataSource.close(); } private void initialize(){ String driverClassName = "com.mysql.jdbc.Driver"; String url = "jdbc:mysql://localhost:3306/quartz"; String username = "root"; String password = "123456"; this.basicDataSource = new BasicDataSource(); this.basicDataSource.setDriverClassName(driverClassName); this.basicDataSource.setUrl(url); this.basicDataSource.setUsername(username); this.basicDataSource.setPassword(password); this.basicDataSource.setTestWhileIdle(true); } }
深入trigger及cron表达式