Spring 中正确使用 Quartz 和 CronExpression
Quartz 作为企业级任务调度框架以其灵活的使用方式、强大的功能已经得到广泛应用,作为一向喜欢将业内流行的工具纳入支持的 Spring 自然已经内置 了对 Quartz 的支持,使得 Quartz 中最常使用的 SimpleTrigger 和 CronTrigger 的使用得到了最大简化,分别对应 Spring 的 org.springframework.scheduling.quartz.SimpleTriggerBean 和 org.springframework.scheduling.quartz.CronTriggerBean ,这两个类用起来非常方便,其中 SimpleTrigger 更类似于 JDK 中的 Timer ,它只是简单的以某个时间间隔来执行某个任务而已,比较简陋,而 CronTrigger 功能则十 分强大,可以设定制定任务在任意指定时刻内调用,其使用 Unix 中的 Cron Expression 来制定调度策略,十分灵活,不过 Cron Expression 可能需要用点时间来学习,不过一旦掌握会觉得真的很不错,掌握了这两种 Trigger 基本上就可以应付实现大多数 J2EE 应用中的时 间任务调度服务了。
下面举一个简单例子说明一下在 Spring1.2.5 中使用 Quartz1.5 的方式(所需要的包在 Spring1.2.5 的发行版中的 lib 目录下可以找到,将其拷贝到工程的类路径中)。
首先建立两个要调度的任务,他们都必须继承自 org.springframework.scheduling.quartz.QuartzJobBean ,而业务逻辑都是放在 executeInternal 方法中,指定的任务逻辑实现之后,需要将其注入到一个 JobDetailBean 中, JobDetailBean 可以看作 为一个具体任务的设置它指定了要执行的任务和执行任务的时间策略, JobDetailBean 不用实现,只需要在 Spring 的配置文件中设置便可:
package com.peuo.albumSys.scheduling;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.apache.log4j.Logger;
import com.peuo.albumSys.business.IAlbumSysService;
import com.peuo.albumSys.domainobj.User;
import com.peuo.albumSys.domainobj.Album;
/**
* 在指定的时刻查询当前所有的用户,并显示其姓名。
* @author Shippo Field
*/
public class ScheduledQueryUsers extends QuartzJobBean {
private static Logger log = Logger.getLogger(ScheduledQueryUsers.class);
private IAlbumSysService service = null;
/**
* @param service 要设置的业务逻辑 Bean 实例,可以是已被纳入事务管理的某个 Bean ,这样以来 QuartzJobBean 内部逻辑也可以纳入事务管理。
*/
public void setService(IAlbumSysService service) {
this.service = service;
}
/* (非 Javadoc )
* @see org.springframework.scheduling.quartz.QuartzJobBean#executeInternal(org.quartz.JobExecutionContext)
*/
protected void executeInternal(JobExecutionContext ctx)
throws JobExecutionException {
log.debug("Now entering method executeInternal()...");
List users = service.findAllUser();
StringBuffer str = new StringBuffer();
for(int i = 0; i < users.size(); i++){
User user = (User)users.get(i);
str.append("\nUser[" + i + "] = " + user.getUserName());
Set albums = user.getAlbumSet();
Iterator it = albums.iterator();
str.append("(Albums:");
while(it.hasNext()){
str.append(((Album)it.next()).getAlbumName()).append(";");
}
}
log.debug(str.toString());
}
}
package com.peuo.albumSys.scheduling;
import java.util.Date;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.apache.log4j.Logger;
import com.peuo.albumSys.business.IAlbumSysService;
import com.peuo.albumSys.domainobj.Album;
import com.peuo.albumSys.domainobj.Photo;
/**
* 本程序会在指定的时刻插入照片
* @author Field
*/
public class ScheduledAddPhoto extends QuartzJobBean {
private static Logger log = Logger.getLogger(ScheduledAddPhoto.class);
private IAlbumSysService service = null;
private static int count = 0;
/* (非 Javadoc )
* @see org.springframework.scheduling.quartz.QuartzJobBean#executeInternal(org.quartz.JobExecutionContext)
*/
protected void executeInternal(JobExecutionContext arg0)
throws JobExecutionException {
Date now = new Date(System.currentTimeMillis());
Photo newPhoto = new Photo();
newPhoto.setAlbum(new Album(new Integer(1)));
newPhoto.setPhotoName( now.toString());
newPhoto.setPhotoUrl("Photo url" + ++count);
service.addPhoto(newPhoto);
log.debug("New photo added : name =" + newPhoto.getPhotoName() +"; url = " + newPhoto.getPhotoUrl());
}
/**
* @return 当前业务逻辑实例。
*/
public IAlbumSysService getService() {
return service;
}
/**
* @param service 要设置的业务逻辑实例。
*/
public void setService(IAlbumSysService service) {
this.service = service;
}
}
然后在Spring的配置文件中分别设置一个SimpleTriggerBean和CronTriggerBean来调用上面两个任务,配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" " http://www.springframework.org/dtd/spring-beans.dtd ">
<beans>
……………………………………………………………………………………………………
<bean id="queryUserJob" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass">
<value>com.peuo.albumSys.scheduling.ScheduledQueryUsers</value>
</property>
<property name="jobDataAsMap">
<map>
<entry key="service">
<ref local="albumSysService"/>
</entry>
</map>
</property>
</bean>
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
<property name="jobDetail">
<ref local="queryUserJob"/>
</property>
<property name="startDelay">
<value>15000</value>
</property>
<property name="repeatInterval">
<value>60000</value>
</property>
<property name="repeatCount">
<value>-1</value>
</property>
</bean>
<bean id="addPhotoJob" class="org.springframework.scheduling.quartz.JobDetailBean">
<property name="jobClass">
<value>com.peuo.albumSys.scheduling.ScheduledAddPhoto</value>
</property>
<property name="jobDataAsMap">
<map>
<entry key="service">
<ref local="albumSysService" />
</entry>
</map>
</property>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
<property name="jobDetail">
<ref local="addPhotoJob"/>
</property>
<property name="cronExpression">
<value>0 20 19 * * ?</value>
</property>
</bean>
<bean id="sfb" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref local="simpleTrigger"/>
<ref local="cronTrigger"/>
</list>
</property>
</bean>
上面红色标出的部分可以看出, Spring 对于将 QuartzJob 注入 JobDetailBean 的方式有点特殊,你需要将要注入给 QuartzJob 的某个属性注入到 JobDetailBean 的 jobDataAsMap 中, 这不同于 Spring 平时直接对某个 Bean 的属性直接注入的方式,这主要是因为 Quartz 执行一个任务( JOB 或者叫作业)的时候会提供给任务一个 JobExecutionContext 的任务执行上下文,从这个上下文中可以获得 Job 的环境设置,具体的可以看一下 Quartz 的 API 文档就明白 了,反正只要记住这里的注入方式比较特殊就行了。
以上配置文件分别设置了一个 SimpleTriggerBean 和 CronTriggerBean ,前者将会在程序执行 15 秒之后开始不断以间隔一 分钟的方式不断查询当前用户信息并显示出来,后者则指定每天的 19 : 20 分执行向 ID 为 1 的用户的相册内添加一张照片的任务,其 Cron Expression 为 0 20 19 * * ? 。
从以上可以看出, Spring 大大简化了对 Quartz 任务时间调度框架的使用,将其完美的融合入了 Spring 提供的 IoC 容器,只是其注入方式需要注意,还有就是 Cron Expression 一定要掌握,这个是 Quartz 真正的强大之处。
网上似乎对 Cron 表达式的中文介绍相当少,我干脆就把 Quartz 中的 doc 翻译一下,各位需要的朋友可以快速了解一下大致用法:
一个 Cron- 表达式是一个由六至七个字段组成由空格分隔的字符串,其中 6 个字段是必须的而一个是可选的,如下:
字段名 |
|
允许的值 |
|
允许的特殊字符 |
秒 |
|
0-59 |
|
, - * / |
分 |
|
0-59 |
|
, - * / |
小时 |
|
0-23 |
|
, - * / |
日 |
|
1-31 |
|
, - * ? / L W C |
月 |
|
1-12 or JAN-DEC |
|
, - * / |
周几 |
|
1-7 or SUN-SAT |
|
, - * ? / L C # |
年 ( 可选字段 ) |
|
empty, 1970-2099 |
|
, - * / |
'*' 字符可以用于所有字段,在 “ 分 ” 字段中设为 "*" 表示 " 每一分钟 " 的含义。
'?' 字符可以用在 “ 日 ” 和 “ 周几 ” 字段 . 它用来指定 ' 不明确的值 '. 这在你需要指定这两个字段中的某一个值而不是另外一个的时候会被用到。在后面的例子中可以看到其含义。
'-' 字符被用来指定一个值的范围,比如在 “ 小时 ” 字段中设为 "10-12" 表示 "10 点到 12 点 ".
',' 字符指定数个值。比如在 “ 周几 ” 字段中设为 "MON,WED,FRI" 表示 "the days Monday, Wednesday, and Friday".
'/' 字符用来指定一个值的的增加幅度 . 比如在 “ 秒 ” 字段中设置为 "0/15" 表示 " 第 0, 15, 30, 和 45 秒 " 。而 "5/15" 则表示 " 第 5, 20, 35, 和 50". 在 '/' 前加 "*" 字符相当于指定从 0 秒开始 . 每个字段都有一系列可以开始或结束的数值。对于 “ 秒 ” 和 “ 分 ” 字段来说,其数值范围为 0 到 59 ,对于 “ 小时 ” 字段来说其为 0 到 23, 对于 “ 日 ” 字段来说为 0 到 31, 而对于 “ 月 ” 字段来说为 1 到 12 。 "/" 字段仅仅只是帮助你在允许的数值范围内从开始 " 第 n" 的值。 因此 对于 “ 月 ” 字段来说 "7/6" 只是表示 7 月被开启而不是 “ 每六个月 ”, 请注意其中微妙的差别。
'L' 字符可用在 “ 日 ” 和 “ 周几 ” 这两个字段。它是 "last" 的缩写 , 但是在这两个字段中有不同的含义。例如 ,“ 日 ” 字段中的 "L" 表示 " 一个月中的最后一天 " —— 对于一月就是 31 号对于二月来说就是 28 号(非闰年)。而在 “ 周几 ” 字段中 , 它简单的表示 "7" or "SAT" ,但是如果在 “ 周几 ” 字段中使用时跟在某个数字之后 , 它表示 " 该月最后一个星期 ×" —— 比如 "6L" 表示 " 该月最后一个周五 " 。当使用 'L' 选项时 , 指定确定的列表或者范围非常重要,否则你会被结果搞糊涂的。
'W' 可用于 “ 日 ” 字段。用来指定历给定日期最近的工作日 ( 周一到周五 ) 。比如你将 “ 日 ” 字段设为 "15W" ,意为 : " 离该月 15 号最近的工作日 " 。因此如果 15 号为周六,触发器会在 14 号即周五调用。如果 15 号为周日 , 触发器会在 16 号也就是周一触发。如果 15 号为周二 , 那么当天就会触发。然而如果你将 “ 日 ” 字段设为 "1W", 而一号又是周六 , 触发器会于下周一也就是当月的 3 号触发 , 因为它不会越过当月的值的范围边界。 'W' 字符只能用于 “ 日 ” 字段的值为单独的一天而不是一系列值的时候。
'L' 和 'W' 可以组合用于 “ 日 ” 字段表示为 'LW' ,意为 " 该月最后一个工作日 " 。
'#' 字符可用于 “ 周几 ” 字段。该字符表示 “ 该月第几个周 ×” ,比如 "6#3" 表示该月第三个周五 ( 6 表示周五而 "#3" 该月第三个 ) 。再比如 : "2#1" = 表示该月第一个周一而 "4#5" = 该月第五个周三。注意如果你指定 "#5" 该月没有第五个 “ 周 ×” ,该月是不会触发的。
'C' 字符可用于 “ 日 ” 和 “ 周几 ” 字段,它是 "calendar" 的缩写。 它表示为基于相关的日历所计算出的值(如果有的话)。如果没有关联的日历 , 那它等同于包含全部日历。 “ 日 ” 字段值为 "5C" 表示 " 日历中的第一天或者 5 号以后 " , “ 周几 ” 字段值为 "1C" 则表示 " 日历中的第一天或者周日以后 " 。
对于 “ 月份 ” 字段和 “ 周几 ” 字段来说合法的字符都不是大小写敏感的。
下面是一些完整的例子 :
表达式 |
|
含义 |
"0 0 12 * * ?" |
|
每天中午十二点触发 |
"0 15 10 ? * *" |
|
每天早上 10 : 15 触发 |
"0 15 10 * * ?" |
|
每天早上 10 : 15 触发 |
"0 15 10 * * ? *" |
|
每天早上 10 : 15 触发 |
"0 15 10 * * ? 2005" |
|
2005 年的每天早上 10 : 15 触发 |
"0 * 14 * * ?" |
|
每天从下午 2 点开始到 2 点 59 分每分钟一次触发 |
"0 0/5 14 * * ?" |
|
每天从下午 2 点开始到 2 : 55 分结束每 5 分钟一次触发 |
"0 0/5 14,18 * * ?" |
|
每天的下午 2 点至 2 : 55 和 6 点至 6 点 55 分两个时间段内每 5 分钟一次触发 |
"0 0-5 14 * * ?" |
|
每天 14:00 至 14:05 每分钟一次触发 |
"0 10,44 14 ? 3 WED" |
|
三月的每周三的 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" |
|
每月最后一个周五的 10 : 15 触发 |
"0 15 10 ? * 6L 2002-2005" |
|
2002 年至 2005 年的每月最后一个周五的 10 : 15 触发 |
"0 15 10 ? * 6#3" |
|
每月的第三个周五的 10 : 15 触发 |
请注意 '?' 和 '*' 在 “ 日 ” 和 “ 周几 ” 字段中的用法 !