Task Execution and Scheduling

  • Trigger

  • CronTrigger

  • TriggerContext

  • Scheduled

  • Async

  • @EnableScheduling

  • @Scheduled

  • @Async

  • task:scheduler

  • task:executor

  • task:scheduled-tasks

Spring 框架提供抽象操作,可通过 TaskExecutorTaskScheduler 接口分别异步执行和计划任务。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 接口或两者。请参见 SchedulingConfigurerAsyncConfigurer javadoc 以获得详细信息。

请注意,在之前的 XML 中,提供了一个执行程序引用来处理那些与带有 @Async 注释的方法相对应任务,而调度程序引用是用来管理那些用 @Scheduled 注释的方法的。

用于处理 @Async 注释的默认建议模式是 proxy,它仅允许通过代理拦截调用。此方式不能拦截同一类中本地调用。对于更高级的拦截模式,考虑结合编译时或加载时编织切换到 aspectj 模式。

The @Scheduled annotation

您可以将 @Scheduled 注释添加到方法中,以及触发器元数据。例如,以下方法每隔五秒(5000 毫秒)使用固定延迟触发一次,这意味着该周期从每次前一次调用的完成时间开始计算。

@Scheduled(fixedDelay = 5000)
public void doSomething() {
	// something that should run periodically
}

默认情况下,毫秒将用作固定延迟、固定速率和初始延迟值的单位。如果您希望使用其他时间单位(如秒或分钟),可以通过 @Scheduled 中的 timeUnit 属性对其进行配置。 例如,也可以将前一个示例写成如下形式。

@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
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
}

您还可以使用 zone 属性指定从中解析 cron 表达式的时区。

请注意,要调度的那些方法必须具有 void 返回值,并且不得接受任何参数。如果方法需要与应用程序上下文的其他对象进行交互,则通常会通过依赖项注入提供这些对象。

@Scheduled 可用作可重复注释。如果在同一个方法上找到多个计划声明,则会独立处理它们中的每一个,并为它们中的每一个触发一个单独的触发器。因此,这样的并置计划可能会重叠,并行或立即连续执行多次。请确保您指定的 cron 表达式等不会意外重叠。

从 Spring Framework 4.3 开始,@Scheduled 方法受支持于任何范围的 bean。 确保在运行时不初始化同一 @Scheduled 注解类的多个实例,除非希望计划对每个此类实例进行回调。与此相关,确保不对用 @Scheduled 注解并作为常规 Spring Bean 注册到容器的 Bean 类使用 @Configurable。否则,将进行双重初始化(一次通过容器,一次通过 @Configurable 方面),从而导致每个 @Scheduled 方法被调用两次。

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");
}

CompletableFuture 类是可以典型地适应到 Publisher 但不支持延迟订阅的类型的示例。它在注册表中的 ReactiveAdapter 通过带有 getDescriptor().isDeferred() 方法返回 false 表示这一点。

  • Kotlin 挂起函数,如下面的示例所示:

@Scheduled(fixedDelay = 500)
suspend fun something() {
	// do something asynchronous
}
  • 返回 Kotlin FlowDeferred 实例的方法,如下面的示例所示:

@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 框架会取消计划任务,其中包括下一个计划的 Publisher 订阅,以及任何仍然处于活动状态的过去订阅(例如对于长期运行的发布者,甚至无限的发布者)。

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 方法不仅可以声明常规 java.util.concurrent.Future 返回类型,还可以声明 Spring’s org.springframework.util.concurrent.ListenableFuture 或 Spring 4.2 的 java.util.concurrent.CompletableFuture,以便与异步任务进行更丰富的交互,并立即组合进一步的处理步骤。

您不能将 @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();
	}

}

@Async 没有直接对应的 XML 等效项,因为此类方法一开始就应设计为异步执行,而不是外部重新声明为异步。但是,您可以结合自定义切入点,使用 Spring AOP 手动设置 Spring’s AsyncExecutionInterceptor

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 命名空间,用于配置 TaskExecutorTaskScheduler 实例。它还提供了一种方便的方法来配置要使用触发器调度的任务。

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

默认情况下,队列是无界的,但这很少是期望的配置,因为如果所有池线程都处于繁忙状态时,向该队列添加足够的任务可能会导致 OutOfMemoryErrors。此外,如果队列是无界的,则最大值没有任何效果。由于执行器在创建超过核心大小的新线程之前总是先尝试队列,因此队列必须具有有限的容量,以便线程池增长超过核心大小(这就是固定大小池在使用无界队列时是唯一合理的场景的原因)。

考虑上述所提到的场景,即当一个任务被拒绝时。默认情况下,当一个任务被拒绝时,线程池执行器会抛出一个 TaskRejectedException。然而,拒绝策略实际上是可配置的。当使用默认拒绝策略(即 AbortPolicy 实现)时,会抛出异常。对于重负荷下可以跳过某些任务的应用程序,你可以配置 DiscardPolicyDiscardOldestPolicy。另一种适用于需要在重负载下限制已提交的任务的应用程序的选项是 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-delayfixed-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。如果以数字或三个字母的名称为前缀 (dLDDDL),则表示 the last day of week (d or DDD) in the month

  • 本月日期字段可以是 nW,表示 the nearest weekday to day of the month n`. If `n falls on Saturday, this yields the Friday before it. If n falls on Sunday, this yields the Monday after, which also happens if n is 1 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),表示 the n`th day of week `d (or DDD) in the month

这里一些示例:

Cron Expression Meaning

0 0 * * * *

每天的每小时整点

*/10 * * * * *

every ten seconds

0 0 8-10 * * *

每天的 8、9 点和 10 点

0 0 6,19 * * *

每天上午 6:00 和下午 7:00

0 0/30 8-10 * * *

每天 8:00、8:30、9:00、9:30、10:00 和 10:30

0 0 9-17 * * MON-FRI

每天九点至下午五点工作日

0 0 0 25 DEC ?

每年的圣诞节午夜

0 0 0 L * *

每月最后一天的午夜

0 0 0 L-3 * *

每月倒数第三天的午夜

0 0 0 * * 5L

每月最后一个星期五的午夜

0 0 0 * * THUL

每月最后一个星期四的午夜

0 0 0 1W * *

每月第一个星期几午夜

0 0 0 LW * *

每月最后一个星期几午夜

0 0 0 ? * 5#2

每月第二个星期五午夜

0 0 0 ? * MON#1

每月第一个星期一午夜

Macros

诸如 0 0 * * * * 的表达式对人类来说很难解析,因此在出现错误的情况下很难修复。为了提高可读性,Spring 支持以下宏,表示常用的序列。你可以使用这些宏代替六位数字的值,如下所示:@Scheduled(cron = "@hourly")

Macro Meaning

@yearly (or @annually)

每年一次 (0 0 0 1 1 *)

@monthly

每月一次 (0 0 0 1 * *)

@weekly

每周一次 (0 0 0 * * 0)

@daily (or @midnight)

每天一次 (0 0 0 * * *)

@hourly

或每小时一次 (0 0 * * * *)

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

作业数据映射中的所有其他属性也对你可用。

通过使用 namegroup 属性,您可以分别修改名称和作业组。默认情况下,作业名称与 JobDetailFactoryBean 的 Bean 名称相匹配(在上面的前一个示例中为 exampleJob)。

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 实现,带有便捷的默认值:CronTriggerFactoryBeanSimpleTriggerFactoryBean

需要安排触发器。Spring 提供了一个 SchedulerFactoryBean,用于将触发器公开为属性设置。SchedulerFactoryBean 使用这些触发器安排实际作业。

以下清单同时使用了 SimpleTriggerFactoryBeanCronTriggerFactoryBean

<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 以获取更多信息。

SchedulerFactoryBean 也识别一个 quartz.properties 文件中的类路径,基于 Quartz 属性键,就像使用常规 Quartz 配置一样。请注意,许多 SchedulerFactoryBean 设置会与属性文件中常见的 Quartz 设置交互;因此,不建议在两个级别上都指定值。例如,如果您打算依赖 Spring 提供的数据源,不要设置一个“org.quartz.jobStore.class”属性,或者指定一个 org.springframework.scheduling.quartz.LocalDataSourceJobStore 变种,它将完全替代标准 org.quartz.impl.jdbcjobstore.JobStoreTX