Spring提供的三种定时任务机制及其比较:

来源:

1. 基于Quartz的定时机制:

下面详细解释这个类图中涉及的关键类及其使用场景

 

1.1. SchedulerFactoryBean:

这是Spring中基于Quartz的定时机制入口,只要Spring容器装载了这个类,Quartz定时机制就会启动,并加载定义在这个类中的所有trigger.

 

Spring配置范例:

 



<bean id="sfb" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
 <!-- 添加触发器 -->
 <property name="triggers">
  <list>
   <ref local="appSubscTrigger" />
  </list>
 </property>
		
 <!-- 添加listener -->
 <property name="globalTriggerListeners">
  <list>
   <ref local="myTaskTriggerListener" />
  </list>
 </property>
</bean>



  

1.2. CronTriggerBean:

实现了Trigger接口,基于Cron表达式的触发器,这种触发器的好处是表达式与linux下的crontab一致,能够满足非常复杂的定时需求,也容易配置。

 

Spring配置范例:

 


<bean id="notifyTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
 <property name="jobDetail" ref="notifyJobDetail" />
 <property name="cronExpression" value="${notify_trigger_cron_expression}" />
</bean>


1.3. SimpleTriggerBean:

该类也实现了Trigger接口,基于配置的定时调度 ,这个触发器的优点在于很容易配置一个简单的定时调度策略。

 

Spring配置范例:



<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>



1.4. JobDetailBean:

JobDetail类的简单扩展,能够包装一个继承自QuartzJobBean的普通Bean,使之成为定时运行的Job。

缺点是包装的Bean必须继承自一个指定的类,通用性不强,对普通Job的侵入性过强,不推荐使用。

 

1.5. MethodInvokingJobDetailFactoryBean:

Spring提供的一个不错的JobDetail包装工具,能够包装任何bean,并执行类中指定的任何stati或非static的方法,避免强制要求bean去实现某接口或继承某基础类。

 

Spring配置范例:

 


<bean id="notifyJobDetail" parent="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
 <property name="targetObject" ref="notifyServerHandler" />
 <property name="targetMethod" value="execute" />
</bean>


 

 

1.6. 关于TriggerListener和JobListener:

Quartz中提供了类似WebWork的拦截器的功能,系统执行任务前或任务执行完毕后,都会检查是否有对应的Listener需要被执行,这种AOP的思想为我们带来了灵活的业务需求实现方式。

 

例如现在有一个简单的业务要求:任务执行前先判断当前服务器是否为task服务器,不是则不执行任务。对于这种业务需求,我们可以简单的实现一个TriggerListener,并将其插入SchedulerFactoryBean的globalTriggerListeners中,这样所有的job在执行前后都会调用TriggerListener中对应的方法。

 

代码范例:

 

public class MyTaskTriggerListener implements TriggerListener {
	protected static final Log logger = LogFactory.getLog(MyTaskTriggerListener.class);

	/**
	 * 需要运行task任务的机器列表
	 */
	private String taskServers;

	public String getName() {
		return "MyTaskTriggerListener";
	}

	public void triggerComplete(Trigger arg0, JobExecutionContext arg1, int arg2) {
	}

	public void triggerFired(Trigger arg0, JobExecutionContext arg1) {
	}

	public void triggerMisfired(Trigger arg0) {
	}

	/**
	 * 判断当前服务器是否为task服务器,来决定是否执行task
	 * @return
	 */
	public boolean vetoJobExecution(Trigger arg0, JobExecutionContext arg1) {
		String serverName;
		try {
			serverName = InetAddress.getLocalHost().getHostName();//获取主机名
		} catch (UnknownHostException e) {
			e.printStackTrace();
			return true;
		}
		if (taskServers.indexOf(serverName) > -1) {
			if (logger.isInfoEnabled()) {
				logger.info("this is a task server, job will be executed");
			}
			return false;
		} else {
			if (logger.isInfoEnabled()) {
				logger.info("this is not a task server, job will be vetoed");
			}
			return true;
		}
	}

	public String getTaskServers() {
		return taskServers;
	}

	public void setTaskServers(String taskServers) {
		this.taskServers = taskServers;
	}
}

 

 

2. 基于Timer的定时机制:

JDK提供了基础的定时类:Timer,在这个类的基础上,Spring提供了一套简单的定时机制。


 下面详细解释这个类图中涉及的关键类及其使用场景:

 

2.1. TimerFactoryBean:

这个类非常类似Quartz中的SchedulerFactoryBean,是基于Timer的定时机制的入口,Spring容器装载此类后会自动开始定时器。

 

Spring配置范例:

 


<bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean"> <property name="scheduledTimerTasks">
  <list>
   <ref bean="scheduledTask" />
  </list>
 </property>
</bean>


 

2.2. ScheduledTimerTask:

 

类似于Quartz中的Trigger的SimpleTriggerBean实现,任务是在设定的时间触发并执行配置的任务,特点是配置简单、明了,使用于简单的任务触发逻辑。

 

Spring配置范例:

 


<bean id=”scheduledReportTask” class=”org.springframework.scheduling.timer.ScheduledTimerTask”> <property name=”timerTask”>
  <ref bean =”reportTimerTask”/>
 </property>
 <property name=”period”>
  <value>86400000</value>
 </property>
</bean>

2.3. TimerTask抽象类:

 

普通task实现必须要继承的父类,主要包含一个run()的方法,类似Quartz中的QuartzJobBean,对应用侵入性较强,也不推荐使用。

 

2.4. MethodInvokingTimerTaskFactoryBean:

类似Quartz中的MethodInvokingJobDetailFactoryBean,用于封装任何bean,并可以执行bean中的任意方法,不再复述。

 

 

3. 基于Executor的定时机制:


这种定时机制与上面两种定时机制没有太大区别,特别是在配置和实现功能上,不同的是它的核心是基于ScheduledExecutorService(ScheduledThreadPoolExecutor是默认实现),一种JDK5.0中提供的基于线程的并发机制,关于JDK5中的线程池的概念及其一些深入分析,请参考老唐的博客:这里不再复述。

 

 

 

4. 三种定时机制的比较和案例分析:

看完了这三种定时机制,各有各的优劣,不同场景下我们应该灵活选择不同的定时机制。总的来说,如果我们需要简单的定时器,我们可以选用基于timer的定时器,如果定时规则较为复杂,我们可以选用基于Quartz的定时器,如果我们要用到线程池来处理异步任务,我们可以选用基于Executor的定时机制,虽然只是任务实现中用到线程池,毕竟也是一脉相承的,当然也可以用Quartz的定时器+基于Executor的任务线程池,完全没有任何冲突的。

 

 

 

 

spring定时任务时间格式cronExpression设置:

 

org.springframework.scheduling.quartz.CronTriggerBean允许你更精确地控制任务的运行时间,只需要设置其cronExpression属性。

一个cronExpression表达式有至少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)

示例:

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 ----每个工作日的工作时间

 

各个时间可用值如下:

秒 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"的值。

“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