基于Spring的任务调度(4)
当写Cron表达式时,需要记住的最后一件事是夏时制的时间变化。由于夏时制的变化可能引起触发器在秋天时在Spring中被触发两次或从不触发。 实际上Cron表达式要比我们这里所讨论的有更多变化。你可以在CronTrigger类的Java文档中找到关于cron语法的更多介绍。例19展示了一个使用CronTrigger类的示例。
例19 使用CronTrigger类
package cn.hurraysoft.quartz; import java.text.ParseException; import java.util.Date; import java.util.Map; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; public class CronTriggerTest { public static void main(String[] args) throws SchedulerException, ParseException { Scheduler scheduler=new StdSchedulerFactory().getScheduler(); scheduler.start(); JobDetail jobDetail=new JobDetail("MessageJob",Scheduler.DEFAULT_GROUP,MessageJob.class); Map jobDetailMap=jobDetail.getJobDataMap(); jobDetailMap.put("message", "This is a message from Quartz"); jobDetailMap.put("jobDetailMessage", "A jobDetail message"); String cronExpression="3/5 * 20,21,22,23 * * ?"; Trigger trigger=new CronTrigger("cronTrigger",Scheduler.DEFAULT_GROUP,cronExpression); trigger.getJobDataMap().put("message","Message Form Trigger"); trigger.getJobDataMap().put("triggerMessage","Anther Trigger Message"); scheduler.scheduleJob(jobDetail, trigger); } }
对你来说上面的许多代码应该不陌生了。唯一的重要不同是我们使用Cron表达式。CronTrigger类的创建和SimpleTrigger的创建是非常相似的,你必须提供一个任务名和组名。为了帮助你理解示例中的Cron表达式,我们将它分解为几个部分。
第一个部分3/5,意思是每分钟的第3s作为开始每5s运行一次。第二个部分*代表每分钟。第三部分,14,15,16,17,限制触发器只能够运行在14:00到17:59之间--也就是时间必须以14、15、16或17开头。接下来的部分都是通配符--?,表示触发器可以运行在一个星期的任意一天。这个表达式将使得触发器14:00到17:59之间每1 min的第3 s开始运行,并每5 s运行一次。 如果你运行该示例,根据你的运行时间,你要么看到一个空白的屏幕要么打印出一直增长的"Hello World!"消息。试着修改表达式的第一个部分来改变频率或每分钟触发器开始的时间。你也应该试着修改其他部分看看产生的效果。
CronTrigger类对于所有的触发器需求来说都是很好的。但是在你需要考虑一些例外情况时,表达式可能会变得过于复杂。例如,考虑每个周一、周三和周五的11:00和15:00都有个进程会检查一个用户的任务列表。现在考虑一下用户放假时,你想阻止触发器被触发会发生的事情。幸运的是,Quartz使用Calendar接口对这种需求提供了支持。你使用Calendar接口可以精确的从一个触发器的通常调度计划中显式地包含或者排除某一段时间。Quartz包含了6个Calendar接口的实现,其中一个是HolidayCalendar类,它从一个触发器的调度计划中排除日期。例20展示了对于先前示例的修改,它使用HolidayCalendar排除2007年12月25日。
例20 使用HolidayCalendar显式排除日期
package cn.hurraysoft.quartz; import java.text.ParseException; import java.util.Calendar; import java.util.Date; import java.util.Map; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; import org.quartz.impl.calendar.HolidayCalendar; public class CronTriggerTest { public static void main(String[] args) throws SchedulerException, ParseException { Calendar cal=Calendar.getInstance(); cal.set(2010, cal.MARCH,7); HolidayCalendar calendar=new HolidayCalendar(); calendar.addExcludedDate(cal.getTime()); Scheduler scheduler=new StdSchedulerFactory().getScheduler(); scheduler.start(); scheduler.addCalendar("xmasCalendar",calendar, true, false); JobDetail jobDetail=new JobDetail("MessageJob",Scheduler.DEFAULT_GROUP,MessageJob.class); Map jobDetailMap=jobDetail.getJobDataMap(); jobDetailMap.put("message", "This is a message from Quartz"); jobDetailMap.put("jobDetailMessage", "A jobDetail message"); String cronExpression="3/5 * 20,21,22,23 * * ?"; Trigger trigger=new CronTrigger("cronTrigger",Scheduler.DEFAULT_GROUP,cronExpression); trigger.getJobDataMap().put("message","Message Form Trigger"); trigger.getJobDataMap().put("triggerMessage","Anther Trigger Message"); trigger.setCalendarName("xmasCalendar"); scheduler.scheduleJob(jobDetail, trigger); } }
从上面代码中,你可以看到我们创建了一个HolidayCalendar的实例,然后使用addExcludedDate()方法排除了12月25日。我们使用创建好的Calendar实例的addCalendar()方法将Calendar加到Scheduler中,并给它一个名字:xmasCalendar。接着,在加入CronTrigger之前,我们将它和xmasCalendar关联起来。使用这个方法可以将你从创建复杂的Cron表达式中解放出来,而这些表达式只是排除了一些日期。
4.)关于任务持久化
Quartz为任务持久化提供了支持,这可以让你在运行时增加任务或者对现存的任务进行修改,并为后续任务的执行持久化这些变更和增加的部分。中心概念就是JobStore接口,Quartz执行持久化时将使用它的实现。默认情况下,Quartz使用RAMJobStore实现,它简单的把任务放在内存中。其他可用实现是JobStoreCMT和JobStoreTX。这两个类都使用一个配置好的数据源来持久化任务细节,这就支持将任务的创建和修改作为事务的一部分。JobStoreCMT实现倾向于用在一个服务器环境里并参加容器管理事务。对于独立的程序,你应该使用JobStoreTX实现。Spring为JobStore提供了自己的LocalDataSourceJobStore实现,它可以参加Spring管理的事务。当我们讨论Spring对Quartz支持时我们会关注该实现。
从前面开始,你了解了如何修改JobDataMap的内容来在同一个任务的不同执行中传递消息。但是,如果你希望使用JobStore而不是RAMJobStore来运行该示例,你将发现它不能工作。原因是Quartz支持无状态和有状态的任务。在使用RAMJobStore并修改JobDataMap时,你实际上是直接修改保存的内容,任务的内容是不重要的,但是当你使用RAMJobStore以外的其他实现时就不同了。一个无状态的Job只能使用它被增加到Scheduler中时JobDataMap所保存的数据,而有状态的Job在每次执行之后都会持久化它的JobDataMap。为了使一个Job有状态,需要实现StatefulJob接口而不是Job接口。StatefulJob是Job的一个子接口,因此你无需额外实现Job接口。你也应该知道,由于Quartz把JobDataMap作为一个序列化的blob写到数据库中,所以在使用Job持久化时放在JobDataMap里的任何数据都应该是可序列化的。
2.2 Spring对Quartz的支持
Spring对Quartz的集成与Spring对Timer的集成相似,它可以让你使用Spring配置文件对你的任务调度进行完全配置。除此之外,Spring还提供辅助类集成Quartz JobStore接口,可以在Spring配置文件中配置任务持久化,并将对任务的修改加入到Spring事务管理中。
1.) 使用Spring调度任务
如你所料,很多需要用于调度Quartz任务的代码被放到Spring配置文件中。实际上,你仅仅需要在程序中加载ApplicationContext来让配置生效,Spring将自动启动调度器。例21,你可以看到每3s运行一次的MessageJob类的配置代码,而在先前例16中需要配置。
例21 声明式配置调度计划
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="job" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="cn.hurraysoft.quartz.MessageJob"/> <property name="jobDataAsMap"> <map> <entry key="message" value="This is a message from Spring config file!"/> </map> </property> </bean> <bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"> <property name="jobDetail" ref="job"></property> <property name="startDelay" value="1000"></property> <property name="repeatInterval" value="5000"></property> <property name="jobDataAsMap"> <map> <entry key="triggerMessage" value="This is a Trigger message from Spring config file!"/> </map> </property> </bean> <bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref local="trigger"/> </list> </property> </bean> </beans>
在这里,我们使用了JobDetailBean类,它扩展了JobDetail类,并用一种可声明的方式配置任务数据。JobDetailBean提供了更多的Spring可访问的JavaBean风格的属性,它也设置了合理的默认值给那些不得不自己提供的属性。例如,我们通常并不想提供一个任务名或者组名。默认情况下,JobDetailBean使用<bean>标签的ID作为任务名,并使用Scheduler的默认组作为组名。注意我们可以通过jobDataAsMap的属性把数据加入到JobDataMap的属性中。属性名不仅仅是摆设--因为你不可以直接向JobDataMap的属性中添加数据。它是JobDataMap类型, Spring配置文件不支持该类型。
配置好JobDetailBean后,下一步是创建一个触发器。Spring提供两个类:SimpleTriggerBean和CronTriggerBean,它们包装了SimpleTrigger和CronTrigger类,可以让你声明式地配置它们并把它们和一个JobDetail bean关联--所有这些都是在你的配置文件中完成的。注意在上面代码清单11-21的示例中,我们在程序开始时定义了1 s的延迟而后每3 s执行一次任务。默认情况下,SimpleTriggerBean把可重复执行的次数设置为无限。
需要配置的最后一部分是关于SchedulerFactoryBean。默认情况下,SchedulerFactoryBean创建一个StdSchedulerFactory的实例,后者创建Scheduler的实现来替换的类应该实现SchedulerFactory接口。你需要配置调度计划的唯一属性是triggers属性,它接受一个TriggerBean的列表作为参数。
由于所有的任务调度配置都在配置文件中,因此你只需要很少的代码来实际启动Scheduler和执行Job实例。实际上,你需要做的所有工作就是创建ApplicationContext,例22所示。
例22 声明式配置调度计划
package cn.hurraysoft.test; import java.io.IOException; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class Test { public static void main(String[] args) throws IOException { ApplicationContext ctx=new ClassPathXmlApplicationContext( "applicationContext_3.xml"); } }
正如你所见到的,这个类除了使用代码清单例21中的配置创建一个ApplicationContext类的实例之外不做任何工作。若运行这个程序并让调度的任务触发若干次,你最后得到下面的输出:
Previous Fire Time: null Current Fire Time: Thu Mar 18 00:16:56 CST 2010 Next Fire Time: Thu Mar 18 00:16:59 CST 2010 This is a message from Spring config file! null This is a Trigger message from Spring config file! Previous Fire Time: Thu Mar 18 00:16:56 CST 2010 Current Fire Time: Thu Mar 18 00:16:59 CST 2010 Next Fire Time: Thu Mar 18 00:17:02 CST 2010 This is a message from Spring config file! null This is a Trigger message from Spring config file! Previous Fire Time: Thu Mar 18 00:16:59 CST 2010 Current Fire Time: Thu Mar 18 00:17:02 CST 2010 Next Fire Time: Thu Mar 18 00:17:05 CST 2010 This is a message from Spring config file! null This is a Trigger message from Spring config file!
你会注意到它和前面一个MessageJob示例的运行结果非常相似,但是所显示的消息是在Spring配置文件中配置的。
2.) 使用持久化任务
Quartz的一个重要特性是它能创建有状态、可持久化的任务。这让你可以使用一些在基于Timer的任务调度中所没有的强大功能。你可以使用持久化任务在运行时把任务添加到Quartz中,并且重启程序时任务仍然在你的程序中。你也可以使用修改两个Job执行期间传递的JobDataMap,而前面所做的修改在重启后仍然有效。
在本示例中,我们将调度两个任务,一个使用Spring配置机制而另一个将在运行时进行调度。我们将看到Quartz持久化机制如何为这些Job处理对JobDataMap的修改,以及在程序的后续执行中将发生什么情况。
在开始之前,你需要创建一个Quartz用于保存任务信息的数据库。在Quartz发布版中(使用1.6.0版),你将发现一个关于数据库选项的脚本,它可以用于选择不同的关系数据库。在示例中,我们使用Oracle,但是你若使用其他的数据库,并且Quartz为它提供一个数据库脚本时,那就不应该有问题。在1.6.0版中,使用Quartz发布版的docs/dbTables子目录。一旦你找到为你选择的数据库所使用的脚本,可以对你的数据库执行它并检查数据库中是否创建了12个表,每一个表的前缀都是qrtz。
下一步,创建测试Job。因为我们希望在Job执行中对JobDataMap进行修改,我们需要提示Quartz让它把这个Job作为有状态的Job来处理。我们通过实现StatefulJob接口而不是Job接口来通知Quartz。例23中的代码。
例23 创建一个有状态的任务
package cn.hurraysoft.quartz; import java.util.Map; import javax.swing.text.AbstractDocument.Content; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.StatefulJob; public class PersistentJob implements StatefulJob { @Override public void execute(JobExecutionContext context) throws JobExecutionException { Map map=context.getJobDetail().getJobDataMap(); System.out.println("["+context.getJobDetail().getName()+"]"+map.get("message")); map.put("message", "Updated Message"); } }
StatefulJob接口没有为你的类增加需要实现的额外方法:它仅是一个标识用来告诉Quartz应在每次执行之后持久化JobDetail。在这里,可以看到我们显示保存在JobDataMap的消息和Job名。
下一步,在Spring里配置Job并将一个DataSource配置给Scheduler,Scheduler将用此DataSource进行持久化,例24所示。
例24 在Spring中配置Quartz持久化
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd"> <aop:aspectj-autoproxy /> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName"> <value>oracle.jdbc.driver.OracleDriver</value> </property> <property name="url"> <value>jdbc:oracle:thin:@10.1.99.18:1521:gs</value> </property> <property name="username"> <value>gs13</value> </property> <property name="password"> <value>gs13</value> </property> </bean> <bean id="job" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="cn.hurraysoft.quartz.PersistentJob"/> <property name="jobDataAsMap"> <map> <entry key="message" value="This is a message from Spring config file!"/> </map> </property> </bean> <bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"> <property name="jobDetail" ref="job"></property> <property name="startDelay" value="1000"></property> <property name="repeatInterval" value="3000"></property> <property name="jobDataAsMap"> <map> <entry key="triggerMessage" value="This is a Trigger message from Spring config file!"/> </map> </property> </bean> <bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref local="trigger"/> </list> </property> <property name="dataSource" ref="dataSource"/> </bean> </beans>
你将发现这里许多的配置代码都是例21中的配置代码,代码中最重要的部分是dataSource bean。我们在代码中使用Spring的SingleConnectionDataSource类:这是对于测试环境时很有用的dataSource实现,但不能在产品运行环境中使用(具体原因请参考该类的Javadoc文档)。记住,你需要根据你的应用环境配置来修改连接细节。
我们使用配置好的dataSource bean来设置SchedulerFactoryBean的dataSource属性。通过这样做,我们让Spring创建一个Scheduler,使用既定的DataSource配置持久化Job数据。实际上,这是由Spring自己的JobStore实现--LocalDataSourceJobStore来完成的。
完成配置后,剩下的工作就只是将它加载到一个程序中,并在运行时加载另一个Job到Scheduler。例25展示了具体代码。
例25 测试任务持久化 上述代码无需解释。
package cn.hurraysoft.test; import java.io.IOException; import java.util.Date; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class SpringWithJobPersistence { public static void main(String[] args) throws IOException, SchedulerException { ApplicationContext ctx=new ClassPathXmlApplicationContext( "applicationContext_3.xml"); Scheduler scheduler=(Scheduler) ctx.getBean("schedulerFactory"); JobDetail job=scheduler.getJobDetail("otherJob",Scheduler.DEFAULT_GROUP); if(job==null){ job=(JobDetail) ctx.getBean("job"); job.setName("otherJob"); job.getJobDataMap().put("message", "This is anthor message"); Trigger trigger=new SimpleTrigger("simpleTrigger",Scheduler.DEFAULT_GROUP,new Date(),null,org.quartz.SimpleTrigger.REPEAT_INDEFINITELY,3000); scheduler.scheduleJob(job,trigger); } } }
但是需要注意在我们调用第二个Job之前,我们使用Scheduler.getJobDetail()方法检查它是否已经在Scheduler中。这样我们就不会再这个程序的后续执行时覆盖此Job。 在你第一次运行示例时,你将得到类似下面的输出:
[otherJob]Updated Message [job]Updated Message [otherJob]Updated Message [job]Updated Message [otherJob]Updated Message [job]Updated Message
如你所见,每个Job在第一次执行时,显示的消息是在Job调度时被配置在JobDataMap中的原始信息。在后续执行时,每一个Job显示了上一次执行时设置的已更新消息。若重启程序,你将会发现输出结果有些不同
这次你可以看到,由于持久化了Job数据,你无需重新创建第二个Job并且JobDataMap仍使用程序上次执行所做的修改。
3.) 使用Quartz调度任意任务
同基于Timer的调度类一样,Spring提供了使用Quartz调度执行任意方法的能力。我们不讨论细节,因为这和使用Timer的情况基本一样。使用MethodInvokingJobDetailFactoryBean代替MethodInvokingTimerTaskFactoryBean,你可以自动创建JobDetail实现,而不是自动创建TimerTask实现。