任务执行和调度
Spring 框架分别使用TaskExecutor
和TaskScheduler
接口提供了用于异步执行和调度任务的抽象。Spring 还提供了这些接口的实现,这些实现支持线程池或委托给应用程序服务器环境中的 CommonJ。最终,在通用接口后面使用这些实现抽象了 Java SE 和 Jakarta EE 环境之间的差异。
Spring 还提供了集成类以支持与Quartz Scheduler 的调度。
Spring 的TaskExecutor
抽象
Executor 是 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
抽象来池化线程。但是,如果您的 Bean 需要线程池行为,您也可以将此抽象用于自己的需求。
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
:此实现在 JSR-236 兼容的运行时环境(例如 Jakarta EE 应用程序服务器)中使用 JNDI 获取的ManagedExecutorService
,从而替换此目的的 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
注解的支持,您可以将 @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
接口或两者兼而有之。有关完整详细信息,请参阅SchedulingConfigurer
和AsyncConfigurer
javadoc。
请注意,使用前面的 XML,提供了执行器引用来处理与带有 @Async
注解的方法相对应的方法,并提供了调度程序引用来管理用 @Scheduled
注解的方法。
处理 @Async 注解的默认建议模式为 proxy ,它允许仅通过代理拦截调用。同一类中的本地调用无法以这种方式拦截。对于更高级别的拦截模式,请考虑在结合编译时或加载时编织的情况下切换到 aspectj 模式。 |
@Scheduled
注解
您可以将 @Scheduled
注解添加到方法中,以及触发器元数据。例如,以下方法以固定的延迟每五秒(5000 毫秒)调用一次,这意味着周期是从每次先前调用的完成时间开始测量的。
@Scheduled(fixedDelay = 5000)
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 上的 确保您在运行时没有初始化同一个 |
反应式方法或 Kotlin 挂起函数上的 @Scheduled
注解
从 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");
}
|
-
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 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
订阅在前五秒内失败两次。然后订阅开始成功,每五秒钟向标准输出打印一条消息
@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 会取消计划的任务,其中包括对 |
@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的AsyncExecutionInterceptor 与Spring AOP结合使用,并结合自定义切点。 |
使用@Async
进行执行器限定
默认情况下,当在方法上指定@Async
时,使用的执行器是启用异步支持时配置的执行器,即如果您使用XML,则为“annotation-driven”元素,或者如果您有的话,为您的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包含一个XML命名空间,用于配置TaskExecutor
和TaskScheduler
实例。它还提供了一种方便的方式来配置任务,使其按触发器计划执行。
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表达式 | 含义 |
---|---|
|
每天每小时的开始时间 |
|
每十秒 |
|
每天8点、9点和10点 |
|
每天早上6点和晚上7点 |
|
每天8:00、8:30、9:00、9:30、10:00和10:30 |
|
工作日9点到17点的整点 |
|
每年圣诞节的午夜 |
|
每月最后一天的午夜 |
|
每月倒数第三天的午夜 |
|
每月最后一个星期五的午夜 |
|
每月最后一个星期四的午夜 |
|
每月第一个工作日的午夜 |
|
每月最后一个工作日的午夜 |
|
每月第二个星期五的午夜 |
|
每月第一个星期一的午夜 |
使用Quartz Scheduler
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>
前面的示例设置了两个触发器,一个以10秒的初始延迟每50秒运行一次,另一个每天早上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 的完整替代品。 |