对于最新稳定版本,请使用 Spring Framework 7.0.6spring-doc.cadn.net.cn

任务执行与调度

Spring 框架分别通过 TaskExecutorTaskScheduler 接口提供了对任务异步执行和调度的抽象。Spring 还提供了这些接口的实现,支持线程池,或在应用服务器环境中委托给 CommonJ。最终,通过这些通用接口背后的实现,可以抽象掉 Java SE 和 Jakarta EE 环境之间的差异。spring-doc.cadn.net.cn

Spring 还提供了集成类,以支持与 Quartz 调度器 进行调度。spring-doc.cadn.net.cn

SpringTaskExecutor抽象

执行器(Executors)是 JDK 中对线程池概念的称呼。“执行器”这一命名的原因在于,底层实现并不保证实际上是一个池。执行器可能是单线程的,甚至是同步的。Spring 的抽象层隐藏了 Java SE 和 Jakarta EE 环境之间的实现细节。spring-doc.cadn.net.cn

Spring 的 TaskExecutor 接口与 java.util.concurrent.Executor 接口完全相同。事实上,它最初存在的主要原因是为了在使用线程池时消除对 Java 5 的依赖。该接口仅包含一个方法(execute(Runnable task)),用于根据线程池的语义和配置来接受任务并执行。spring-doc.cadn.net.cn

TaskExecutor 最初是为了给其他 Spring 组件提供一个在需要时进行线程池管理的抽象而创建的。ApplicationEventMulticaster、JMS 的 AbstractMessageListenerContainer 以及 Quartz 集成等组件都使用 TaskExecutor 抽象来进行线程池管理。然而,如果你的 Bean 需要线程池行为,也可以将此抽象用于你自己的需求。spring-doc.cadn.net.cn

TaskExecutor类型

Spring 包含了若干个预构建的 TaskExecutor 实现。 在大多数情况下,你很可能永远都不需要自己实现一个。 Spring 提供的变体如下所示:spring-doc.cadn.net.cn

  • SyncTaskExecutor: 该实现不会异步执行调用。相反,每次调用都在调用线程中进行。它主要用于不需要多线程的场景,例如简单的测试用例。spring-doc.cadn.net.cn

  • SimpleAsyncTaskExecutor: 此实现不会重用任何线程。相反,它会为每次调用启动一个新线程。 然而,它确实支持并发限制,当调用数量超过该限制时,后续调用将被阻塞, 直到有空闲槽位为止。如果您需要真正的线程池功能,请参见本列表稍后介绍的 ThreadPoolTaskExecutorspring-doc.cadn.net.cn

  • ConcurrentTaskExecutor: 此实现是 java.util.concurrent.Executor 实例的一个适配器。 还有一个替代方案(ThreadPoolTaskExecutor),它将 Executor 的配置参数以 bean 属性的形式暴露出来。 通常很少需要直接使用 ConcurrentTaskExecutor。 然而,如果 ThreadPoolTaskExecutor 无法满足你的灵活性需求,ConcurrentTaskExecutor 可作为替代选择。spring-doc.cadn.net.cn

  • ThreadPoolTaskExecutor: 这是最常用的实现。它暴露了 Bean 属性,用于配置一个 java.util.concurrent.ThreadPoolExecutor,并将其包装在 TaskExecutor 中。 如果你需要适配其他类型的 java.util.concurrent.Executor, 我们建议你改用 ConcurrentTaskExecutorspring-doc.cadn.net.cn

  • DefaultManagedTaskExecutor: 此实现在符合 JSR-236 标准的运行时环境(例如 Jakarta EE 应用服务器)中使用通过 JNDI 获取的 ManagedExecutorService, 以替代 CommonJ WorkManager 来实现该目的。spring-doc.cadn.net.cn

使用一个TaskExecutor

Spring 的 TaskExecutor 实现通常与依赖注入一起使用。 在下面的示例中,我们定义了一个 bean,它使用 ThreadPoolTaskExecutor 来异步打印一组消息:spring-doc.cadn.net.cn

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 会根据其内部规则决定何时运行该任务。spring-doc.cadn.net.cn

为了配置 TaskExecutor 所使用的规则,我们提供了简单的 bean 属性:spring-doc.cadn.net.cn

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

SpringTaskScheduler抽象

除了 TaskExecutor 抽象之外,Spring 还提供了一个 TaskScheduler SPI,其中包含多种方法用于调度任务在未来某个时间点执行。以下代码清单展示了 TaskScheduler 接口的定义:spring-doc.cadn.net.cn

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。 该方法会在指定时间之后执行一次任务。所有其他方法 都能够调度任务重复运行。固定频率(fixed-rate)和固定延迟(fixed-delay) 方法适用于简单的周期性执行,而接受 Trigger 的方法则 灵活得多。spring-doc.cadn.net.cn

Trigger接口

Trigger 接口的设计在本质上受到 JSR-236 的启发。Trigger 的基本思想是,执行时间可以根据先前执行的结果甚至任意条件来确定。如果这些判断考虑了前一次执行的结果,那么该信息可通过 TriggerContext 获取。Trigger 接口本身非常简单,如下列代码所示:spring-doc.cadn.net.cn

public interface Trigger {

	Instant nextExecution(TriggerContext triggerContext);
}

TriggerContext 是最重要的部分。它封装了所有相关数据,并且在将来如有必要,还可以进行扩展。TriggerContext 是一个接口(默认使用 SimpleTriggerContext 实现)。以下列表展示了 Trigger 实现类可用的方法。spring-doc.cadn.net.cn

public interface TriggerContext {

	Clock getClock();

	Instant lastScheduledExecution();

	Instant lastActualExecution();

	Instant lastCompletion();
}

Trigger实现

Spring 提供了 Trigger 接口的两种实现。其中最有趣的是 CronTrigger。它允许基于cron 表达式来调度任务。 例如,以下任务被安排在每小时过 15 分钟时运行,但仅限于工作日的 9 点到 17 点“工作时间”内:spring-doc.cadn.net.cn

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

另一种实现是 PeriodicTrigger,它接受一个固定的周期、一个可选的初始延迟值,以及一个布尔值来指示该周期应被解释为固定频率(fixed-rate)还是固定延迟(fixed-delay)。由于 TaskScheduler 接口已经定义了用于以固定频率或固定延迟调度任务的方法,因此只要可能,应直接使用这些方法。PeriodicTrigger 实现的价值在于,你可以在依赖于 Trigger 抽象的组件中使用它。例如,可以方便地让周期性触发器、基于 cron 表达式的触发器,甚至自定义的触发器实现相互替换使用。这样的组件可以利用依赖注入,从而允许你在外部配置这些 Triggers,并轻松地对其进行修改或扩展。spring-doc.cadn.net.cn

TaskScheduler实现

与 Spring 的 TaskExecutor 抽象类似,TaskScheduler 配置的主要优势在于,应用程序的调度需求与其部署环境解耦。这种抽象层级在部署到应用服务器环境时尤为重要,因为在该环境中应用程序不应直接创建线程。针对此类场景,Spring 提供了一个 DefaultManagedTaskScheduler,它在 Jakarta EE 环境中会委托给 JSR-236 的 ManagedScheduledExecutorServicespring-doc.cadn.net.cn

只要不需要外部线程管理,一种更简单的替代方案是在应用程序内部设置一个本地的 ScheduledExecutorService,并通过 Spring 的 ConcurrentTaskScheduler 进行适配。为方便起见,Spring 还提供了 ThreadPoolTaskScheduler,它在内部委托给 ScheduledExecutorService,以提供类似于 ThreadPoolTaskExecutor 的常见 Bean 风格配置。 这些变体在宽松的应用服务器环境中(尤其是 Tomcat 和 Jetty 上)用于本地嵌入式线程池设置时,也能很好地工作。spring-doc.cadn.net.cn

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

Spring 为任务调度和异步方法执行提供了注解支持。spring-doc.cadn.net.cn

启用调度注解

要启用对 @Scheduled@Async 注解的支持,您可以在其中一个 @EnableScheduling 类上添加 @EnableAsync@Configuration,如下例所示:spring-doc.cadn.net.cn

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

您可以为应用程序挑选并选择相关的注解。例如, 如果您只需要 @Scheduled 的支持,可以省略 @EnableAsync。若需更细粒度的控制, 您还可以额外实现 SchedulingConfigurer 接口、AsyncConfigurer 接口,或两者都实现。 请参阅 SchedulingConfigurerAsyncConfigurer 的 Javadoc 以获取完整详情。spring-doc.cadn.net.cn

如果你更喜欢使用 XML 配置,可以使用 <task:annotation-driven> 元素,如下例所示:spring-doc.cadn.net.cn

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

请注意,通过上述 XML 配置,提供了执行器(executor)引用,用于处理带有 @Async 注解的方法所对应的任务;同时提供了调度器(scheduler)引用,用于管理带有 @Scheduled 注解的方法。spring-doc.cadn.net.cn

处理 @Async 注解的默认通知模式是 proxy,该模式仅允许通过代理拦截方法调用。同一类中的本地方法调用无法以这种方式被拦截。如需更高级的拦截模式,请考虑切换到 aspectj 模式,并结合编译时或加载时织入(weaving)使用。

@Scheduled注解

您可以将 @Scheduled 注解添加到方法上,并附带触发器元数据。例如,以下方法将以固定延迟每五秒(5000 毫秒)调用一次,这意味着每次调用的间隔是从前一次调用完成的时间开始计算的。spring-doc.cadn.net.cn

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

默认情况下,固定延迟(fixed delay)、固定速率(fixed rate)和初始延迟(initial delay)的值将使用毫秒作为时间单位。如果您希望使用其他时间单位(例如秒或分钟),可以通过 timeUnit 注解中的 @Scheduled 属性进行配置。spring-doc.cadn.net.cn

例如,前面的示例也可以按如下方式编写。spring-doc.cadn.net.cn

@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
	// something that should run periodically
}

如果你需要固定频率的执行,可以在注解中使用 fixedRate 属性。以下方法每五秒调用一次(以每次调用的开始时间间隔计算):spring-doc.cadn.net.cn

@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
	// something that should run periodically
}

对于固定延迟(fixed-delay)和固定速率(fixed-rate)任务,你可以通过指定在方法首次执行前需要等待的时间来设置初始延迟,如下所示的 fixedRate 示例所示:spring-doc.cadn.net.cn

@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void doSomething() {
	// something that should run periodically
}

如果简单的周期性调度表达能力不足,您可以提供一个 cron 表达式。 以下示例仅在工作日运行:spring-doc.cadn.net.cn

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

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

@Scheduled 可用作可重复注解。如果在同一方法上找到多个定时调度声明,每个声明都将被独立处理,并为每个声明分别触发各自的触发器。因此,这些位于同一位置的调度可能会重叠,并行执行或连续立即执行多次。 请确保您指定的 cron 表达式等不会意外地产生重叠。spring-doc.cadn.net.cn

从 Spring Framework 4.3 起,@Scheduled 方法在任意作用域的 bean 上均受支持。spring-doc.cadn.net.cn

请确保在运行时不要初始化多个相同的 @Scheduled 注解类的实例,除非你确实希望为每个这样的实例都安排回调。与此相关,请确保不要在已使用 @Configurable 注解并作为常规 Spring Bean 注册到容器中的 Bean 类上使用 @Scheduled。否则,会导致重复初始化(一次通过容器,另一次通过 @Configurable 切面),从而导致每个 @Scheduled 方法被调用两次。spring-doc.cadn.net.cn

@Async注解

你可以在一个方法上添加 @Async 注解,以使该方法的调用异步执行。换句话说,调用方在调用时会立即返回,而该方法的实际执行则会在一个已提交给 Spring TaskExecutor 的任务中进行。在最简单的情况下,你可以将该注解应用于一个返回 void 的方法,如下例所示:spring-doc.cadn.net.cn

@Async
void doSomething() {
	// this will be run asynchronously
}

与使用 @Scheduled 注解标注的方法不同,这些方法可以接受参数,因为它们在运行时是由调用者以“常规”方式调用的,而不是由容器管理的定时任务所调用。例如,以下代码是对 @Async 注解的合法应用:spring-doc.cadn.net.cn

@Async
void doSomething(String s) {
	// this will be run asynchronously
}

即使返回值的方法也可以被异步调用。然而,这类方法的返回值类型必须是 Future。这样仍然可以享受异步执行的好处,使得调用方可以在调用该 get()Future 方法之前执行其他任务。以下示例展示了如何在返回值的方法上使用 @Asyncspring-doc.cadn.net.cn

@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,然后由该 Bean 调用目标对象上带有 @Async 注解的方法, 如下例所示:spring-doc.cadn.net.cn

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 配置,因为这类方法从一开始就应该被设计为异步执行,而不应通过外部方式重新声明为异步。 不过,你可以结合自定义切入点(pointcut),手动使用 Spring AOP 来配置 Spring 的 AsyncExecutionInterceptor

执行器资格认证,适用于@Async

默认情况下,在方法上指定 @Async 注解时,所使用的执行器(executor)是在启用异步支持时配置的执行器, 即如果你使用 XML 配置,则为 “annotation-driven” 元素;或者如果你有自定义的 AsyncConfigurer 实现,则为该实现所配置的执行器。 然而,当你需要为某个特定方法指定使用非默认的执行器时,可以使用 value 注解的 @Async 属性。 以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

@Async("otherExecutor")
void doSomething(String s) {
	// this will be run asynchronously by "otherExecutor"
}

在这种情况下,"otherExecutor" 可以是 Spring 容器中任意 Executor bean 的名称,也可以是与任意 Executor 关联的限定符(qualifier)的名称(例如,通过 <qualifier> 元素或 Spring 的 @Qualifier 注解所指定的名称)。spring-doc.cadn.net.cn

使用进行异常管理@Async

当一个 @Async 方法具有 Future 类型的返回值时,很容易处理方法执行期间抛出的异常,因为该异常会在调用 get 结果的 Future 方法时抛出。然而,对于 void 返回类型,异常将不会被捕获,也无法传递出去。此时,你可以提供一个 AsyncUncaughtExceptionHandler 来处理此类异常。以下示例展示了如何实现这一点:spring-doc.cadn.net.cn

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

	@Override
	public void handleUncaughtException(Throwable ex, Method method, Object... params) {
		// handle exception
	}
}

默认情况下,异常仅会被记录。你可以通过使用 AsyncUncaughtExceptionHandlerAsyncConfigurer XML 元素来定义自定义的 <task:annotation-driven/>spring-doc.cadn.net.cn

task命名空间

从 3.0 版本开始,Spring 提供了一个用于配置 TaskExecutorTaskScheduler 实例的 XML 命名空间。它还提供了一种便捷的方式来配置任务,使其通过触发器进行调度。spring-doc.cadn.net.cn

'scheduler' 元素

以下元素将创建一个具有指定线程池大小的 ThreadPoolTaskScheduler 实例:spring-doc.cadn.net.cn

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

id 属性提供的值将用作线程池中线程名称的前缀。scheduler 元素相对简单。如果您未提供 pool-size 属性,则默认线程池仅包含一个线程。调度器没有其他配置选项。spring-doc.cadn.net.cn

executor元素

以下代码将创建一个 ThreadPoolTaskExecutor 实例:spring-doc.cadn.net.cn

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

上一节中所示的调度器一样,为id属性提供的值将用作池中线程名称的前缀。就池大小而言,executor元素比scheduler元素支持更多的配置选项。首先,ThreadPoolTaskExecutor的线程池本身具有更高的可配置性。执行器的线程池不仅可以设置单一大小,还可以为核心线程数和最大线程数分别指定不同的值。如果只提供单个值,则执行器将使用固定大小的线程池(核心大小和最大大小相同)。然而,executor元素的pool-size属性也接受形如min-max的范围值。以下示例将最小值设置为5,最大值设置为25spring-doc.cadn.net.cn

<task:executor
		id="executorWithPoolSizeRange"
		pool-size="5-25"
		queue-capacity="100"/>

在上述配置中,还提供了一个queue-capacity值。 线程池的配置还应结合执行器的队列容量来考虑。关于池大小与队列容量之间关系的完整描述,请参阅 ThreadPoolExecutor的文档。 其核心思想是:当提交一个任务时,如果当前活动线程数小于核心线程数,执行器会首先尝试使用空闲线程。 如果已达到核心线程数,只要队列容量尚未耗尽,该任务将被加入队列。 只有当队列容量已满时,执行器才会创建超出核心线程数的新线程。如果同时已达到最大线程数,则执行器将拒绝该任务。spring-doc.cadn.net.cn

默认情况下,队列是无界的,但这种配置很少是期望的, 因为当所有线程池中的线程都处于忙碌状态时,如果向该队列中添加了足够多的任务,就可能导致 OutOfMemoryErrors。 此外,如果队列是无界的,最大线程数(max size)将完全不起作用。 由于执行器在创建超出核心线程数的新线程之前总是先尝试将任务加入队列, 因此只有当队列具有有限容量时,线程池才能扩展到超过核心线程数 (这也是为什么在使用无界队列时,固定大小的线程池是唯一合理的选择)。spring-doc.cadn.net.cn

考虑上面提到的任务被拒绝的情况。默认情况下,当一个任务被拒绝时,线程池执行器会抛出一个 TaskRejectedException。然而, 拒绝策略实际上是可配置的。该异常在使用默认拒绝策略时抛出,即AbortPolicy的实现。对于一些在高负载情况下可以跳过的任务,您可以配置为DiscardPolicyDiscardOldestPolicy。另一个在高负载情况下需要限制提交任务的应用程序可以使用的选项是CallerRunsPolicy。该策略不会抛出异常或丢弃任务,而是强制调用 submit 方法的线程自己执行该任务。这种设计的理念是,调用方在执行该任务时处于忙碌状态,无法立即提交其他任务。因此,它提供了一种简单的方法来限制传入的负载,同时保持线程池和队列的限制。通常,这允许执行器在处理的任务上“追赶”进度,从而释放队列、线程池或两者中的一些容量。您可以在executor元素的rejection-policy属性上可用的值枚举中选择这些选项中的任何一个。spring-doc.cadn.net.cn

以下示例展示了一个带有多个属性的 executor 元素,用于指定各种行为:spring-doc.cadn.net.cn

<task:executor
		id="executorWithCallerRunsPolicy"
		pool-size="5-25"
		queue-capacity="100"
		rejection-policy="CALLER_RUNS"/>

最后,keep-alive 设置决定了线程在被停止之前可以保持空闲状态的时间限制(以秒为单位)。如果当前线程池中的线程数量超过核心线程数,在等待指定时间而未处理任何任务后,多余的线程将被停止。若将该时间值设为零,则多余的线程在执行完一个任务且任务队列中没有后续工作时会立即停止。 以下示例将 keep-alive 值设置为两分钟:spring-doc.cadn.net.cn

<task:executor
		id="executorWithKeepAlive"
		pool-size="5-25"
		keep-alive="120"/>

scheduled-tasks 元素

Spring 任务命名空间最强大的功能是支持在 Spring 应用上下文中配置任务调度。这种方式类似于 Spring 中其他“方法调用器”(method-invokers)的实现,例如 JMS 命名空间所提供的用于配置消息驱动 POJO 的机制。基本上,ref 属性可以指向任意由 Spring 管理的对象,而 method 属性则指定要在该对象上调用的方法名称。以下代码清单展示了一个简单的示例:spring-doc.cadn.net.cn

<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 delay)的周期性触发器,表示每次任务执行完成后需等待的毫秒数。另一种选项是 fixed-rate,表示无论前一次执行耗时多长,都按固定频率运行该方法。此外,对于 fixed-delayfixed-rate 两种任务,你都可以指定一个 'initial-delay' 参数,用于指示在首次执行该方法之前需要等待的毫秒数。如果需要更精细的控制,也可以提供一个 cron 属性来指定一个cron 表达式。 以下示例展示了这些其他选项:spring-doc.cadn.net.cn

<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 表达式(例如 * * * * * *)由六个以空格分隔的时间和日期字段组成,每个字段都有其各自的有效取值范围:spring-doc.cadn.net.cn

 ┌───────────── 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)
 │ │ │ │ │ │
 * * * * * *

有一些适用的规则:spring-doc.cadn.net.cn

  • 字段可以是一个星号(*),它始终表示“从第一个到最后一个”。 对于“日期”或“星期”字段,可以使用问号(?)代替 星号。spring-doc.cadn.net.cn

  • 逗号(,)用于分隔列表中的各项。spring-doc.cadn.net.cn

  • 两个用连字符(-)分隔的数字表示一个数字范围。 所指定的范围是包含端点的。spring-doc.cadn.net.cn

  • 在范围(或 *)后加上 / 可指定该数值在范围内的间隔。spring-doc.cadn.net.cn

  • 月份和星期字段也可以使用英文名称。 请使用特定日期或月份的前三个字母(大小写不敏感)。spring-doc.cadn.net.cn

  • 日期(day-of-month)和星期(day-of-week)字段可以包含一个 L 字符,其含义有所不同。spring-doc.cadn.net.cn

    • 在“月份中的日期”字段中,L 代表该月的最后一天。 如果后跟一个负偏移量(即 L-n),则表示该月的倒数第 nspring-doc.cadn.net.cn

    • 在星期几字段中,L 代表一周的最后一天。 如果前面加上数字或三个字母的缩写名称(dLDDDL),则表示该月中星期几(dDDD)的最后一天spring-doc.cadn.net.cn

  • 日期(日)字段可以是 nW,表示最接近当月 1 日的工作日。 如果 n 日是星期六,则结果为之前的星期五。 如果 n 日是星期日,则结果为之后的星期一;同样地,如果 n 日是 1 且为星期六(即:1W 表示当月的第一个工作日),也会得到相同结果。spring-doc.cadn.net.cn

  • 如果日期字段为 LW,则表示该月的最后一个工作日spring-doc.cadn.net.cn

  • 星期字段可以是 d#n(或 DDD#n),表示该月第 n 个星期 d(或 DDDspring-doc.cadn.net.cn

以下是一些示例:spring-doc.cadn.net.cn

Cron 表达式 含义

0 0 * * * *spring-doc.cadn.net.cn

每天每小时的整点spring-doc.cadn.net.cn

*/10 * * * * *spring-doc.cadn.net.cn

每十秒spring-doc.cadn.net.cn

0 0 8-10 * * *spring-doc.cadn.net.cn

每天8点、9点和10点spring-doc.cadn.net.cn

0 0 6,19 * * *spring-doc.cadn.net.cn

每天早上6点和晚上7点spring-doc.cadn.net.cn

0 0/30 8-10 * * *spring-doc.cadn.net.cn

每天 8:00、8:30、9:00、9:30、10:00 和 10:30spring-doc.cadn.net.cn

0 0 9-17 * * MON-FRIspring-doc.cadn.net.cn

工作日周一至周五,整点从上午九点到下午五点spring-doc.cadn.net.cn

0 0 0 25 DEC ?spring-doc.cadn.net.cn

每年圣诞节午夜spring-doc.cadn.net.cn

0 0 0 L * *spring-doc.cadn.net.cn

每月最后一天的午夜spring-doc.cadn.net.cn

0 0 0 L-3 * *spring-doc.cadn.net.cn

每月倒数第三天的午夜spring-doc.cadn.net.cn

0 0 0 * * 5Lspring-doc.cadn.net.cn

每月最后一个星期五的午夜spring-doc.cadn.net.cn

0 0 0 * * THULspring-doc.cadn.net.cn

每月最后一个星期四的午夜spring-doc.cadn.net.cn

0 0 0 1W * *spring-doc.cadn.net.cn

每月第一个工作日的午夜spring-doc.cadn.net.cn

0 0 0 LW * *spring-doc.cadn.net.cn

每月最后一个工作日的午夜spring-doc.cadn.net.cn

0 0 0 ? * 5#2spring-doc.cadn.net.cn

每月第二个星期五的午夜spring-doc.cadn.net.cn

0 0 0 ? * MON#1spring-doc.cadn.net.cn

每月第一个星期一的午夜spring-doc.cadn.net.cn

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

含义

@yearly (or @annually)spring-doc.cadn.net.cn

每年一次(0 0 0 1 1 *spring-doc.cadn.net.cn

@monthlyspring-doc.cadn.net.cn

每月一次(0 0 0 1 * *spring-doc.cadn.net.cn

@weeklyspring-doc.cadn.net.cn

每周一次(0 0 0 * * 0spring-doc.cadn.net.cn

@daily (or @midnight)spring-doc.cadn.net.cn

每天一次(0 0 0 * * *),或spring-doc.cadn.net.cn

@hourlyspring-doc.cadn.net.cn

每小时一次(0 0 * * * *spring-doc.cadn.net.cn

使用 Quartz 调度器

Quartz 使用 TriggerJobJobDetail 对象来实现各种作业的调度。有关 Quartz 背后的基本概念,请参阅Quartz 官方网站。为方便起见,Spring 提供了几个类,以简化在基于 Spring 的应用程序中使用 Quartz。spring-doc.cadn.net.cn

使用JobDetailFactoryBean

Quartz 的 JobDetail 对象包含运行一个作业所需的所有信息。Spring 提供了一个 JobDetailFactoryBean,它为 XML 配置提供了类似 bean 的属性。请参见以下示例:spring-doc.cadn.net.cn

<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)所需的全部信息。 超时时间在作业数据映射(job data map)中指定。作业数据映射可通过 JobExecutionContext(在执行时传递给您)获取,但 JobDetail 也会将作业数据映射中的属性自动映射到作业实例的属性上。因此,在以下示例中,ExampleJob 包含一个名为 timeout 的 bean 属性,而 JobDetail 会自动将其应用:spring-doc.cadn.net.cn

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

作业数据映射(job data map)中的所有附加属性也同样可供您使用。spring-doc.cadn.net.cn

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

使用MethodInvokingJobDetailFactoryBean

通常,你只需要调用某个特定对象上的一个方法。通过使用 MethodInvokingJobDetailFactoryBean,你可以精确地实现这一点,如下例所示:spring-doc.cadn.net.cn

<bean id="jobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
	<property name="targetObject" ref="exampleBusinessObject"/>
	<property name="targetMethod" value="doIt"/>
</bean>

前面的示例会导致在 doIt 对象上调用 exampleBusinessObject 方法,如下例所示:spring-doc.cadn.net.cn

public class ExampleBusinessObject {

	// properties and collaborators

	public void doIt() {
		// do the actual work
	}
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

通过使用 MethodInvokingJobDetailFactoryBean,您无需再创建仅用于调用某个方法的单行作业。您只需创建实际的业务对象,并配置好详细信息对象即可。spring-doc.cadn.net.cn

默认情况下,Quartz 作业(Job)是无状态的,这可能导致多个作业相互干扰。如果你为同一个 JobDetail 指定了两个触发器(trigger),那么第二个作业有可能在第一个作业完成之前就开始执行。如果 JobDetail 类实现了 Stateful 接口,这种情况就不会发生:第二个作业会等到第一个作业完成之后才开始执行。spring-doc.cadn.net.cn

要使由 MethodInvokingJobDetailFactoryBean 生成的任务变为非并发执行,请将 concurrent 标志设置为 false,如下例所示:spring-doc.cadn.net.cn

<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

我们已经创建了作业详情(job details)和作业(jobs)。我们还介绍了用于在特定对象上调用方法的便捷 Bean。当然,我们仍然需要对这些作业进行调度。这是通过使用触发器(triggers)和 SchedulerFactoryBean 来实现的。Quartz 提供了多种触发器,而 Spring 提供了两种 Quartz FactoryBean 实现,并带有便捷的默认配置:CronTriggerFactoryBeanSimpleTriggerFactoryBeanspring-doc.cadn.net.cn

触发器需要被调度。Spring 提供了一个 SchedulerFactoryBean,它将触发器作为属性暴露出来。SchedulerFactoryBean 使用这些触发器来调度实际的作业。spring-doc.cadn.net.cn

以下示例同时使用了 SimpleTriggerFactoryBeanCronTriggerFactoryBeanspring-doc.cadn.net.cn

<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,如下例所示:spring-doc.cadn.net.cn

<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 以获取更多信息。spring-doc.cadn.net.cn

SchedulerFactoryBean 还会识别类路径(classpath)中的 quartz.properties 文件, 该文件基于 Quartz 的属性键(property keys),与常规的 Quartz 配置方式相同。请注意,许多 SchedulerFactoryBean 的设置会与属性文件中的通用 Quartz 设置相互作用; 因此,不建议在两个层面同时指定值。例如,如果您打算依赖 Spring 提供的 DataSource, 就不要设置 "org.quartz.jobStore.class" 属性,或者应指定一个 org.springframework.scheduling.quartz.LocalDataSourceJobStore 的变体, 该变体是标准 org.quartz.impl.jdbcjobstore.JobStoreTX 的完整替代方案。