Task Execution and Scheduling
-
Trigger
-
CronTrigger
-
TriggerContext
-
Scheduled
-
Async
-
@EnableScheduling
-
@Scheduled
-
@Async
-
task:scheduler
-
task:executor
-
task:scheduled-tasks
Spring 框架提供抽象操作,可通过 TaskExecutor
和 TaskScheduler
接口分别异步执行和计划任务。Spring 还提供对这些接口的实现,支持应用程序服务器环境中的线程池或委派到 CommonJ。最终,在常用接口后面使用这些实现从 Java SE 和 Jakarta EE 环境中抽象出差异。
Spring 还提供集成类来支持使用 Quartz Scheduler 进行计划。
The Spring TaskExecutor
Abstraction
Executor 是 JDK 中线程池概念的名称。“executor
” 命名归因于一个事实,即无法保证底层实现事实上是一个池。执行器可能是单线程,甚至是同步的。Spring 的抽象层隐藏了 Java SE 和 Jakarta EE 环境中的实现详情。
Spring 的 TaskExecutor
接口与 java.util.concurrent.Executor
接口相同。实际上,最初,它存在的主要原因是为了在使用线程池时抽象掉对 Java 5 的需求。该接口具有一个方法(execute(Runnable task)
),它基于线程池的语义和配置接受一个任务执行。
TaskExecutor
最初是为了为其他需要线程池的 Spring 组件提供抽象而创建的。诸如 ApplicationEventMulticaster
、JMS 中的 AbstractMessageListenerContainer
和 Quartz 集成的组件都使用 TaskExecutor
抽象来池化线程。然而,如果你的 Bean 需要线程池行为,你也可以将此抽象用于你的自身需求。
TaskExecutor
Types
Spring 包括许多 TaskExecutor
的预构建实现。你很可能永远不需要实现自己的实现。Spring 提供的变体如下:
-
SyncTaskExecutor
:此实现不会异步运行调用。相反,每个调用都在调用线程中发生。它主要用于不需要多线程的情况中,例如在简单的测试用例中。 -
SimpleAsyncTaskExecutor
:此实现不会复用任何线程。反之,它为每个调用启动一个新的线程。但是,它支持并发限制,该限制会阻止超出限制的所有调用,直到腾出一个位置为止。如果您正在寻找真正的池化,请参见此列表后面的ThreadPoolTaskExecutor
。 -
ConcurrentTaskExecutor
:此实现是java.util.concurrent.Executor
的适配器实例。有另一个 (ThreadPoolTaskExecutor
) 将Executor
配置参数作为 Bean 属性公开。很少需要直接使用ConcurrentTaskExecutor
。但是,如果ThreadPoolTaskExecutor
不够灵活以满足您的需要,则ConcurrentTaskExecutor
是一个可选方案。 -
ThreadPoolTaskExecutor
:此实现是使用最频繁的。它为配置java.util.concurrent.ThreadPoolExecutor
而公开 Bean 属性并将其包装在TaskExecutor
中。如果您需要对不同类型的java.util.concurrent.Executor
进行适配,我们建议您改用ConcurrentTaskExecutor
。 -
DefaultManagedTaskExecutor
:此实现使用 JNDI 获取的ManagedExecutorService
处于与 JSR-236 兼容的运行时环境(如 Jakarta EE 应用程序服务器)中,用 CommonJ WorkManager 代替后者的用途。
从 6.1 开始,ThreadPoolTaskExecutor
提供了一个暂停/恢复功能,并通过 Spring 的生命周期管理来优雅关闭。SimpleAsyncTaskExecutor
上还有一个新的“virtualThreads”选项,它与 JDK 21 的虚拟线程保持一致,以及为 SimpleAsyncTaskExecutor
提供的优雅关闭功能。
Using a TaskExecutor
Spring 的 TaskExecutor
实现通常与依赖注入一起使用。在以下示例中,我们定义了一个使用 ThreadPoolTaskExecutor
来异步打印一组消息的 Bean:
-
Java
-
Kotlin
public class TaskExecutorExample {
private class MessagePrinterTask implements Runnable {
private String message;
public MessagePrinterTask(String message) {
this.message = message;
}
public void run() {
System.out.println(message);
}
}
private TaskExecutor taskExecutor;
public TaskExecutorExample(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
public void printMessages() {
for(int i = 0; i < 25; i++) {
taskExecutor.execute(new MessagePrinterTask("Message" + i));
}
}
}
class TaskExecutorExample(private val taskExecutor: TaskExecutor) {
private inner class MessagePrinterTask(private val message: String) : Runnable {
override fun run() {
println(message)
}
}
fun printMessages() {
for (i in 0..24) {
taskExecutor.execute(
MessagePrinterTask(
"Message$i"
)
)
}
}
}
如你所见,不是从池中检索线程并自己执行线程,而是将 Runnable
添加到队列中。然后,TaskExecutor
会使用其内部规则来决定在何时运行任务。
为了配置 TaskExecutor
使用的规则,我们公开了简单的 Bean 属性:
-
Java
-
Kotlin
-
Xml
@Bean
ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(5);
taskExecutor.setMaxPoolSize(10);
taskExecutor.setQueueCapacity(25);
return taskExecutor;
}
@Bean
TaskExecutorExample taskExecutorExample(ThreadPoolTaskExecutor taskExecutor) {
return new TaskExecutorExample(taskExecutor);
}
@Bean
fun taskExecutor() = ThreadPoolTaskExecutor().apply {
corePoolSize = 5
maxPoolSize = 10
queueCapacity = 25
}
@Bean
fun taskExecutorExample(taskExecutor: ThreadPoolTaskExecutor) = TaskExecutorExample(taskExecutor)
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="10"/>
<property name="queueCapacity" value="25"/>
</bean>
<bean id="taskExecutorExample" class="TaskExecutorExample">
<constructor-arg ref="taskExecutor"/>
</bean>
The Spring TaskScheduler
Abstraction
除了 TaskExecutor
抽象层外,Spring 还具有 TaskScheduler
SPI,它提供了多种方法来计划任务以便某个时间点运行。以下清单显示了 TaskScheduler
接口定义:
public interface TaskScheduler {
Clock getClock();
ScheduledFuture schedule(Runnable task, Trigger trigger);
ScheduledFuture schedule(Runnable task, Instant startTime);
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
最简单的方法是一个名为 schedule
的方法,它只采用一个 Runnable
和一个 Instant
。该方法会导致任务在指定时间后运行一次。所有其他方法都能够计划任务重复运行。固定速率和固定延迟方法用于简单,周期性执行,但是接受 Trigger
的方法要灵活得多。
Trigger
Interface
Trigger
接口基本上受 JSR-236 启发。Trigger
的基本思想是执行时间可能会根据过去的执行结果甚至任意条件来确定。如果这些确定会考虑前一个执行的结果,那么这些信息将在 TriggerContext
中可用。Trigger
接口本身非常简单,如下面的清单所示:
public interface Trigger {
Instant nextExecution(TriggerContext triggerContext);
}
TriggerContext
是最重要的部分。它封装了所有相关数据,并且将来如果需要,可以进行扩展。TriggerContext
是一个接口(默认情况下使用 SimpleTriggerContext
实现)。以下清单显示了 Trigger
实现可用的方法。
public interface TriggerContext {
Clock getClock();
Instant lastScheduledExecution();
Instant lastActualExecution();
Instant lastCompletion();
}
Trigger
Implementations
Spring 提供了 Trigger
接口的两种实现。最有趣的是 CronTrigger
。它支持基于 cron expressions 的任务计划。例如,以下任务计划在每小时过去 15 分钟后运行,但仅限于工作日的 9 点至 5 点的“工作时间”:
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
另一个实现是接受固定周期、可选初始延迟值和一个布尔值来指示 period 应解释为固定速率还是固定延迟的 PeriodicTrigger
。由于 TaskScheduler
接口已定义了用于以固定速率或以固定延迟计划任务的方法,因此应尽可能直接使用这些方法。PeriodicTrigger
实现的价值在于你可以在依赖 Trigger
抽象的组件中使用该实现。例如,允许交替使用周期性触发器、基于 Cron 的触发器,甚至是自定义触发器实现可能很方便。这样的组件可以利用依赖注入,以便你能够在外部配置此类 Triggers
,从而轻松修改或扩展它们。
TaskScheduler
implementations
与 Spring 的 TaskExecutor
抽象层一样,TaskScheduler
设置的主要优点在于将应用程序的调度需求与部署环境分离。在将应用程序部署到应用程序服务器的环境中(应用程序本身不应直接创建线程)时,此抽象级别尤为重要。对于此类方案,Spring 提供了一个 DefaultManagedTaskScheduler
,该调度器在 Jakarta EE 环境中委派给 JSR-236 ManagedScheduledExecutorService
。
只要不需要外部线程管理,更简单的替代方案是应用程序中安装一个本地 ScheduledExecutorService
,它可以通过 Spring 的 ConcurrentTaskScheduler
进行调整。为了方便起见,Spring 还提供了一个 ThreadPoolTaskScheduler
,它在内部委派给 ScheduledExecutorService
来提供类似于 ThreadPoolTaskExecutor
的通用 Bean 样式配置。这些变体对于宽松的应用程序服务器环境中的本地嵌入式线程池安装同样非常有效,尤其是在 Tomcat 和 Jetty 上。
从 6.1 开始,ThreadPoolTaskScheduler
通过 Spring 的生命周期管理提供了暂停/恢复功能和优雅的关闭功能。还有一个称为 SimpleAsyncTaskScheduler
的新选项,该选项与 JDK 21 的 Virtual Threads 对齐,它使用单个调度程序线程,但为每个计划的任务执行启动一个新线程(除了固定延迟任务的所有任务都在单个调度程序线程上运行,因此对于此基于虚拟线程的选项,建议使用固定速率和 cron 触发器)。
Annotation Support for Scheduling and Asynchronous Execution
Spring 为任务计划和异步方法执行提供注释支持。
Enable Scheduling Annotations
要启用对 @Scheduled
和 @Async
注释的支持,可以将 @EnableScheduling
和 @EnableAsync
添加到您的一个 @Configuration
类,或 <task:annotation-driven>
元素,如下面的示例所示:
-
Java
-
Kotlin
-
Xml
@Configuration
@EnableAsync
@EnableScheduling
public class SchedulingConfiguration {
}
@Configuration
@EnableAsync
@EnableScheduling
class SchedulingConfiguration
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:task="http://www.springframework.org/schema/task"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/task
https://www.springframework.org/schema/task/spring-task.xsd">
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
</beans>
你可以挑选并选择与你的应用程序关联的注释。例如,如果你仅需要 @Scheduled
的支持,那么你可以省略 @EnableAsync
。为了进行更精细的控制,你还可以额外地实现 SchedulingConfigurer
接口、AsyncConfigurer
接口或两者。请参见 SchedulingConfigurer
和 AsyncConfigurer
javadoc 以获得详细信息。
请注意,在之前的 XML 中,提供了一个执行程序引用来处理那些与带有 @Async
注释的方法相对应任务,而调度程序引用是用来管理那些用 @Scheduled
注释的方法的。
用于处理 |
The @Scheduled
annotation
您可以将 @Scheduled
注释添加到方法中,以及触发器元数据。例如,以下方法每隔五秒(5000 毫秒)使用固定延迟触发一次,这意味着该周期从每次前一次调用的完成时间开始计算。
@Scheduled(fixedDelay = 5000)
public void doSomething() {
// something that should run periodically
}
默认情况下,毫秒将用作固定延迟、固定速率和初始延迟值的单位。如果您希望使用其他时间单位(如秒或分钟),可以通过
|
如果您需要固定速率执行,则可以在注释中使用 fixedRate
属性。以下方法每隔五秒(从每次调用的连续开始时间测量)触发一次:
@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
// something that should run periodically
}
对于固定延迟和固定速率任务,您可以通过指示在方法的首次执行前等待的时间量来指定初始延迟,如下面的 fixedRate
示例所示:
@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void doSomething() {
// something that should run periodically
}
对于一次性任务,您可以仅仅通过指示在方法的预期执行前等待的时间量来指定初始延迟:
@Scheduled(initialDelay = 1000)
public void doSomething() {
// something that should run only once
}
如果简单的周期性计划不够明确,你可以提供一个 cron expression。以下示例仅在工作日运行:
@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
// something that should run on weekdays only
}
您还可以使用 |
请注意,要调度的那些方法必须具有 void 返回值,并且不得接受任何参数。如果方法需要与应用程序上下文的其他对象进行交互,则通常会通过依赖项注入提供这些对象。
@Scheduled
可用作可重复注释。如果在同一个方法上找到多个计划声明,则会独立处理它们中的每一个,并为它们中的每一个触发一个单独的触发器。因此,这样的并置计划可能会重叠,并行或立即连续执行多次。请确保您指定的 cron 表达式等不会意外重叠。
从 Spring Framework 4.3 开始, |
The @Scheduled
annotation on Reactive methods or Kotlin suspending functions
从 Spring Framework 6.1 开始,@Scheduled
方法还受支持于多种类型的响应方法:
-
带有
Publisher
返回类型(或Publisher
的任何具体实现)的方法,如下面的示例所示:
@Scheduled(fixedDelay = 500)
public Publisher<Void> reactiveSomething() {
// return an instance of Publisher
}
-
具有可通过
ReactiveAdapterRegistry
共享实例适配到Publisher
的返回类型的方法,前提是该类型支持 deferred subscription,如下面的示例所示:
@Scheduled(fixedDelay = 500)
public Single<String> rxjavaNonPublisher() {
return Single.just("example");
}
|
-
Kotlin 挂起函数,如下面的示例所示:
@Scheduled(fixedDelay = 500)
suspend fun something() {
// do something asynchronous
}
-
返回 Kotlin
Flow
或Deferred
实例的方法,如下面的示例所示:
@Scheduled(fixedDelay = 500)
fun something(): Flow<Void> {
flow {
// do something asynchronous
}
}
所有这些类型的办法都必须在没有参数的情况下宣告。在 Kotlinsuspending 函数的情况下,kotlinx.coroutines.reactor
桥接器也必须存在,以允许框架调用一个 suspending 函数作为 Publisher
。
Spring 框架将为带注释的方法获取一个 Publisher
一次,并将会安排一个 Runnable
,它将在其中订阅所述 Publisher
。这些内部的规则订阅根据相应的 cron
/fixedDelay
/fixedRate
配置发生。
如果 Publisher
发射一个或多个 onNext
信号,它们将被忽略并丢弃(以与同步 @Scheduled
方法的返回值被忽略相同的方式)。
在下例中,Flux
每 5 秒发出 onNext("Hello")
、onNext("World")
,但是这些值没有被使用:
@Scheduled(initialDelay = 5000, fixedRate = 5000)
public Flux<String> reactiveSomething() {
return Flux.just("Hello", "World");
}
如果 Publisher
发射一个 onError
信号,它将在 WARN
级别被记录并恢复。由于 Publisher
实例的异步和惰性特性,异常不会从 Runnable
任务中被抛出:这意味着 ErrorHandler
合同不会涉及响应式方法。
因此,尽管有错误,后续计划订阅仍会发生。
在下例中,Mono
订阅在前 5 秒内失败了 2 次。然后订阅开始生效,每 5 秒向标准输出打印一条消息:
@Scheduled(initialDelay = 0, fixedRate = 5000)
public Mono<Void> reactiveSomething() {
AtomicInteger countdown = new AtomicInteger(2);
return Mono.defer(() -> {
if (countDown.get() == 0 || countDown.decrementAndGet() == 0) {
return Mono.fromRunnable(() -> System.out.println("Message"));
}
return Mono.error(new IllegalStateException("Cannot deliver message"));
})
}
当销毁带注释的 bean 或关闭应用程序上下文时,Spring 框架会取消计划任务,其中包括下一个计划的 |
The @Async
annotation
您可以在方法上提供 @Async
注释,以便异步调用该方法。换句话说,在调用时,调用者会立即返回,而方法的实际执行发生在已提交到 Spring TaskExecutor
的任务中。在最简单的情况下,您可以将此注释应用到返回 void
的方法,如下例所示:
@Async
void doSomething() {
// this will be run asynchronously
}
与使用 @Scheduled
注释注释的方法不同,这些方法可以期待参数,因为它们是由调用者在运行时以“正常”的方式调用的,而不是由容器管理的计划任务调用的。例如,以下代码是 @Async
注释的合法应用:
@Async
void doSomething(String s) {
// this will be run asynchronously
}
即使是返回一个值的方法也可以异步调用。但是,这样的方法必须具有 Future
类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在调用 Future
上的 get()
之前执行其他任务。以下示例展示了如何在返回一个值的方法上使用 @Async
:
@Async
Future<String> returnSomething(int i) {
// this will be run asynchronously
}
|
您不能将 @Async
与生命周期回调结合使用,例如 @PostConstruct
。要异步初始化 Spring bean,您当前必须使用一个单独的初始化 Spring bean,然后在目标上调用 @Async
注释的方法,如下例所示:
public class SampleBeanImpl implements SampleBean {
@Async
void doSomething() {
// ...
}
}
public class SampleBeanInitializer {
private final SampleBean bean;
public SampleBeanInitializer(SampleBean bean) {
this.bean = bean;
}
@PostConstruct
public void initialize() {
bean.doSomething();
}
}
|
Executor Qualification with @Async
默认情况下,当在方法上指定 @Async
时,所使用的执行器是 configured when enabling async support, 即 “annotation-driven” 元素(如果你使用的是 XML)或你的 AsyncConfigurer
实现(如果有)。然而,当你需要表明在执行给定方法时需要使用一个不同于默认执行器的执行器时,可以使用 @Async
注解的 value
属性。以下示例展示了如何这样做:
@Async("otherExecutor")
void doSomething(String s) {
// this will be run asynchronously by "otherExecutor"
}
在这种情况下,“otherExecutor
”可以是 Spring 容器中任何 Executor
bean 的名称,或者它可以是与任何 Executor
关联的限定符的名称(例如,使用 <qualifier>
元素或 Spring 的 @Qualifier
注释指定)。
Exception Management with @Async
当一个 @Async
方法具有 Future
类型的返回值时,很容易管理在方法执行期间抛出的异常,因为在调用 Future
结果上的 get
时会抛出此异常。但是,对于 void
返回类型,异常不会被捕获且无法被传输。您可以提供一个 AsyncUncaughtExceptionHandler
来处理此类异常。以下示例展示了如何做到这一点:
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
// handle exception
}
}
默认情况下,异常只是被记录。您可以使用 AsyncConfigurer
或 <task:annotation-driven/>
XML 元素定义一个自定义 AsyncUncaughtExceptionHandler
。
The task
Namespace
从版本 3.0 开始,Spring 包含了一个 XML 命名空间,用于配置 TaskExecutor
和 TaskScheduler
实例。它还提供了一种方便的方法来配置要使用触发器调度的任务。
The 'scheduler' Element
以下元素使用指定的线程池大小创建 ThreadPoolTaskScheduler
实例:
<task:scheduler id="scheduler" pool-size="10"/>
为 id
属性提供的值被用作池中线程名称的前缀。scheduler
元素相对简单。如果您没有提供 pool-size
属性,则默认线程池只有一个线程。调度器没有其他配置选项。
The executor
Element
以下内容创建一个 ThreadPoolTaskExecutor
实例:
<task:executor id="executor" pool-size="10"/>
与 previous section 中所示的计划程序一样,为 id
属性提供的值被用作池内线程名称的前缀。就池大小而言,executor
元素支持比 scheduler
元素更多的配置选项。首先,对于 ThreadPoolTaskExecutor
的线程池本身更具可配置性。执行器的线程池不仅仅是单个大小,还可以为核心大小和最大大小设置不同的值。如果你提供一个单一值,则执行器具有固定大小的线程池(核心和最大大小相同)。然而,executor
元素的 pool-size
属性也接受范围的形式 min-max
。以下示例设置了一个最小值 5
和一个最大值 25
:
<task:executor
id="executorWithPoolSizeRange"
pool-size="5-25"
queue-capacity="100"/>
在之前的配置中,还提供了 queue-capacity
值。线程池的配置还应根据执行器的队列容量进行考虑。有关池大小和队列容量之间关系的完整说明,请参阅 ThreadPoolExecutor
的文档 https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html。主要思想是,当提交一项任务时,如果活动线程数当前小于核心大小,执行器首先尝试使用一个空闲线程。如果已经达到核心大小,则会将该任务添加到队列中,只要其容量尚未达到上限。仅在队列的容量达到上限时,执行器才会创建一个超过核心大小的新线程。如果也达到最大值,则执行器会拒绝该任务。
默认情况下,队列是无界的,但这很少是期望的配置,因为如果所有池线程都处于繁忙状态时,向该队列添加足够的任务可能会导致 OutOfMemoryErrors
。此外,如果队列是无界的,则最大值没有任何效果。由于执行器在创建超过核心大小的新线程之前总是先尝试队列,因此队列必须具有有限的容量,以便线程池增长超过核心大小(这就是固定大小池在使用无界队列时是唯一合理的场景的原因)。
考虑上述所提到的场景,即当一个任务被拒绝时。默认情况下,当一个任务被拒绝时,线程池执行器会抛出一个 TaskRejectedException
。然而,拒绝策略实际上是可配置的。当使用默认拒绝策略(即 AbortPolicy
实现)时,会抛出异常。对于重负荷下可以跳过某些任务的应用程序,你可以配置 DiscardPolicy
或 DiscardOldestPolicy
。另一种适用于需要在重负载下限制已提交的任务的应用程序的选项是 CallerRunsPolicy
。该策略不会抛出异常或丢弃任务,而是强制调用提交方法的线程运行任务本身。其思想是,此类调用方在运行该任务时会处于繁忙状态,并且无法立即提交其他任务。因此,它提供了限制传入负载(同时保持线程池和队列的限制)的简单方法。通常情况下,这允许执行器“赶上”它正在处理的任务,从而释放队列、池或两者的部分容量。你可以从 executor
元素上 rejection-policy
属性的枚举值中选择任何这些选项。
以下示例显示了一个 executor
元素,该元素具有多个属性以指定各种行为:
<task:executor
id="executorWithCallerRunsPolicy"
pool-size="5-25"
queue-capacity="100"
rejection-policy="CALLER_RUNS"/>
最后,keep-alive
设置确定线程在停止之前可以保持空闲状态的时间限制(以秒为单位)。如果池中当前的线程数量超过核心数量,则在等待此段时间且没有处理任务后,将停止多余的线程。零值会造成多余的线程在执行任务且任务队列中没有后续工作后立即停止。以下示例将 keep-alive
值设置为两分钟:
<task:executor
id="executorWithKeepAlive"
pool-size="5-25"
keep-alive="120"/>
The 'scheduled-tasks' Element
Spring 任务命名空间最强大的功能是对在 Spring Application Context 中计划的任务进行配置的支持。这种方式与 Spring 中其他“方法调用者”类似,例如 JMS 命名空间为配置消息驱动的 POJO 提供的方法调用者。基本上,ref
属性可以指向任何 Spring 管理的对象,而 method
属性提供要在该对象上调用的方法的名称。以下列表显示了一个简单的示例:
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
调度程序由外部元素引用,每个单独的任务都包含其触发器元数据配置。在前一个示例中,元数据定义了一个周期性触发器,其中固定延迟指定在每个任务执行完成后等待的毫秒数。另一种选择是 fixed-rate
, 它指示该方法应运行的频率,而不考虑任何先前执行需要多长时间。此外,对于 fixed-delay
和 fixed-rate
任务,你可以指定一个“初始延迟”参数,它指示在方法首次执行之前要等待的毫秒数。为了更好地控制,你可以提供一个 cron
属性来提供一个 cron expression。以下示例展示了这些其他选项:
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/>
<task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/>
<task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
Cron Expressions
无论是在 @Scheduled
annotations,task:scheduled-tasks
elements, 还是在其他地方使用,所有的 Spring cron 表达式都必须符合相同的格式。有效的 cron 表达式(如 * * * * * *
)包含六个以空格分隔的时间和日期字段,每个字段都有自己的有效值范围:
┌───────────── second (0-59) │ ┌───────────── minute (0 - 59) │ │ ┌───────────── hour (0 - 23) │ │ │ ┌───────────── day of the month (1 - 31) │ │ │ │ ┌───────────── month (1 - 12) (or JAN-DEC) │ │ │ │ │ ┌───────────── day of the week (0 - 7) │ │ │ │ │ │ (0 or 7 is Sunday, or MON-SUN) │ │ │ │ │ │ * * * * * *
有一些适用的规则:
-
字段可以是星号 (
*
),它始终代表 “first-last”。对于本月日期或星期几的字段,可以使用问号 (?
) 代替星号。 -
使用逗号 (
,
) 分隔列表的各个项目。 -
用连字符 (
-
) 分隔的两个数字表示数字范围。指定的范围是包含的。 -
使用
/
跟在范围 (或*
) 之后即可指定数字值在范围内的间隔。 -
月份和星期几的字段也可以使用英文名称。使用特定日期或月份的前三个字母(大小写无关紧要)。
-
本月日期和星期几的字段可以包含
L
字符,该字符具有不同的含义。-
在本月日期字段中,
L
表示 the last day of the month。如果后跟负偏移量(即L-n
),则表示 `n`th-to-last day of the month。 -
在星期几的字段中,
L
表示 the last day of the week。如果以数字或三个字母的名称为前缀 (dL
或DDDL
),则表示 the last day of week (d
orDDD
) in the month。
-
-
本月日期字段可以是
nW
,表示 the nearest weekday to day of the monthn`
. If `n falls on Saturday, this yields the Friday before it. Ifn
falls on Sunday, this yields the Monday after, which also happens ifn
is1
and falls on a Saturday (that is:1W
stands for the first weekday of the month)。 -
如果本月日期字段为
LW
,则表示 the last weekday of the month。 -
星期几的字段可以是
d#n
(或DDD#n
),表示 then`th day of week `d
(orDDD
) in the month。
这里一些示例:
Cron Expression | Meaning |
---|---|
|
每天的每小时整点 |
|
every ten seconds |
|
每天的 8、9 点和 10 点 |
|
每天上午 6:00 和下午 7:00 |
|
每天 8:00、8:30、9:00、9:30、10:00 和 10:30 |
|
每天九点至下午五点工作日 |
|
每年的圣诞节午夜 |
|
每月最后一天的午夜 |
|
每月倒数第三天的午夜 |
|
每月最后一个星期五的午夜 |
|
每月最后一个星期四的午夜 |
|
每月第一个星期几午夜 |
|
每月最后一个星期几午夜 |
|
每月第二个星期五午夜 |
|
每月第一个星期一午夜 |
Macros
诸如 0 0 * * * *
的表达式对人类来说很难解析,因此在出现错误的情况下很难修复。为了提高可读性,Spring 支持以下宏,表示常用的序列。你可以使用这些宏代替六位数字的值,如下所示:@Scheduled(cron = "@hourly")
。
Macro | Meaning |
---|---|
|
每年一次 ( |
|
每月一次 ( |
|
每周一次 ( |
|
每天一次 ( |
|
或每小时一次 ( |
Using the Quartz Scheduler
Quartz 使用 Trigger
, Job
, 和 JobDetail
对象来实现各种作业的调度。有关 Quartz 背后的基本概念,请参阅 Quartz Web site。为了方便,Spring 提供了几种类,简化了在基于 Spring 的应用程序中使用 Quartz。
Using the JobDetailFactoryBean
Quartz JobDetail
对象包含运行作业所需的所有信息。Spring 提供了一个 JobDetailFactoryBean
,它为 XML 配置目的提供了 bean 样式的属性。考虑以下示例:
<bean name="exampleJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="example.ExampleJob"/>
<property name="jobDataAsMap">
<map>
<entry key="timeout" value="5"/>
</map>
</property>
</bean>
作业详细配置具有运行作业(ExampleJob
)所需的所有信息。超时在作业数据映射中指定。作业数据映射可通过 JobExecutionContext
(在执行时传递给你)获得,但 JobDetail
也从映射到作业实例属性的作业数据获取其属性。因此,在以下示例中,ExampleJob
包含一个名为 timeout
的 bean 属性,JobDetail
会自动应用该属性:
public class ExampleJob extends QuartzJobBean {
private int timeout;
/**
* Setter called after the ExampleJob is instantiated
* with the value from the JobDetailFactoryBean.
*/
public void setTimeout(int timeout) {
this.timeout = timeout;
}
protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
// do the actual work
}
}
作业数据映射中的所有其他属性也对你可用。
通过使用 |
Using the MethodInvokingJobDetailFactoryBean
通常,你只需要在特定对象上调用一个方法。通过使用 MethodInvokingJobDetailFactoryBean
,你可以执行此操作,如下面的示例所示:
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
</bean>
上一个示例导致在 exampleBusinessObject
方法上调用 doIt
方法,如下一个示例所示:
public class ExampleBusinessObject {
// properties and collaborators
public void doIt() {
// do the actual work
}
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>
通过使用 MethodInvokingJobDetailFactoryBean
,无需创建仅调用方法的单行作业。只需创建实际业务对象并连接详细信息对象即可。
默认情况下,Quartz 作业是无状态的,这会导致作业相互干扰的可能性。如果针对同一个 JobDetail
指定两个触发器,第二个触发器有可能在第一个作业完成之前启动。如果 JobDetail
类实现了 Stateful
接口,这种情况不会发生:第二个作业不会在第一个作业完成之前启动。
若要使 MethodInvokingJobDetailFactoryBean
产生的作业变为非并发,请将 concurrent
标志设置为 false
,如下例所示:
<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="exampleBusinessObject"/>
<property name="targetMethod" value="doIt"/>
<property name="concurrent" value="false"/>
</bean>
默认情况下,作业将以并发的方式运行。 |
Wiring up Jobs by Using Triggers and SchedulerFactoryBean
我们已经创建了作业详细信息和作业。我们还查看了让你在一个特定对象中调用方法的便捷 Bean。当然,我们仍然需要安排作业本身。这是通过使用触发器和 SchedulerFactoryBean
来完成的。在 Quartz 中有几种触发器可用,而 Spring 提供了两个 Quartz FactoryBean
实现,带有便捷的默认值:CronTriggerFactoryBean
和 SimpleTriggerFactoryBean
。
需要安排触发器。Spring 提供了一个 SchedulerFactoryBean
,用于将触发器公开为属性设置。SchedulerFactoryBean
使用这些触发器安排实际作业。
以下清单同时使用了 SimpleTriggerFactoryBean
和 CronTriggerFactoryBean
:
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<!-- see the example of method invoking job above -->
<property name="jobDetail" ref="jobDetail"/>
<!-- 10 seconds -->
<property name="startDelay" value="10000"/>
<!-- repeat every 50 seconds -->
<property name="repeatInterval" value="50000"/>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="exampleJob"/>
<!-- run every morning at 6 AM -->
<property name="cronExpression" value="0 0 6 * * ?"/>
</bean>
前一个示例设置了两个触发器,一个每 50 秒运行一次,启动延迟 10 秒,另一个每天上午 6 点运行一次。为最终确定一切,我们需要设置 SchedulerFactoryBean
,如下例所示:
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
<ref bean="simpleTrigger"/>
</list>
</property>
</bean>
SchedulerFactoryBean
有更多属性可用,例如任务详细信息使用的日历、用于自定义 Quartz 的属性以及 Spring 提供的 JDBC DataSource。请参见 SchedulerFactoryBean
javadoc 以获取更多信息。
|