任务执行和调度

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:此实现最常用。它公开了用于配置java.util.concurrent.ThreadPoolExecutor的bean属性,并将其包装在TaskExecutor中。如果您需要适应不同类型的java.util.concurrent.Executor,我们建议您使用ConcurrentTaskExecutor

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

从6.1版本开始,ThreadPoolTaskExecutor通过Spring的生命周期管理提供了暂停/恢复功能和优雅关闭。SimpleAsyncTaskExecutor还增加了一个新的"virtualThreads"选项,与JDK 21的Virtual Threads对齐,以及对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的方法,只接受一个Runnable和一个Instant。这会导致任务在指定时间后运行一次。所有其他方法都能够安排任务重复运行。固定速率和固定延迟方法用于简单的周期性执行,但接受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。它可以根据 安排任务运行。例如,以下任务被安排在每小时的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注解的支持,您可以将@EnableScheduling@EnableAsync添加到您的一个@Configuration类中,如下例所示:

@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() {
	// 定期运行的内容
}

默认情况下,毫秒将用作固定延迟、固定速率和初始延迟值的时间单位。如果您想要使用不同的时间单位,如秒或分钟,可以通过@Scheduled中的timeUnit属性进行配置。

例如,前面的示例也可以写成如下形式。

@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
	// 定期运行的内容
}

如果需要固定速率执行,可以在注解中使用fixedRate属性。以下方法每五秒调用一次(在每次调用的连续启动时间之间测量):

@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
	// 定期运行的内容
}

对于固定延迟和固定速率任务,您可以通过指定初始延迟来指示在方法第一次执行之前等待的时间量,如下面的fixedRate示例所示:

@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void doSomething() {
	// 定期运行的内容
}

对于一次性任务,您只需指定初始延迟,指示在方法预期执行之前等待的时间量:

@Scheduled(initialDelay = 1000)
public void doSomething() {
	// 仅应运行一次的内容
}

如果简单的周期性调度不够表达性,您可以提供一个cron表达式。以下示例仅在工作日运行:

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
	// 仅在工作日运行的内容
}
您还可以使用zone属性来指定解析cron表达式的时区。

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

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

从Spring Framework 4.3开始,支持在任何范围的bean上使用@Scheduled方法。

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

在响应式方法或Kotlin挂起函数上的@Scheduled注解

从Spring Framework 6.1开始,@Scheduled方法也支持几种类型的响应式方法:

  • 具有Publisher返回类型(或任何Publisher的具体实现)的方法,如以下示例:

@Scheduled(fixedDelay = 500)
public Publisher<Void> reactiveSomething() {
	// 返回一个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() {
	// 执行一些异步操作
}
  • 返回Kotlin FlowDeferred实例的方法,如以下示例:

@Scheduled(fixedDelay = 500)
fun something(): Flow<Void> {
	flow {
		// 执行一些异步操作
	}
}

所有这些类型的方法必须在没有任何参数的情况下声明。对于Kotlin挂起函数,还必须存在kotlinx.coroutines.reactor桥接,以便允许框架将挂起函数调用为Publisher

Spring Framework将为带注解的方法获取一次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秒内失败两次。然后订阅开始成功,每五秒向标准输出打印一条消息:

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

@Async注解

您可以在方法上提供@Async注解,以便该方法的调用异步进行。换句话说,在调用时,调用者立即返回,而实际方法的执行发生在已提交给Spring TaskExecutor的任务中。在最简单的情况下,您可以将注解应用于返回void的方法,如以下示例所示:

@Async
void doSomething() {
	// 这将异步运行
}

与使用@Scheduled注解的方法不同,这些方法可以期望参数,因为它们是在运行时由调用者以“正常”方式调用,而不是从容器管理的定时任务中调用。例如,以下代码是@Async注解的合法应用:

@Async
void doSomething(String s) {
	// 这将异步运行
}

甚至返回值的方法也可以异步调用。但是,这些方法需要具有Future类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在调用该Futureget()之前执行其他任务。以下示例展示了如何在返回值的方法上使用@Async

@Async
Future<String> returnSomething(int i) {
	// 这将异步运行
}
@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,则是“annotation-driven”元素,如果有的话则是您的AsyncConfigurer实现。但是,当需要指示在执行给定方法时应使用除默认执行器之外的其他执行器时,可以使用@Async注解的value属性。以下示例显示了如何做到这一点:

@Async("otherExecutor")
void doSomething(String s) {
	// 这将由“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) {
		// 处理异常
	}
}

默认情况下,异常仅仅被记录。您可以通过使用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表达式

所有Spring的cron表达式都必须符合相同的格式,无论您是在@Scheduled注解中使用它们,还是在task:scheduled-tasks元素中使用,或者其他地方。一个格式良好的cron表达式,例如* * * * * *,由六个以空格分隔的时间和日期字段组成,每个字段都有自己的有效值范围:

 ┌───────────── 秒 (0-59)
 │ ┌───────────── 分钟 (0 - 59)
 │ │ ┌───────────── 小时 (0 - 23)
 │ │ │ ┌───────────── 月份中的日期 (1 - 31)
 │ │ │ │ ┌───────────── 月份 (1 - 12) (或 JAN-DEC)
 │ │ │ │ │ ┌───────────── 星期中的日期 (0 - 7)
 │ │ │ │ │ │          (0或7是星期日,或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和19: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调度器

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;

	/**
	 * 在实例化ExampleJob后调用的setter
	 * 使用来自JobDetailFactoryBean的值。
	 */
	public void setTimeout(int timeout) {
		this.timeout = timeout;
	}

	protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {
		// 执行实际工作
	}
}

作业数据映射中的所有附加属性也对您可用。

通过使用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 {

	// 属性和协作者

	public void doIt() {
		// 执行实际工作
	}
}
<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提供了两个带有方便默认值的QuartzFactoryBean实现:CronTriggerFactoryBeanSimpleTriggerFactoryBean

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

以下清单同时使用了SimpleTriggerFactoryBeanCronTriggerFactoryBean

<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
	<!-- 参见上面的方法调用作业示例 -->
	<property name="jobDetail" ref="jobDetail"/>
	<!-- 10秒 -->
	<property name="startDelay" value="10000"/>
	<!-- 每50秒重复一次 -->
	<property name="repeatInterval" value="50000"/>
</bean>

<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
	<property name="jobDetail" ref="exampleJob"/>
	<!-- 每天早上6点运行 -->
	<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数据源。有关更多信息,请参阅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的全功能替代品。