任务执行和调度

Spring 框架为任务的异步执行和调度提供了抽象,分别使用 TaskExecutorTaskScheduler 接口。Spring 还提供了这些接口的实现,它们支持线程池或在应用程序服务器环境中委托给 CommonJ。最终,在通用接口背后的这些实现的使用抽象化了 Java SE 和 Jakarta EE 环境之间的差异。

Spring 还提供了集成类来支持使用 Quartz Scheduler 进行调度。

Spring TaskExecutor 抽象

执行器是 JDK 中线程池概念的名称。之所以命名为“执行器”,是因为无法保证底层实现实际上是一个池。执行器可能是单线程的,甚至可能是同步的。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 类型

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

  • DefaultManagedTaskExecutor:此实现使用在 JSR-236 兼容运行时环境(例如 Jakarta EE 应用程序服务器)中获得的 JNDI ManagedExecutorService,为此目的替换 CommonJ WorkManager。

从 6.1 开始,ThreadPoolTaskExecutor 通过 Spring 的生命周期管理提供了暂停/恢复功能和优雅关闭。SimpleAsyncTaskExecutor 上还有一个新的“virtualThreads”选项,它与 JDK 21 的虚拟线程保持一致,并且还为 SimpleAsyncTaskExecutor 提供了优雅的关闭功能。

使用 TaskExecutor

Spring 的 TaskExecutor 实现通常与依赖注入一起使用。在以下示例中,我们定义了一个使用 ThreadPoolTaskExecutor 异步打印一组消息的 bean

import org.springframework.core.task.TaskExecutor;

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

如你所见,与其从池中检索线程并自己执行,不如将 Runnable 添加到队列中。然后,TaskExecutor 使用其内部规则来决定何时运行任务。

要配置 TaskExecutor 使用的规则,我们公开简单的 bean 属性

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

Spring TaskScheduler 抽象

除了 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 的方法,它只接受 RunnableInstant。这会导致任务在指定时间后运行一次。所有其他方法都能够安排任务重复运行。固定速率和固定延迟方法用于简单的周期性执行,但接受 Trigger 的方法更加灵活。

Trigger 接口

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

Spring 提供了 Trigger 接口的两种实现。最有趣的是 CronTrigger。它支持基于 cron 表达式 安排任务。例如,以下任务计划在每小时的 15 分钟后运行,但仅在工作日的 9 点到 5 点“工作时间”内运行

scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));

另一种实现是 PeriodicTrigger,它接受一个固定周期、一个可选的初始延迟值和一个布尔值,以指示该周期应解释为固定速率还是固定延迟。由于 TaskScheduler 接口已经定义了以固定速率或固定延迟调度任务的方法,因此应尽可能直接使用这些方法。PeriodicTrigger 实现的价值在于,您可以在依赖于 Trigger 抽象的组件中使用它。例如,允许交替使用周期性触发器、基于 cron 的触发器,甚至自定义触发器实现可能很方便。此类组件可以利用依赖注入,以便您可以外部配置此类 Triggers,因此可以轻松修改或扩展它们。

TaskScheduler 实现

与 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 的虚拟线程保持一致,使用单个调度程序线程,但为每个计划的任务执行启动一个新线程(除了所有在单个调度程序线程上运行的固定延迟任务,因此对于此虚拟线程对齐选项,建议使用固定速率和 cron 触发器)。

用于调度和异步执行的注释支持

Spring 为任务调度和异步方法执行提供注释支持。

启用调度注释

要启用对 @Scheduled@Async 注释的支持,您可以在其中一个 @Configuration 类中添加 @EnableScheduling@EnableAsync,如下例所示

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

您可以为您的应用程序选择相关的注解。例如,如果您只需要对 @Scheduled 提供支持,则可以省略 @EnableAsync。为了获得更细粒度的控制,您还可以实现 SchedulingConfigurer 接口、AsyncConfigurer 接口或两者。有关完整详细信息,请参阅 SchedulingConfigurerAsyncConfigurer javadoc。

如果您更喜欢 XML 配置,则可以使用 <task:annotation-driven> 元素,如下例所示

<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>

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

处理 @Async 注解的默认通知模式是 proxy,它只允许通过代理拦截调用。同一类中的本地调用无法以这种方式被拦截。对于更高级的拦截模式,请考虑与编译时或加载时织入结合使用 aspectj 模式。

@Scheduled 注解

您可以将 @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 表达式。以下示例仅在工作日运行

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
	// something that should run on weekdays only
}
您还可以使用 zone 属性来指定解析 cron 表达式的时区。

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

@Scheduled 可用作可重复注解。如果在同一函数上找到多个已调度的声明,则会独立处理每个声明,并为每个声明触发一个单独的触发器。因此,此类并置调度可能会重叠,并并行或立即连续执行多次。请确保您指定的 cron 表达式等不会意外重叠。

从 Spring Framework 4.3 开始,任何作用域的 bean 都支持 @Scheduled 函数。

确保您不会在运行时初始化同一 @Scheduled 注解类的多个实例,除非您确实希望对每个此类实例调度回调。与此相关,确保您不在用 @Scheduled 注解的 bean 类上使用 @Configurable,并且将其作为常规 Spring bean 注册到容器中。否则,您将获得双重初始化(一次通过容器,一次通过 @Configurable 方面),从而导致每个 @Scheduled 函数被调用两次。

Reactive 函数或 Kotlin 挂起函数上的 @Scheduled 注解

从 Spring Framework 6.1 开始,@Scheduled 函数还支持多种类型的 reactive 函数

  • 具有 Publisher 返回类型(或 Publisher 的任何具体实现)的函数,如下例所示

@Scheduled(fixedDelay = 500)
public Publisher<Void> reactiveSomething() {
	// return an instance of Publisher
}
  • 具有可通过 ReactiveAdapterRegistry 的共享实例调整为 Publisher 的返回类型的函数,前提是该类型支持延迟订阅,如下例所示

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

所有这些类型的函数都必须在不带任何参数的情况下声明。对于 Kotlin 挂起函数,kotlinx.coroutines.reactor 桥接器也必须存在,以允许框架将挂起函数调用为 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 订阅在前五秒内失败了两次。然后订阅开始成功,每五秒向标准输出打印一条消息

@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 的下一次计划订阅以及任何仍然处于活动状态的过去订阅(例如,对于长时间运行的发布者或甚至无限发布者)。

@Async 注释

您可以在方法上提供 @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 的 org.springframework.util.concurrent.ListenableFuture,或者从 Spring 4.2 开始,还可以声明 JDK 8 的 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 的 AsyncExecutionInterceptor,并结合自定义切入点。

使用 @Async 进行执行程序限定

默认情况下,在方法上指定 @Async 时,使用的执行程序是 在启用异步支持时配置 的执行程序,即如果你使用 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 注解指定)。

使用 @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

task 命名空间

从 3.0 版本开始,Spring 包含了一个用于配置 TaskExecutorTaskScheduler 实例的 XML 命名空间。它还提供了一种方便的方法来配置要使用触发器调度的任务。

'scheduler' 元素

以下元素使用指定的线程池大小创建一个 ThreadPoolTaskScheduler 实例

<task:scheduler id="scheduler" pool-size="10"/>

id 属性提供的用于作为池中线程名称的前缀。scheduler 元素相对简单。如果你不提供 pool-size 属性,则默认线程池只有一个线程。调度程序没有其他配置选项。

executor 元素

以下创建了一个 ThreadPoolTaskExecutor 实例

<task:executor id="executor" pool-size="10"/>

上一节中所示的调度程序一样,为 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 的文档。主要思想是,当提交任务时,如果活动线程数当前小于核心大小,执行程序首先尝试使用空闲线程。如果已达到核心大小,则任务将被添加到队列中,只要其容量尚未达到。只有在这种情况下,如果队列的容量已达到,执行程序才会在核心大小之外创建一个新线程。如果最大大小也已达到,则执行程序将拒绝该任务。

默认情况下,队列是无界的,但这很少是所需的配置,因为如果在所有池线程都处于繁忙状态时向该队列添加足够的任务,这可能会导致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"/>

“scheduled-tasks” 元素

Spring 任务命名空间最强大的功能是对在 Spring 应用程序上下文中安排任务进行配置的支持。这遵循与 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 任务,你可以指定一个“initial-delay”参数,指示在首次执行该方法之前等待的毫秒数。为了更好地控制,你可以提供一个 cron 属性来提供 cron 表达式。以下示例显示了这些其他选项

<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 表达式

无论是在 @Scheduled 注释task:scheduled-tasks 元素 还是其他地方使用,所有 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)
 │ │ │ │ │ │
 * * * * * *

有一些规则适用

  • 一个字段可以是一个星号 (*),它总是表示“第一个到最后一个”。对于每月中的某天或每周中的某天字段,可以使用问号 (?) 代替星号。

  • 逗号 (,) 用于分隔列表中的项。

  • 用连字符 (-) 分隔的两个数字表示一个数字范围。指定范围具有包容性。

  • 在范围(或 *)后跟 / 指定数字值在范围内的间隔。

  • 月份和每周中的某天字段也可以使用英文名称。使用特定日期或月份的前三个字母(大小写无关)。

  • 每月中的某天和每周中的某天字段可以包含一个 L 字符,它有不同的含义。

    • 在日字段中,L 表示当月的最后一天。如果后面跟一个负偏移(即 L-n),则表示当月倒数第 n

    • 在星期字段中,L 表示当周的最后一天。如果前面加一个数字或三个字母的名称(dLDDDL),则表示当月星期 (dDDD) 的最后一天

  • 日字段可以是 nW,表示最接近当月第 n 天的星期几。如果 n 落在星期六,则表示它之前的星期五。如果 n 落在星期日,则表示它之后的星期一,如果 n1 且落在星期六也会出现这种情况(即:1W 表示当月的第一个星期几)。

  • 如果日字段为 LW,则表示当月的最后一个星期几

  • 星期字段可以是 d#n(或 DDD#n),表示当月星期 d(或 DDD)的第 n

以下是一些示例

Cron 表达式 含义

0 0 * * * *

每天每小时的整点

*/10 * * * * *

每十秒

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

当月的第一个星期一午夜

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

含义

@yearly(或 @annually

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

@monthly

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

@weekly

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

@daily (或 @midnight)

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

@hourly

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

使用 Quartz Scheduler

Quartz 使用 TriggerJobJobDetail 对象来实现各种作业的调度。有关 Quartz 背后的基本概念,请参阅 Quartz 网站。出于方便的目的,Spring 提供了几种类,简化了在基于 Spring 的应用程序中使用 Quartz。

使用 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 自动应用它

package example;

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

使用 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>
默认情况下,作业将以并发方式运行。

使用触发器和 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 提供的 DataSource,请不要设置 "org.quartz.jobStore.class" 属性,或者指定 org.springframework.scheduling.quartz.LocalDataSourceJobStore 变体,它是标准 org.quartz.impl.jdbcjobstore.JobStoreTX 的完全替代品。