任务执行与调度
Spring 框架提供了用于异步执行和任务调度的抽象,分别通过 TaskExecutor
和 TaskScheduler
接口。Spring 还提供了这些接口的实现,支持线程池或在应用服务器环境中委托给 CommonJ。最终,使用这些实现通过通用接口抽象了 Java SE 和 Jakarta EE 环境之间的差异。
Spring 还提供了集成类,以支持与 Quartz Scheduler 的调度。
Spring TaskExecutor
抽象
Executors 是 JDK 对线程池概念的命名。之所以称为“executor”,是因为没有保证底层实现实际上是一个池。一个 executor 可能是单线程的,甚至是同步的。Spring 的抽象隐藏了 Java SE 和 Jakarta EE 环境之间的实现细节。
Spring 的 TaskExecutor
接口与 java.util.concurrent.Executor
接口是完全相同的。实际上,最初它存在的主要原因是为了在使用线程池时抽象掉对 Java 5 的需求。该接口有一个单一的方法 (execute(Runnable task)
),接受一个任务以根据线程池的语义和配置进行执行。
TaskExecutor
最初是为了给其他 Spring 组件提供一个线程池的抽象而创建的。像 ApplicationEventMulticaster
、JMS 的 AbstractMessageListenerContainer
和 Quartz 集成等组件都使用 TaskExecutor
抽象来池化线程。然而,如果你的 beans 需要线程池行为,你也可以根据自己的需要使用这个抽象。
TaskExecutor
类型
Spring 包含多个预构建的 TaskExecutor
实现。很可能你永远不需要自己实现一个。Spring 提供的变体如下:
-
SyncTaskExecutor
:该实现不会异步运行调用。相反,每个调用都在调用线程中进行。它主要用于不需要多线程的情况,例如简单的测试用例。 -
SimpleAsyncTaskExecutor
:该实现不重用任何线程。相反,它为每个调用启动一个新线程。然而,它确实支持并发限制,阻止任何超过限制的调用,直到释放出一个插槽。如果您在寻找真正的线程池,请参见列表后面的ThreadPoolTaskExecutor
。当启用“virtualThreads”选项时,它将使用 JDK 21 的虚拟线程。该实现还通过 Spring 的生命周期管理支持优雅关闭。 -
ConcurrentTaskExecutor
:该实现是java.util.concurrent.Executor
实例的适配器。还有一个替代方案(ThreadPoolTaskExecutor
),它将Executor
配置参数暴露为 bean 属性。通常不需要直接使用ConcurrentTaskExecutor
。但是,如果ThreadPoolTaskExecutor
对您的需求不够灵活,ConcurrentTaskExecutor
是一个替代方案。 -
ThreadPoolTaskExecutor
:该实现是最常用的。它暴露了用于配置java.util.concurrent.ThreadPoolExecutor
的 bean 属性,并将其包装在TaskExecutor
中。如果您需要适配不同类型的java.util.concurrent.Executor
,我们建议您使用ConcurrentTaskExecutor
。它还提供暂停/恢复功能,并通过 Spring 的生命周期管理支持优雅关闭。 -
DefaultManagedTaskExecutor
:该实现使用在 JNDI 中获取的ManagedExecutorService
,在 JSR-236 兼容的运行时环境(例如 Jakarta EE 应用服务器)中,取代 CommonJ WorkManager。
使用 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>
大多数 TaskExecutor
实现提供了一种方法,可以使用 TaskDecorator
自动包装提交的任务。装饰器应该委托给它所包装的任务,可能在任务执行之前/之后实现自定义行为。
让我们考虑一个简单的实现,它将在执行任务之前和之后记录消息:
- Java
- Kotlin
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.task.TaskDecorator;
public class LoggingTaskDecorator implements TaskDecorator {
private static final Log logger = LogFactory.getLog(LoggingTaskDecorator.class);
@Override
public Runnable decorate(Runnable runnable) {
return () -> {
logger.debug("Before execution of " + runnable);
runnable.run();
logger.debug("After execution of " + runnable);
};
}
}
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.springframework.core.task.TaskDecorator
class LoggingTaskDecorator : TaskDecorator {
override fun decorate(runnable: Runnable): Runnable {
return Runnable {
logger.debug("Before execution of $runnable")
runnable.run()
logger.debug("After execution of $runnable")
}
}
companion object {
private val logger: Log = LogFactory.getLog(
LoggingTaskDecorator::class.java
)
}
}
我们可以在 TaskExecutor
实例上配置我们的装饰器:
- Java
- Kotlin
- Xml
@Bean
ThreadPoolTaskExecutor decoratedTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setTaskDecorator(new LoggingTaskDecorator());
return taskExecutor;
}
@Bean
fun decoratedTaskExecutor() = ThreadPoolTaskExecutor().apply {
setTaskDecorator(LoggingTaskDecorator())
}
<bean id="decoratedTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="taskDecorator" ref="loggingTaskDecorator"/>
</bean>
如果需要多个装饰器,可以使用 org.springframework.core.task.support.CompositeTaskDecorator
来顺序执行多个装饰器。
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
。它使得基于 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
,或者使用 <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
注解的方法。
处理 @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() {
// 每隔一段时间运行的某些操作
}
如果您需要固定速率的执行,可以在注解中使用 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 开始,@Scheduled
方法支持在任何作用域的 bean 上使用。
确保在运行时不初始化多个相同 @Scheduled
注解类的实例,除非您确实希望为每个这样的实例调度回调。与此相关,确保不在带有 @Scheduled
注解并作为常规 Spring beans 注册到容器的 bean 类上使用 @Configurable
。否则,您将会遇到双重初始化(一次通过容器,一次通过 @Configurable
切面),导致每个 @Scheduled
方法被调用两次。
@Scheduled
注解在反应式方法或 Kotlin 挂起函数上的应用
从 Spring Framework 6.1 开始,@Scheduled
方法也支持多种类型的反应式方法:
- 返回类型为
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
Flow
或Deferred
实例的方法,如以下示例所示:
@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
类型的返回值。这仍然提供了异步执行的好处,以便调用者可以在调用 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();
}
}
没有直接的 XML 等价物用于 @Async
,因为这样的 方法 应该首先设计为异步执行,而不是外部重新声明为异步。然而,您可以手动设置 Spring 的 AsyncExecutionInterceptor
,结合 Spring AOP 和自定义切点。
执行器资格与 @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
类型的返回值时,管理在方法执行期间抛出的异常很简单,因为在调用 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 包含一个用于配置 TaskExecutor
和 TaskScheduler
实例的 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 的文档。主要思想是,当提交一个任务时,如果当前活动线程的数量少于核心大小,执行器首先尝试使用一个空闲线程。如果核心大小已达到,任务将被添加到队列中,只要其容量尚未达到。只有在队列的容量达到时,执行器才会创建一个超出核心大小的新线程。如果最大大小也已达到,则执行器会拒绝该任务。
默认情况下,队列是无限制的,但这很少是期望的配置,因为如果在所有池线程忙碌时向该队列添加足够的任务,可能会导致 OutOfMemoryError
。此外,如果队列是无限制的,最大大小根本没有任何效果。由于执行器总是在创建超出核心大小的新线程之前尝试使用队列,因此队列必须具有有限的容量,以便线程池能够超出核心大小增长(这就是为什么在使用无限制队列时,固定大小的池是唯一合理的情况)。
考虑上述提到的任务被拒绝的情况。默认情况下,当任务被拒绝时,线程池执行器会抛出一个 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"/>
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-delay
和 fixed-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 表达式,例如 * * * * * *
,由六个以空格分隔的时间和日期字段组成,每个字段都有其有效值的范围:
┌───────────── 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
代表 本周的最后一天。如果前面加上一个数字或三个字母的名称(dL
或DDDL
),则表示 本月的第d
天(或DDD
)的最后一天。
-
-
日历日期字段可以是
nW
,表示 离日期n
最近的工作日。如果n
落在星期六,则返回前一个星期五。如果n
落在星期天,则返回下一个星期一,如果n
是1
并且落在星期六(即:1W
代表 本月的第一个工作日)。 -
如果日历日期字段是
LW
,则表示 本月的最后一个工作日。 -
星期字段可以是
d#n
(或DDD#n
),表示 本月的第n
个星期d
(或DDD
)的天。
以下是一些示例:
Cron 表达式 | 说明 |
---|---|
0 0 * * * * | 每天每小时的开始时刻 |
*/10 * * * * * | 每十秒钟一次 |
0 0 8-10 * * * | 每天的 8 点、9 点和 10 点 |
0 0 6,19 * * * | 每天的 6:00 AM 和 7:00 PM |
0 0/30 8-10 * * * | 每天的 8:00、8:30、9:00、9:30、10:00 和 10:30 |
0 0 9-17 * * MON-FRI | 每周一至周五的整点 9 点到 5 点 |
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 使用 Trigger
、Job
和 JobDetail
对象来实现各种作业的调度。有关 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
}
}
所有来自工作数据映射的附加属性也可供您使用。
通过使用 name
和 group
属性,您可以分别修改作业的名称和组。默认情况下,作业的名称与 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
实现: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。
SchedulerFactoryBean
还会识别类路径中的 quartz.properties
文件,基于 Quartz 属性键,与常规 Quartz 配置一样。请注意,许多 SchedulerFactoryBean
设置与属性文件中的常见 Quartz 设置相互作用;因此,不建议在两个层次上同时指定值。例如,如果您打算依赖 Spring 提供的数据源,请不要设置 "org.quartz.jobStore.class" 属性,或者指定 org.springframework.scheduling.quartz.LocalDataSourceJobStore
变体,这是标准 org.quartz.impl.jdbcjobstore.JobStoreTX
的完整替代。