使用Quartz调度器
Quartz调度器为调度工作提供了更丰富的支持。和Java定时器一样,可以使用Quartz来每隔多少毫秒执行一个工作。但Quartz比Java Timer更先进之处在于它允许你调度一个工作在某个特定的时间或日期执行。
关于Quartz的更多信息,可以访问Quartz位于http://www.opensymphony.com/quartz的主页。
让我们从定义发送报表邮件的工作开始使用Quartz:
创建一个工作
定义Quartz工作的第一步是创建一个类来定义工作。要做到这一点,你需要从Spring的QuartzJobBean中派生子类,如程序清单7.3所示:
程序清单7.3 定义一个Quartz工作
public class EmailReportJob extends QuartzJobBean {
public EmailReportJob() {}
protected void executeInternal(JobExecutionContext context)
throws JobExecutionException {
courseService.sendCourseEnrollmentReport();
}
private CourseService courseService;
public void setCourseService(CourseService courseService) {
this.courseService = courseService;
}
}
n
QuartzJobBean是Quartz中与Java的TimerTask等价的类。它实现了org.quartz.Job接口。executeInternal()方法定义了当预定的时刻来临时应该执行哪些动作。在这里,正如EmailReportTask,你只是简单地调用了courseService属性的sendCourseEnrollmentReport()方法。
在Spring配置文件中按以下方式声明这个工作:
<bean id="reportJob"
class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass">
<value>com.springinaction.training.
➥schedule.EmailReportJob</value>
</property>
<property name="jobDataAsMap">
<map>
<entry key="courseService">
<ref bean="courseService"/>
</entry>
</map>
</property>
</bean>
值得注意的是,在这里你并没有直接声明一个EmailReportJob Bean,而是声明了一个JobDetailBean。这是使用Quartz时的一个特点。JobDetailBean是Quartz的org.quartz.JobDetail的子类,它要求通过jobClass属性来设置一个Job对象。
使用Quartz的JobDetail中的另一个特别之处是EmailReportJob的courseService属性是间接设置的。JobDetail的jobDataAsMap属性接受一个java.util.Map,其中包含了需要设置给jobClass的各种属性。在这里,这个map包含了一个指向courseService Bean的引用,它的键值为courseService。当JobDetailBean实例化时,它会将courseService Bean注入到EmailReportJob的courseService属性中。
调度工作
现在工作已经被定义好了,接下来你需要调度这个工作。Quartz的org.quartz.Trigger类描述了何时及以怎样的频度运行一个Quartz工作。Spring提供了两个触发器,SimpleTriggerBean和CronTriggerBean。你应该使用哪个触发器?让我们分别考察一下这两个触发器,首先从SimpleTriggerBean开始。
SimpleTriggerBean与ScheduledTimerTask类似。你可以用它来指定一个工作应该以怎样的频度运行,以及(可选地)在第一次运行工作之前应该等待多久。例如,要调度报表工作每24小时运行一次,第一次在1小时之后开始运行,可以按照以下方式进行声明:
<bean id="simpleReportTrigger"
class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<property name="jobDetail">
<ref bean="reportJob"/>
</property>
<property name="startDelay">
<value>3600000</value>
</property>
<property name="repeatInterval">
<value>86400000</value>
</property>
</bean>
属性jobDetail装配了将要被调度的工作,在这个例子中是reportJob Bean。属性repeatInterval告诉触发器以怎样的频度运行这个工作(以毫秒作为单位)。这里,我们设置它为86400000,因此每隔24小时它会被触发一次。你也可以选择设置startDelay属性来延迟工作的第一次执行。我们设置它为3600000,因此在第一次触发之前它会等待1小时。
调度一个cron工作
尽管你可能认为SimpleTriggerBean适用于大多数应用,但它仍然不能满足发送注册报表邮件的需求。正如ScheduledTimerTask,你只能指定工作执行的频度,而不能准确指定它于何时运行。因此,你无法使用SimpleTriggerBean在每天早晨6:00给课程主任发送注册报表邮件。
然而,CronTriggerBean允许你更精确地控制任务的运行时间。如果你对Unix的cron工具很熟悉,则会觉得CronTriggerBean很亲切。你不是定义工作的执行频度,而是指定工作的准确运行时间(和日期)。例如,要在每天早上6:00运行报表工作,可以按照以下方式声明一个CronTriggerBean:
<bean id="cronReportTrigger"
class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail">
<ref bean="reportJob"/>
</property>
<property name="cronExpression">
<value>0 0 6 * * ?</value>
</property>
</bean>
和SimpleTriggerBean一样,jobDetail属性告诉触发器调度哪个工作。这里我们又一次装配了一个reportJob Bean。属性cronExpression告诉触发器何时触发。如果你不熟悉cron,这个属性可能看上去有点神秘,因此让我们进一步考察一下这个属性。
一个cron表达式有至少6个(也可能是7个)由空格分隔的时间元素。从左至右,这些元素的定义如下:
1.秒(0–59)
2.分钟(0–59)
3.小时(0–23)
4.月份中的日期(1–31)
5.月份(1–12或JAN–DEC)
6.星期中的日期(1–7或SUN–SAT)
7.年份(1970–2099)
每一个元素都可以显式地规定一个值(如6),一个区间(如9-12),一个列表(如9,11,13)或一个通配符(如*)。“月份中的日期”和“星期中的日期”这两个元素是互斥的,因此应该通过设置一个问号(?)来表明你不想设置的那个字段。表7.1中显示了一些cron表达式的例子和它们的意义:
表7.1 一些cron表达式的例子
表 达 式 | 意 义 |
0 0 10,14,16 * * ? | 每天上午10点,下午2点和下午4点 |
0 0,15,30,45 * 1-10 * ? | 每月前10天每隔15分钟 |
30 0 0 1 1 ? 2012 | 在2012年1月1日午夜过30秒时 |
0 0 8-5 ? * MON-FRI | 每个工作日的工作时间 |
对于cronReportTrigger,我们设置cronExpression为0 0 6 * * ?可以把它读作“在任何月份任何日期(不管是星期几)的6时0分0秒执行触发器。”换句话说,这个触发器会在每天早晨6:00执行。
使用CronTriggerBean完全能够满足课程主任的期望了。现在剩下要做的只是启动这个工作了。
启动工作
Spring的SchedulerFactoryBean是Quartz中与TimerFactoryBean等价的类。按照如下方式在Spring配置文件中声明它:
<bean class="org.springframework.scheduling.
➥quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronReportTrigger"/>
</list>
</property>
</bean>
属性triggers接受一组触发器。由于目前只有一个触发器,因此只需简单地装配一个包含cronReportTrigger Bean的一个引用的列表即可。
现在,你已经实现了调度发送注册报表邮件的需求。但在这个过程中,你做了一些额外的工作。在开始新的话题之前,首先让我们看一下如何通过更简单一些的方式调度报表邮件。
按调度计划调用方法
为了调度报表邮件,你不得不编写EmailReportJob Bean(或者在使用定时器任务的情况下,是EmailReportTask Bean)。但这个Bean只是简单地调用了一次CourseService的sendCourseEnrollmentReport()方法。以这一点而论,EmailReportTask和EmailReportJob看上去都有些空洞。如果你不用写这个额外的类就能指定调用sendCourseEnrollmentReport()方法,是不是很棒?
好消息!你能够不用编写一个单独的TimerTask或QuartzJobBean类就可以调度单次方法调用。要达到这一目的,Spring提供了MethodInvokingTimerTaskFactoryBean和MethodInvokingJobDetailFactoryBean,可以分别使用Java的定时器支持或Quartz调度器对方法调用进行调度。
例如,要使用Java的定时器服务调度一个对sendCourseEnrollmentReport()方法的调用,可以按照以下方式重新声明scheduledReportTask Bean:
<bean id="scheduledReportTask"
class="org.springframework.scheduling.timer.
➥MethodInvokingTimerTaskFactoryBean">
<property name="targetObject">
<ref bean="courseService"/>
</property>
<property name="targetMethod">
<value>sendCourseEnrollmentReport</value>
</property>
</bean>
在幕后,MethodInvokingTimerTaskFactoryBean创建了一个TimerTask来对targetObject属性指定的对象调用targetMethod属性指定的方法。这与EmailReportTask的效果是一样的。
使用根据以上方式声明的scheduledReportTask,你现在能够去掉EmailReportTask类以及声明它的reportTimerTask Bean了。
当使用ScheduledTimerTask时,MethodInvokingTimerTaskFactoryBean可以很好地完成简单的单次方法调用。但你现在是使用Quartz的CronTriggerBean在每天早晨6:00发送报表邮件。因此,你不是使用MethodInvokingTimerTaskFactoryBean,而是按以下方式重新声明reportJob Bean:
<bean id="courseServiceInvokingJobDetail"
class="org.springframework.scheduling.quartz.
MethodInvokingJobDetailFactoryBean">
<property name="targetObject">
<ref bean="courseService"/>
</property>
<property name="targetMethod">
<value>sendCourseEnrollmentReport</value>
</property>
</bean>
MethodInvokingJobDetailFactoryBean是Quartz中与MethodInvokingTimerTaskFactoryBean等价的类。在幕后,它是通过创建一个Quartz JobDetail对象来调用一次由targetObject和targetMethod属性指定的对象和方法。以这种方式使用MethodInvokingJobDetailFactoryBean,你就能去掉空洞的EmailReportJob类了。