任务执行与调度
Spring框架通过TaskExecutor和TaskScheduler接口提供了异步执行和任务调度的抽象。Spring还实现了这些接口,支持在应用服务器环境中使用线程池或委托给CommonJ。最终,通过这些通用接口使用这些实现可以屏蔽Java SE和Jakarta EE环境之间的差异。
Spring还提供了集成类,以支持与Quartz Scheduler的调度功能。
Spring的TaskExecutor抽象
在JDK中,线程池的概念被称为“executor”。之所以使用“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的虚拟线程(virtualThreads)。此实现还通过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实现通常与依赖注入(Dependency Injection)一起使用。在以下示例中,我们定义了一个bean,该bean使用ThreadPoolTaskExecutor异步打印出一组消息:
- 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(Service Provider Interface),其中包含多种方法用于安排任务在未来某个时间点执行。以下是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对象。这个方法会让任务在指定的时间之后运行一次。其他所有方法都能够安排任务重复运行。固定速率(fixed-rate)和固定延迟(fixed-delay)的方法适用于简单的周期性执行,但接受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的虚拟线程(Virtual Threads)相兼容,该选项使用一个调度线程,但会在每次执行预定任务时启动一个新的线程(固定延迟的任务除外,这些任务都在同一个调度线程上运行,因此对于这种与虚拟线程对齐的选项,建议使用固定速率和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时,会提供一个执行器引用(executor reference)来处理那些带有@Async注解的方法所对应的任务,同时会提供一个调度器引用(scheduler reference)来管理那些带有@Scheduled注解的方法。
处理@Async注解的默认建议模式是proxy,这种模式仅允许通过代理来拦截调用。同一类内的本地调用无法通过这种方式被拦截。如果需要更高级的拦截方式,可以考虑结合编译时或加载时编织(compile-time or load-time weaving)技术使用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 可以用作一个可重复执行的注解。如果在同一个方法上发现了多个 @Scheduled 注解,那么每个注解都会被独立处理,每个注解都会触发各自的执行逻辑。因此,这样的安排可能会导致任务重叠,从而出现任务同时执行或连续快速执行的情况。请确保你指定的 Cron 表达式等不会无意中导致任务重叠。
从Spring Framework 4.3开始,任何作用域的bean都支持@Scheduled方法。
请确保在运行时不会初始化同一个带有@Scheduled注解的类的多个实例,除非您确实希望为每个这样的实例安排回调。与此相关的是,请确保不要在同时被@Scheduled注解修饰并作为普通Spring bean注册到容器中的bean类上使用@Configurable注解。否则,将会发生双重初始化(一次通过容器,另一次通过@Configurable切面),导致每个@Scheduled方法会被调用两次。
在Reactive方法或Kotlinsuspend函数上使用@Scheduled注解
从Spring Framework 6.1开始,@Scheduled方法也支持在多种类型的反应式(reactive)方法上使用:
- 具有
Publisher返回类型的方法(或任何Publisher的具体实现),如下例所示:
@Scheduled(fixedDelay = 500)
public Publisher<Void> reactiveSomething() {
// return an instance of Publisher
}
- 具有返回类型的方法,可以通过
ReactiveAdapterRegistry的共享实例适配到Publisher,前提是该类型支持延迟订阅(deferred subscription),如下例所示:
@Scheduled(fixedDelay = 500)
public Single<String> rxjavaNonPublisher() {
return Single.just("example");
}
CompletableFuture 类是一个典型的例子,它通常可以被适配为 Publisher,但不支持延迟订阅。其在注册表中的 ReactiveAdapter 表明,因为 getDescriptor().isDeferred() 方法返回 false。
- Kotlin中的挂起函数(suspending functions),如下例所示:
@Scheduled(fixedDelay = 500)
suspend fun something() {
// do something asynchronous
}
- 返回 Kotlin
Flow或Deferred实例的方法,如下例所示:
@Scheduled(fixedDelay = 500)
fun something(): Flow<Void> {
flow {
// do something asynchronous
}
}
所有这些类型的方法都必须不带任何参数地进行声明。在Kotlin的挂起函数(suspending functions)的情况下,还必须引入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 任务中抛出:这意味着对于反应式(reactive)方法来说,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,然后该 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 等价形式,因为这样的方法本来就应该被设计为异步执行的,而不是事后外部声明为异步的。不过,你可以结合自定义的切点(pointcut),通过 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包含了用于配置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的线程池本身具有更高的可配置性。执行器的线程池不仅可以设置为一个固定的大小,还可以分别设置核心线程数(core)和最大线程数(max size)。如果你提供一个单一的值,那么执行器的线程池就是固定大小的(核心线程数和最大线程数相同)。不过,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 的时间值为零时,线程在执行完任务后会立即停止,而不会在任务队列中留下后续待处理的任务。以下示例将 keep-alive 的值设置为两分钟:
<task:executor
id="executorWithKeepAlive"
pool-size="5-25"
keep-alive="120"/>
scheduled-tasks 元素
Spring的任务命名空间(task namespace)最强大的功能是支持在Spring应用程序上下文(Spring Application Context)中配置任务的调度。这种设计方式与其他Spring中的“方法调用器”(method-invokers)类似,例如JMS命名空间用于配置基于消息驱动的POJOs的方式。基本上,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 表达式
所有的Springcron表达式都必须遵循相同的格式,无论你是在@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点和7点 |
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,它允许你在特定对象上调用方法。当然,我们仍然需要安排这些作业的执行时间。这是通过使用触发器(trigger)和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 数据源。有关更多信息,请参阅 SchedulerFactoryBean 的 Java 文档。
SchedulerFactoryBean 也会根据 Quartz 的属性键来识别类路径中的 quartz.properties 文件,这与常规的 Quartz 配置方式相同。请注意,许多 SchedulerFactoryBean 的设置会与配置文件中的通用 Quartz 设置相互影响;因此,不建议在两个层面上都指定值。例如,如果您打算依赖 Spring 提供的 DataSource,就不要设置 “org.quartz.jobStore.class” 属性;或者应该指定 org.springframework.scheduling.quartz.LocalDataSourceJobStore,它完全可以替代标准的 org.quartz.impl.jdbcjobstore.JobStoreTX。