基于Spring的任务调度(1)

 

    大多数应用程序逻辑是用来反馈某种形式的用户行为的,例如点击一个按钮或提交一个表单。然而,在很多应用程序中存在无需与用户交互来调用的某种处理,通常是在固定时间间隔运行一次。例如,也许我们有个进程每小时清理一次临时文件,或者每天午夜从数据库导出数据并发送到一个外部系统。多数重要的应用都要求某种形式的调度支持,该调度若不是和应用的业务逻辑直接相关就是为系统做一些辅助工作。

    如果你正在为应用程序建立调度任务,那么创建一个任务让它每小时或者一天运行一次是相当简单的。但是该任务需要在每周一、周三和周五的下午三点运行呢?编写代码可能就有些困难了,这也使得选择一个现有的任务调度解决方案比创建自己的调度框架更合理。

    如果从编程的角度来讨论任务调度,我们倾向于讨论3种不同的概念。一个任务是一个需要被调度以指定时间间隔运行的工作单元。一个触发器是一个引发任务运行的条件,可能是一个固定的时间间隔或者是既定片段的数据。一个调度计划是一组触发器的集合,它管理任务的整个时限。一般通过实现某个接口或者扩展某个特定基类来封装一个任务。我们可以使用任务调度框架支持的任何方式定义触发器。一些框架可能只支持简单的基于时间间隔的触发器,但是其他一些,比如Quartz,提供了更灵活的触发器模式。通常情况下,在调度计划中一个任务只有一个触发器,因此术语"调度"和"触发器"经常是交换使用。

    Spring对任务调度的支持有两种不同的形式:基于JDK Timer和基于Quartz。基于JDK Timer方式的任务调度为所有JVM 1.3及后续版本提供了任务调度能力,并且它没有Spring以外的依赖。基于Timer的任务调度比较简单,在定义任务调度时也只能提供有限的灵活性。然而,Timer支持建立在Java标准内并且不需要外部的依赖库,当你受限于程序大小或者企业策略时可以从其中受益。基于Quartz的任务调度具有更好的灵活性,允许我们定义更加接近现实世界的触发器,比如先前的每周一、周三和周五的下午三点运行任务的例子。

   我们将讨论Spring包含的上述两种任务调度解决方案。本文讨论了3个核心主题:使用JDK Timer进行任务调度,基于Quartz的任务调度和任务调度时的需考虑因素。

1 使用JDK Timer调度任务

    Spring支持的最基本的任务调度是基于JDK java.util.Timer类的。当使用Timer进行任务调度时,我们只能使用简单的基于时间间隔的触发器定义,这使得基于Timer的任务调度只适合那些需要在既定未来某个时间执行或者以固定周期运行的任务。

1.1  Timer触发器类型
    基于Timer的任务调度给我们提供了以下3种触发器类型。

  1.       一次性(one-off)。当使用一个一次性触发器时,任务执行是在未来某个时间点调度,使用从某个时间开始的毫秒数来定义该时间点。任务执行以后,它就不会再次被调度使用。我们发现一次性触发器对于那些只需完成一次的任务是很适用的,因为你自己可能忘记去做这个任务。例如,如果一个Web应用程序被调度在下一周进行维护,那么我们可以调度一个任务,在维护开始时,切换到一个维护页面。
  2.       重复和固定延迟(fixed-delay)。当使用一个固定延迟触发器时,我们可以像使用一次性触发器那样调度此任务第一个执行,但是在一个给定时间间隔内它会被重新调度执行。当我们使用固定延迟时,时间间隔是相对于此任务上一次执行的时间。这意味着两次连续执行时间间隔几乎完全一样,即使执行可能晚于原先的调度。使用此种类型的触发器,你指定的时间间隔是连续两次执行的间隔。当希望尽可能让时间间隔是一个常数时,使用这种触发器。
  3.       重复和定时(fixed-rate)。定时触发器的功能和固定延迟触发器很相似,但是下一次执行时间总是基于最初被调度的执行时间。这意味着如果单个执行被延迟了,那后续执行也不会被延迟。使用此种类型的触发器,你指定的时间间隔并不是连续两次执行时间的实际时间间隔。在实际执行的时间点很重要而实际执行时间间隔不重要时,应该使用此种触发器。

      我们可能发现,要形象化地显示固定延迟触发器和定时触发器之间的不同是很困难的。为了清晰地展示它们之间的区别,我们需要创建一个在执行中引起足够长延迟的示例,但是这相当困难。

    考虑一个在13:00开始执行的任务,两次运行之间的指定间隔为30 min。任务一直运行良好,直到16:30,此时系统的负载过重,导致执行垃圾收集,而这导致实际运行完成时间迟于预计时间--任务运行时已经16:31了。现在如果使用固定延迟来调度任务,那么重要的是时间间隔,也就是说,我们希望两次实际执行的时间间隔是30 min,因此下次任务调度执行的时间是17:01而不是17:00。如果我们使用定时调度,时间间隔只定义了预期的调度,也就是说,我们期望以程序开始执行的时间作为基准点,每隔30 min运行一次,而不是基于上次执行的时间,所以任务调度在17:00执行。

    两个触发器类型都有各自的用途。一般情况下,定时触发器用于下述情况:两次执行间的时间间隔规律的情况或者你想避免某次执行被延迟太长时间时,会导致两次执行发生的时间点太近的情况。使用定时触发器可以得到以上效果。一般来说,你在实时敏感系统中使用定时触发器,例如必须每小时整点执行的任务。

 

 


1.2 创建一个简单任务

为了创建是一个使用Timer类的任务,你可以简单扩展TimerTask类并实现run()方法执行你的任务逻辑。

例1 展示了一个简单TimerTask实现,它打印"Hello World!"到标准输出

 

package cn.hurraysoft.HelloWorldTask;

import java.util.TimerTask;

public class HelloWorldTask extends TimerTask {

	@Override
	public void run() {
		System.out.println("Hello World");
	}

}

 

这里你可以看到在run()方法中,我们简单地把"Hello World!"消息输出到标准输出。每次任务执行时,Timer就会调用TimerTask的run()方法。我们能够为这个任务创建的最简单的触发器是在1s后运行此任务的一次性触发器。

例2 在HelloWorldTask类中使用一次性触发器

 

package cn.hurraysoft.HelloWorldTask;

import java.util.Timer;

public class OneOffScheduling {
	public static void main(String[] args) {
		Timer t=new Timer();
		t.schedule(new HelloWorldTask(), 1000);
	}
}

      当使用JDK Timer类为一个指定触发器调度任务时,我们必须先创建一个Timer类的实例,然后使用schedule()或者scheduleAtFixedRate()方法。在例2中,我们使用了schedule()方法来调度一个HelloWorldTask实例,使它在程序开始的1000ms延迟后运行。如果运行上述示例,1s延迟以后,我们会得到如下消息:

 

 

Hello World

这种一次性的触发器一般没有什么用处,想想看你调度一个一次性任务会有多频繁,使它在一个程序启动后的任意时间段内运行。因此在创建一次性触发器时,你可以指定一个绝对时间。因此我们想创建一个任务,它将在一个重要生日前7天提示我们,我们可以用我们自己的调用来替换Timer.schedule()方法,如下所示:

 

Calendar cal=Calendar.getInstance();
cal.set(2008, Calendar.NOVEMBER, 30);
Timer t=new Timer();
t.schedule(new HelloWorldTask(), cal.getTime());

在这个例子中,可以看到我们为日期2008年11月30日创建了一个Calendar实例,然后使用Calendar实例,我们调度HelloWorkdTask的运行时间。这显然比第一个例子更实用,因为无论程序什么时候运行,任务总是被调度在相同时间内运行。该方式的唯一缺陷是,我们将不会为2009年或2010年的生日提示,除非我们显式地加入更多的触发器。不过我们使用一个可以重复执行的触发器避免此问题。 对于两种重复执行触发器,固定延迟和定时触发器,都使用同样的方式进行配置:你指定开始时间点,使用一个绝对日期或通过schedule()方法开始后的相对毫秒数进行定义,然后你以毫秒为单位指定一个时间间隔来控制后续执行的发生的时间。要记住"时间间隔"在你使用固定延迟和定时触发器时表示不同的含义。 我们可以调度HelloWorldTask在程序开始后延迟1 s运行,接着每3s运行一次。

例3 调度一个重复性任务

 

package cn.hurraysoft.HelloWorldTask;

import java.util.Calendar;
import java.util.Timer;

public class FixedDelayScheduling {
	public static void main(String[] args) {
		Timer t=new Timer();
		t.schedule(new HelloWorldTask(),1000,3000);
	}
}

 

      如果我们运行程序,将看到第一条"Hello World!"消息在1 s后显示,而后每3 s显示一条"Hello World!"消息。要使用定时触发器来调度该任务,只需用Timer.scheduleAtFixedRate()方法替换掉Timer.schedule()方法即可

例4 使用定时触发器来调度任务

 

package cn.hurraysoft.HelloWorldTask;

import java.util.Calendar;
import java.util.Timer;

public class FixedDelayScheduling {
	public static void main(String[] args) {
		Timer t=new Timer();
		t.scheduleAtFixedRate(new HelloWorldTask(),1000,3000);
	}
}

 

和一次性触发器一样,可以让固定延迟和定时触发器使用一个固定时间启动。使用该方法,我们可以创建一个用于生日提示的触发器示例,它在一个指定的日期被触发然后每年都会运行一次。 例5 调度生日提示程序

package cn.hurraysoft.HelloWorldTask;

import java.util.Calendar;
import java.util.Timer;

public class SimpleBirthdayReminderScheduling {
	public static final long MILLIS_IN_YEAR=1000*60*60*24*365;
	public static void main(String[] args) {
		Calendar cal=Calendar.getInstance();
		cal.set(2008, Calendar.NOVEMBER, 30);
		Timer t=new Timer();
		t.scheduleAtFixedRate(new HelloWorldTask(),cal.getTime(),MILLIS_IN_YEAR);
	}
}

在这个例子中,我们可以看到我们先计算了代表一年的毫秒数,然后创建一个Calendar实例并用它定义开始时间为11月30日而时间间隔为一年。现在,只要该程序一直运行,每年的11月30日"Hello World!"消息就会打印到标准输出。记住它并不是功能完善的示例,所以它没有真实的通知机制,并且每次我们希望增加一个新的生日提示时都需要修改代码。在之后的文章中,我们将使用Spring 的JDK Timer支持类来创建一个更稳健的生日提示程序。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值