定时任务
6. 执行任务和任务计划
Spring框架分别为异步执行、TaskExecutor
的任务调度和TaskScheduler
接口提供了抽象。Spring还具有支持线程池或委派到应用程序服务器环境CommonJ的接口实现。最终, 在Java SE 5、Java SE 6和Java EE有差异的环境都实现了一套公共的抽象接口。
Spring还具有集成类,支持使用Timer
(JDK自1.3以来的一部分)和Quartz Scheduler(http://quartz-scheduler.org)进行调度。 您可以使用FactoryBean
同时分别对Timer
或Trigger
实例进行可选引用来设置这两个调度程序。 此外,还提供了Quartz Scheduler和 Timer
的便捷类,它允许您调用现有目标对象的方法(类似于正常的MethodInvokingFactoryBean
操作)。
6.1. Spring TaskExecutor
抽象
Executors是JDK中使用的线程池的名字,executor意思是无法保证底层的实现实际是一个池,一个executor可以是单线程的或者是同步的。Spring的抽象隐藏了Java SE和Java EE环境之间的实现细节。
Spring的TaskExecutor
接口和java.util.concurrent.Executor
接口是相同的。实际上,他存在的主要原因是在使用线程池时对Java 5抽象的程度不同。该接口有一个 (execute(Runnable task)
)方法,它根据线程池的语义和配置接受执行任务.
最初创建TaskExecutor
是为了给其他Spring组件提供所需的线程池抽象。诸如ApplicationEventMulticaster
,JMS的AbstractMessageListenerContainer
和Quartz集成之类的组件都使用 TaskExecutor
抽象来池化线程。 但是,如果您的bean需要线程池行为,您也可以根据自己的需要使用此抽象。
6.1.1. TaskExecutor
类型
Spring包含许多TaskExecutor的预构建实现。很可能,你永远不需要实现自己的。 Spring提供的变体如下:
-
SyncTaskExecutor
: 此实现不会异步执行调用。 相反,每次调用都发生在调用线程中。 它主要用于不需要多线程的情况,例如在简单的测试用例中。 -
SimpleAsyncTaskExecutor
: 此实现不会重用任何线程。 相反,它为每次调用启动一个新线程。 但是,它确实支持并发限制,该限制会阻止任何超出限制的调用,直到释放一个插槽。 如果您正在寻找真正的池,请参阅此列表中稍后的ThreadPoolTaskExecutor
。 -
ConcurrentTaskExecutor
: 此实现是java.util.concurrent.Executor
对象的适配器。有一个可选的ThreadPoolTaskExecutor
,它将Executor
配置参数作为bean属性公开。很少需要使用到ConcurrentTaskExecutor
,但如果ThreadPoolTaskExecutor
不够灵活,那么你就需要ConcurrentTaskExecutor
。 -
ThreadPoolTaskExecutor
: 这种实现是最常用的。 它公开了bean属性,用于配置java.util.concurrent.ThreadPoolExecutor
并将其包装在TaskExecutor
中。 如果您需要适应不同类型的java.util.concurrent.Executor
,我们建议您使用ConcurrentTaskExecutor
。 -
WorkManagerTaskExecutor
: 此实现使用CommonJWorkManager
作为其后备服务提供程序,并且是在Spring应用程序上下文中在WebLogic或WebSphere上设置基于CommonJ的线程池集成的中心便利类。 -
DefaultManagedTaskExecutor
: 此实现在JSR-236兼容的运行时环境(例如Java EE 7+应用程序服务器)中使用JNDI获取的ManagedExecutorService
,为此目的替换CommonJ WorkManager。
6.1.2. 使用 TaskExecutor
Spring的 TaskExecutor
实现用作简单的JavaBeans。 在下面的示例中,我们定义了一个使用ThreadPoolTaskExecutor
异步打印出一组消息的bean:
如您所见,您可以将Runnable
添加到队列中,而不是从池中检索线程并自行执行。 然后,TaskExecutor
使用其内部规则来确定任务何时执行。
要配置TaskExecutor
使用的规则,我们公开了简单的bean属性:
6.2. Spring TaskScheduler
抽象
除了TaskExecutor
抽象之外,Spring 3.0还引入了一个TaskScheduler
,它具有各种方法,可以在将来的某个时刻调度任务。 以下清单显示了TaskScheduler
接口定义:
最简单的方法是一个名为schedule
的方法,它只接受Runnable
和Date
。 这会导致任务在指定时间后运行一次。 所有其他方法都能够安排任务重复运行。 通过这些方法,在简单的周期中需要以固定频率和固定时间间隔方法执行任务是实现的,但接受Trigger
会更方便。
6.2.1. Trigger
接口
Trigger
接口基本上受到JSR-236的启发,从Spring 3.0开始,它尚未正式实现。Trigger的基本思想是可以基于过去的执行结果或甚至任意条件来确定执行时间。如果这些确定考虑到了前一次执行的结果, 则该信息在TriggerContext
中可用。Trigger
接口本身非常简单::
TriggerContext
是最重要的部分。 它封装了所有相关数据,如有必要,将来可以进行扩展。 TriggerContext
是一个接口(默认情况下使用 SimpleTriggerContext
实现)。 以下清单显示了Trigger
实现的可用方法。
6.2.2. Trigger
实现
Spring提供了两个Trigger
接口的实现 , 最有趣的是CronTrigger
。 它支持基于cron表达式调度任务。例如,以下任务被安排在每小时超过15分钟, 但仅在工作日的早上9时到下午5时”营业时间”内运行:
另一个现成的实现是PeriodicTrigger
,它接受一个固定的周期、一个可选的初始延迟值和一个布尔值来指示该期间是否应解释为固定速率或固定延迟。由于TaskScheduler
接口已经定义了以固定速率或固定延迟来调度任务的方法,因此应该尽可能直接使用这些方法。 PeriodicTrigger
实现的价值在于,它可以在依赖于Trigger
抽象的组件中使用。例如,允许周期性触发器、cron-based触发器、甚至是可互换使用的自定义触发器实现,可能会很方便。此类组件可以利用依赖项注入,这样可以在外部配置此类Triggers
,因此容易修改或扩展。
6.2.3. TaskScheduler
实现
与Spring的TaskExecutor
抽象一样, TaskScheduler
的主要好处是,依赖于调度行为的代码不必与特定的调度程序实现耦合。当在应用程序服务器环境中运行时,不应由应用程序本身直接创建线程,因此提供的灵活性尤其重要。对于这种情况, Spring提供了一个TimerManagerTaskScheduler
,它委托给WebLogic或WebSphere上的CommonJ TimerManager
,以及一个委托给Java EE 7+环境中的JSR-236 ManagedScheduledExecutorService
的更新的DefaultManagedTaskScheduler
。 两者通常都配置有JNDI查找。
每当外部线程管理不是必需的时候,更简单的替代方案是应用程序中的本地ScheduledExecutorService
设置,可以通过Spring的ConcurrentTaskScheduler
进行调整。 为方便起见,Spring还提供了一个ThreadPoolTaskScheduler
,它在内部委托给ScheduledExecutorService
,以提供沿ThreadPoolTaskExecutor
行的公共bean样式配置。些变体适用于宽松应用程序服务器环境中的本地嵌入式线程池设置,特别是在Tomcat和Jetty上。
6.3. 对调度和异步执行的注解支持
Spring为任务调度和异步方法执行提供了注解支持
6.3.1. 启用调度注解
要启用对@Scheduled
和@Async
注解的支持,请将@EnableScheduling
和@EnableAsync
添加到您的@Configuration
类中。如下例所示::
您可以自由选择您的应用程序的相关注解。例如,如果您只需要支持@Scheduled
,那么就省略@EnableAsync
。对于更多的fine-grained控制,您可以另外实现SchedulingConfigurer
和/或AsyncConfigurer
接口。有关详细信息,请参阅对应的javadocs。
如果您喜欢XML配置, 请使用<task:annotation-driven>
元素。如下:
请注意,在上面的XML中,提供了一个执行器引用来处理与@Async
注解的方法对应的那些任务,并提供了调度程序引用来管理那些用@Scheduled
注解的方法。
处理@Async
注解的默认建议模式是proxy
,它允许仅通过代理拦截调用。 同一类中的本地调用不能以这种方式截获。 对于更高级的拦截模式,请考虑结合编译时或加载时编织切换到aspectj
模式。
6.3.2. @Scheduled
注解
可以将 @Scheduled
注解与触发器元数据一起添加到方法中。例如, 下面的方法每隔5秒调用一次固定的延迟, 这意味着该期间将从每次调用的完成时间计算:
如果需要安装固定的速度来执行,只需简单的改变注解中的属性名即可执行。下面可以每5秒执行速度在每个调用开始后:
对于固定延迟和固定速率任务, 可以指定初始延迟, 指示在第一次执行该方法之前等待的毫秒数。如下面的 fixedRate
示例所示:
如果简单的周期调度不够表达你的意愿, 则可以提供cron表达式。例如, 以下任务将只在工作日执行。
你可以使用 zone
属性来指定解析cron表达式的时区
请注意,要计划的方法必须具有void返回,并且不能期望为任何参数。如果该方法需要与应用程序上下文中的其他对象进行交互, 则通常是通过依赖项注入提供的。
在Spring 4.3框架中, 任何范围的bean都支持@Scheduled
方法。
请确保在运行时不会在同一个@Scheduled
注解类上初始化多个实例,除非您确实希望为每个此类实例都安排回调。与此相关,请确保不要在bean类上使用@Configurable
,并将其与容器@Scheduled
并注册为常规的Spring bean。如果是这样, 程序将会双重初始化,否则,一旦通过容器,并通过@Configurable切面,每个@Scheduled
方法的结果都将被调用两次。
6.3.3. @Async
注解
可以在方法上提供@Async
注解,以便发生该方法的异步调用。换言之,调用方将在调用时立即返回,并且该方法的实际执行将发生在已提交到Spring TaskExecutor
的任务中。在最简单的情况下,注解可以应用于void
返回方法。如下:
与使用@Scheduled
注解的注解方法不同,这些方法可以有预期参数的,因为调用方将在运行时以“normal”方式调用它们,而不是由容器管理的计划任务。例如,以下是@Async
注解的合法应用:
即使返回值的方法也可以异步调用,但是,此类方法需要具有Future
类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在调用Future
上的get()
之前执行其他任务。以下示例显示如何在返回值的方法上使用@Async
:
@Async
方法不仅可以声明一个常规的java.util.concurrent.Future
返回类型,而且还可能是spring的org.springframework.util.concurrent.ListenableFuture
或者如Spring 4.2版本后,存在于JDK8的java.util.concurrent.CompletableFuture
。用于与异步任务进行更丰富的交互,以及通过进一步的处理步骤进行组合操作。
@Async
不能与生命周期回调(如 @PostConstruct
)一起使用。若要异步初始化Spring bean,则当前必须使用单独的初始化Spring bean,然后在目标上调用@Async
注解方法。如下:
没有直接的XML配置等价于@Async
,因为这些方法应该首先设计为异步执行,而不是外部得来的。但是,您可以手动设置Spring的AsyncExecutionInterceptor
与Spring AOP结合使用自定义切点。
6.3.4. 使用 @Async
的Executor的条件
默认情况下,在方法上指定@Async
时,使用的执行程序是 启用异步支持时配置的执行程序,例如,如果使用XML或AsyncConfigurer
实现(如果有),则为“annotation-driven”元素。但是,当需要指示执行给定方法时,应使用非默认的执行器时,可以使用@Async
注解的value
属性。以下示例显示了如何执行此操作:
在这种情况下,"otherExecutor"
可以是Spring容器中任何Executor
bean的名称,也可以是与任何Executor
关联的限定符的名称(例如,使用<qualifier>
元素或Spring的@Qualifier
注解指定)
6.3.5. 使用@Async
的异常管理
当@Async
方法有Future
类型的返回值时,可以很容易地管理在方法执行期间引发的异常,因为当调用对Future
结果的get
时将引发此异常。但是,对于 void
返回类型,异常是无法捕获的,无法传输的。对于这些情况,可以提供AsyncUncaughtExceptionHandler
来处理此类异常。 以下示例显示了如何执行此操作::
默认情况下,仅记录异常。 您可以使用AsyncConfigurer
或<task:annotation-driven/>
XML元素定义自定义AsyncUncaughtExceptionHandler
。
6.4. task
命名空间
从Spring 3.0开始,有一个用于配置TaskExecutor
和TaskScheduler
实例的XML命名空间。
6.4.1. ‘scheduler’ 元素
下面的元素将创建具有指定线程池大小的ThreadPoolTaskScheduler
实例:
为id
属性提供的值将用作池中线程名称的前缀,scheduler
元素相对非常简单。如果不提供pool-size
属性,则默认的线程池将只有一个线程。计划程序再也没有其他配置选项。
6.4.2. executor
元素
以下创建一个ThreadPoolTaskExecutor
实例:
与上面的调度程序一样,为id
属性提供的值将用作池中线程名称的前缀。就池大小而言, executor
元素支持比scheduler
元素更多的配置选项。首先,ThreadPoolTaskExecutor
的线程池本身更具可配置。执行器的线程池可能有不同的核心值和最大大小,,而不仅仅是单个的大小。如果提供了单个值,则执行器将具有固定大小的线程池(核心和最大大小相同)。 但是,executor
元素的pool-size
属性也接受以min-max
形式的范围。以下示例将最小值设置为5
,最大值设置为25
:
从该配置中可以看出,还提供了queue-capacity
(队列容量)值。还应根据执行者的队列容量来考虑线程池的配置,有关池大小和队列容量之间关系的详细说明,请参阅ThreadPoolExecutor
的文档。 主要的想法是,在提交任务时,如果活动线程的数目当前小于核心大小,执行器将首先尝试使用一个空闲线程。如果已达到核心大小,则只要尚未达到其容量,就会将该任务添加到队列中。只有这样,如果已达到队列的容量,执行器将创建一个超出核心大小的新线程。如果还达到最大大小,,则执行器将拒绝该任务。
默认情况下,队列是无限制的,但一般不会这样配置,因为当所有池线程都运行时,如果将很多的任务添加到该队列中,则会导致OutOfMemoryErrors
。此外,如果队列是无界的,则最大大小根本没有效果。由于执行器总是在创建超出核心大小的新线程之前尝试该队列,因此队列必须具有有限的容量,以使线程池超出核心大小(这就是为什么在使用无界队列时,固定大小池是唯一合理的情况)。
如上所述,在任务被拒绝时考虑这种情况。默认情况下,当任务被拒绝时,线程池执行程序会抛出TaskRejectedException
。但是,拒绝策略实际上是可配置的。使用默认拒绝策略时抛出异常,即AbortPolicy
实现。对于可以在高负载下跳过某些任务的应用程序,您可以改为配置DiscardPolicy
或DiscardOldestPolicy
。另一个适用于需要在高负载下限制提交任务的应用程序的选项是CallerRunsPolicy
。该策略不是抛出异常或丢弃任务,而是强制调用submit方法的线程自己运行任务。这个想法是这样的调用者在运行该任务时很忙,并且不能立即提交其他任务。因此,它提供了一种简单的方法来限制传入的负载,同时保持线程池和队列的限制。通常,这允许执行程序“赶上”它正在处理的任务,从而释放队列,池中或两者中的一些容量。您可以从executor
元素上的rejection-policy
属性的可用值枚举中选择任何这些选项。
以下示例显示了一个executor
元素,其中包含许多属性以指定各种行为::
最后, keep-alive
设置确定在终止之前线程可能保持空闲的时间限制(以秒为单位)。如果池中当前有多个线程的核心数目,则在等待此时间量后不处理任务,多余的线程将被终止。时间值为零将导致多余的线程在执行任务后立即终止,而不需要在任务队列中保留后续工作。以下示例将keep-alive
值设置为两分钟::
6.4.3. ‘scheduled-tasks’ 元素
Spring的任务命名空间的最强大功能是支持配置在Spring应用程序上下文中安排的任务。这遵循了类似于Spring中其他“method-invokers”的方法,例如由JMS命名空间提供的用于配置消息驱动的pojo。 基本上, ref
属性可以指向任何Spring管理的对象, method
属性提供要在该对象上调用的方法的名称。 以下清单显示了一个简单示例::
Tscheduler由外部元素引用,每个单独的任务都包括其触发器元数据的配置。在前面的示例中,该元数据定义了一个定期触发器,它具有固定的延迟,表示每个任务执行完成后等待的毫秒数。另一个选项是”fixed-rate
”,表示无论以前执行多长时间,方法的执行频率。此外, ,对于fixed-delay
和fixed-rate
任务,可以指定’initial-delay’参数,指示在第一次执行该方法之前等待的毫秒数。为了获得更多的控制,可以改为提供一个cron
属性。下面是一个演示其他选项的示例:
6.5. 使用Quartz的Scheduler
Quartz使用Trigger
, Job
, 和 JobDetail
等对象来进行各种类型的任务调度。关于Quartz的基本概念,请参阅http://quartz-scheduler.org。为了让基于Spring的应用程序方便使用,Spring提供了一些类来简化quartz的用法。
6.5.1. 使用JobDetailFactoryBean
Quartz JobDetail
对象保存运行一个任务所需的全部信息。Spring提供一个叫作 JobDetailFactoryBean
的类让JobDetail能对一些有意义的初始值(从XML配置)进行初始化,让我们来看个例子:
job detail 配置拥有所有运行job(ExampleJob
)的必要信息。 可以通过job的data map来指定timeout。Job的data map可以通过JobExecutionContext
(在运行时传递给你)来得到,但是 JobDetail
同时把从job的data map中得到的属性映射到实际job中的属性中去。 所以,如果ExampleJob
中包含一个名为 timeout
的属性,JobDetail
将自动为它赋值:
data map中的所有附加属性当然也可以使用的
使用name
和group
属性,你可以分别修改job在哪一个组下运行和使用什么名称。默认情况下,job的名称等于JobDetailFactoryBean
的名称(在上面的例子中为exampleJob
)。
6.5.2. 使用 MethodInvokingJobDetailFactoryBean
通常情况下,你只需要调用特定对象上的一个方法即可实现任务调度。你可以使用MethodInvokingJobDetailFactoryBean
准确的做到这一点:
上面例子将调用exampleBusinessObject
中的doIt
方法如下:
使用MethodInvokingJobDetailFactoryBean
你不需要创建只有一行代码且只调用一个方法的job, 你只需要创建真实的业务对象来包装具体的细节的对象。
默认情况下,Quartz Jobs是无状态的,可能导致jobs之间互相的影响。如果你为相同的JobDetail
指定两个Trigger, 很可能当第一个job完成之前,第二个job就开始了。如果JobDetail
对象实现了Stateful
接口,就不会发生这样的事情。 第二个job将不会在第一个job完成之前开始。为了使得jobs不并发运行,设置MethodInvokingJobDetailFactoryBean
中的concurrent
标记为false
:
默认情况下,jobs在并发的方式下运行。
6.5.3. 使用triggers和SchedulerFactoryBean
来织入任务
我们已经创建了job details,jobs。我们同时回顾了允许你调用特定对象上某一个方法的便捷的bean。 当然我们仍需要调度这些jobs。这需要使用triggers和SchedulerFactoryBean
来完成。 Quartz中提供了几个triggers,Spring提供了两个带有方便默认值的Quartz FactoryBean
实现:CronTriggerFactoryBean
和SimpleTriggerFactoryBean
。
Triggers也需要被调度。Spring提供SchedulerFactoryBean
来公开一些属性来设置triggers。SchedulerFactoryBean
负责调度那些实际的triggers
以下清单使用SimpleTriggerFactoryBean
和CronTriggerFactoryBean
:
现在我们创建了两个triggers,其中一个开始延迟10秒以后每50秒运行一次,另一个每天早上6点钟运行。 我们需要创建一个SchedulerFactoryBean
来最终实现上述的一切:
更多的属性你可以通过SchedulerFactoryBean
来设置,例如job details使用的日期, 用来订制Quartz的一些属性以及其它相关信息。 你可以查阅SchedulerFactoryBean
的Javadoc。
Rod Johnson, Juergen Hoeller, Keith Donald, Colin Sampaleanu, Rob Harrop, Thomas Risberg, Alef Arendsen, Darren Davison, Dmitriy Kopylenko, Mark Pollack, Thierry Templier, Erwin Vervaet, Portia Tung, Ben Hale, Adrian Colyer, John Lewis, Costin Leau, Mark Fisher, Sam Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clement, Dave Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch
Copyright © 2002 - 2024 VMware, Inc. All Rights Reserved.
Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.