20220507 7. Integration - Task Execution and Scheduling
前言
Spring 框架分别使用 TaskExecutor
和 TaskScheduler
接口为任务的异步执行和调度提供了抽象。Spring 还具有那些在应用服务器环境中支持线程池或委托 CommonJ 的接口的实现。最终,在通用接口背后使用这些实现消除了 Java SE 5、 Java SE 6和 Java EE 环境之间的差异。
Spring 还提供了集成类来支持 Timer (从1.3开始就是 JDK 的一部分) 和 Quartz Scheduler 的调度。可以分别使用 FactoryBean
和对 Timer
或 Trigger
实例的可选引用来设置这两个调度器。此外,还有一个方便使用 Quartz Scheduler 和 Timer
的类,它允许您调用现有目标对象的方法 (类似于通常的 MethodInvokingFactoryBean
操作) 。
Spring TaskExecutor
抽象
执行器( Executor )是线程池概念的 JDK 名称。executor
命名是由于不能保证底层实现实际上是一个池。执行器可以是单线程的,甚至是同步的。Spring 的抽象隐藏了 Java SE 和 Java EE 环境之间的实现细节。
Spring 的 TaskExecutor
接口与 java.util.concurrent.Executor
接口相同。事实上,最初,它存在的主要原因是在使用线程池时抽象出对 Java 5 的需求。该接口有一个方法 execute(Runnable task)
,根据线程池的语义和配置接受执行任务。
最初创建 TaskExecutor
是为了在需要的地方为其他 Spring 组件提供线程池的抽象。诸如 ApplicationEventMulticaster
、 JMS 的 AbstractMessageListenerContainer
和 Quartz 集成等组件都使用 TaskExecutor
抽象来池线程。但是,如果您的 bean 需要线程池行为,您也可以根据自己的需要使用这个抽象。
参考
TaskExecutor
接口实现 Executor
接口,并且声明了和 Executor
相同的方法
java.util.concurrent.Executor
org.springframework.core.task.TaskExecutor
TaskExecutor
实现
Spring 包括许多 TaskExecutor
的预构建实现。你应该永远不需要实现你自己的。Spring 提供的变体如下:
SyncTaskExecutor
: 此实现不是异步运行调用。相反,每个调用都发生在调用线程中。它主要用于不需要多线程的情况,例如在简单的测试用例中SimpleAsyncTaskExecutor
: 此实现不重用任何线程。相反,它为每个调用启动一个新线程。但是,它支持并发限制,可以阻止任何超出限制的调用,直到插槽被释放。如果您正在寻找真正的池实现,请参见后面的ThreadPoolTaskExecutor
ConcurrentTaskExecutor
: 这个实现是一个java.util.concurrent.Executor
实例的适配器。还有一种替代方案 (ThreadPoolTaskExecutor
) 将Executor
配置参数作为 bean 属性公开。很少需要直接使用ConcurrentTaskExecutor
。但是,如果ThreadPoolTaskExecutor
不够灵活,不能满足您的需要,可以使用ConcurrentTaskExecutor
ThreadPoolTaskExecutor
: 这个实现是最常用的。它公开了用于配置java.util.concurrent.ThreadPoolExecutor
的 bean 属性。然后将其封装在TaskExecutor
中。如果您需要适配不同类型的java.util.concurrent.Executor
,我们建议您使用ConcurrentTaskExecutor
代替WorkManagerTaskExecutor
: 此实现使用 CommonJWorkManager
作为其底层服务提供者,并且是在 Spring 应用上下文中在 WebLogic 或 WebSphere 上设置基于 CommonJ 的线程池集成的核心便利类DefaultManagedTaskExecutor
: 这个实现在 JSR-236 兼容的运行时环境中使用 JNDI 获得的ManagedExecutorService
(例如 Java EE 7+ 应用服务器) ,为此目的替换 CommonJ WorkManager
使用 TaskExecutor
Spring 的 TaskExecutor
实现被用作简单的 JavaBean 。在下面的例子中,我们定义了一个 bean ,它使用 ThreadPoolTaskExecutor
异步打印一组消息:
import org.springframework.core.task.TaskExecutor;
public class TaskExecutorExample {
private class MessagePrinterTask implements Runnable {
private String message;
public MessagePrinterTask(String message) {
this.message = message;
}
public void run() {
System.out.println(message);
}
}
private TaskExecutor taskExecutor;
public TaskExecutorExample(TaskExecutor taskExecutor) {
this.taskExecutor = taskExecutor;
}
public void printMessages() {
for(int i = 0; i < 25; i++) {
taskExecutor.execute(new MessagePrinterTask("Message" + i));
}
}
}
如您所见,与其从池中检索线程并自己执行它,不如将 Runnable
添加到队列中。然后 TaskExecutor
使用其内部规则来决定何时运行任务。
为了配置 TaskExecutor
使用的规则,我们公开简单的 bean 属性:
<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="10"/>
<property name="queueCapacity" value="25"/>
</bean>
<bean id="taskExecutorExample" class="TaskExecutorExample">
<constructor-arg ref="taskExecutor"/>
</bean>
Spring TaskScheduler
抽象
除了 TaskExecutor
抽象之外,Spring 3.0 还引入了一个 TaskScheduler
,其中包含多种调度任务的方法,以便在未来的某个时刻运行。下面的清单显示了 TaskScheduler
接口定义:
public interface TaskScheduler {
ScheduledFuture schedule(Runnable task, Trigger trigger);
ScheduledFuture schedule(Runnable task, Instant startTime);
ScheduledFuture schedule(Runnable task, Date startTime);
ScheduledFuture scheduleAtFixedRate(Runnable task, Instant startTime, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);
ScheduledFuture scheduleAtFixedRate(Runnable task, Duration period);
ScheduledFuture scheduleAtFixedRate(Runnable task, long period);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Instant startTime, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Duration delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}
最简单的方法是一个名为 schedule
的方法,它只接受 Runnable
和 Date
。这将导致任务在指定时间后运行一次。所有其他方法都能够调度重复运行的任务。固定速率(fixed-rate)和固定延迟(fixed-delay)方法用于简单的周期性执行,而接收 Trigger
的方法则灵活得多。
org.springframework.scheduling.TaskScheduler
Trigger
接口
Trigger
接口基本上受到了 JSR-236的启发,在 Spring 3.0 中, JSR-236 还没有正式实现。Trigger
的基本思想是,执行时间可以根据过去的执行结果甚至任意条件来确定。如果这些决定确实考虑到了前面执行的结果,那么 TriggerContext
中就有该信息。Trigger
接口本身非常简单,如下面的清单所示:
public interface Trigger {
Date nextExecutionTime(TriggerContext triggerContext);
}
TriggerContext
是最重要的部分。它封装了所有相关的数据,并且在将来如果需要的话可以进行扩展。TriggerContext
是一个接口 (默认情况下使用 SimpleTriggerContext
实现) 。下面的清单显示了用于 Trigger
实现的可用方法。
public interface TriggerContext {
Date lastScheduledExecutionTime();
Date lastActualExecutionTime();
Date lastCompletionTime();
}
org.springframework.scheduling.Trigger
Trigger
实现
Spring 提供了 Trigger
接口的两个实现。最有趣的是 CronTrigger
。它支持基于 cron 表达式 的任务调度。例如,下列任务定于每小时过后 15 分钟执行,但只在工作日朝九晚五的 ”营业时间” 执行:
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
另一个实现是 PeriodicTrigger
,它接受一个固定周期、一个可选的初始延迟值和一个布尔值,以指示该周期是应该解释为固定速率还是固定延迟。由于 TaskScheduler
接口已经定义了以固定速率或固定延迟调度任务的方法,因此只要有可能,就应该直接使用这些方法。PeriodicTrigger
实现的价值在于,您可以在依赖于 Trigger
抽象的组件中使用它。例如,允许周期触发器、基于 cron 的触发器、甚至自定义触发器实现可以互换使用可能比较方便。这样的组件可以利用触发依赖注入,这样你就可以在外部配置这样的 Trigger
,因此可以轻松地修改或扩展它们。
TaskScheduler
实现
与 Spring 的 TaskExecutor
抽象一样,TaskScheduler
的主要好处是可以将应用程序的调度需求与部署环境解耦。当部署到一个应用服务器环境时,这个抽象层特别相关,因为线程不应该由应用程序本身直接创建。对于这些场景,Spring 提供了一个 TimerManagerTaskScheduler
,它在 WebLogic 或 WebSphere 上委托给 CommonJ TimerManager ,以及在 Java EE 7+ 环境中委托给 JSR-236 ManagedScheduledExecutorService
的更新的 DefaultManagedTaskScheduler
。两者通常都配置了 JNDI 查找。
无论何时外部线程管理不是一个需求,一个更简单的替代方案是在应用程序中设置本地 ScheduledExecutorService
,它可以通过 Spring 的 ConcurrentTaskScheduler
进行调整。为了方便起见,Spring 还提供了一个 ThreadPoolTaskScheduler
,它在内部委托 ScheduledExecutorService
来提供类似 ThreadPoolTaskExecutor
的通用 bean 风格的配置。在宽松的应用程序服务器环境中,这些变体对于本地嵌入的线程池设置非常适用,特别是在 Tomcat 和 Jetty 上。
调度和异步执行的注解支持
Spring 为任务调度和异步方法执行提供注解支持。
启用调度注解
为了支持 @Scheduled
和 @Async
注解,你可以在 @Configuration
类中添加 @EnableScheduling
和 @EnableAsync
,如下例所示:
@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}
您可以为您的应用程序挑选相关的注解。例如,如果只需要对 @Scheduled
的支持,可以省略 @EnableAsync
。对于更细粒度的控制,可以另外实现 SchedulingConfigurer
接口、 AsyncConfigurer
接口,或两者都实现。有关详细信息,请参阅 SchedulingConfigurer
和 AsyncConfigurer
。
如果您更喜欢 XML 配置,可以使用 <task:annotation-driven>
标签,如下面的示例所示:
<task:annotation-driven executor="myExecutor" scheduler="myScheduler"/>
<task:executor id="myExecutor" pool-size="5"/>
<task:scheduler id="myScheduler" pool-size="10"/>
请注意,对于前面的 XML,提供了一个执行器引用,用于处理与 @Async
注解方法对应的任务,并提供了调度器引用,用于管理那些用@Scheduled
注解的方法。
处理
@Async
注解的默认通知模式是proxy
,它只允许通过代理拦截调用。同一个类中的本地调用不能以这种方式被拦截。对于更高级的拦截模式,可以考虑结合编译时编织或加载时编织切换到aspectj
模式。
@Scheduled
注解
可以将 @Scheduled
注解和触发器元数据一起添加到方法中。例如,以下方法每五秒调用一次,具有固定的延迟时间,这意味着周期是从每次调用前的完成时间来度量的:
@Scheduled(fixedDelay=5000)
public void doSomething() {
// something that should run periodically
}
如果需要固定速率的执行,可以更改注解中指定的属性名。以下方法每五秒调用一次 (在每次调用的连续开始时间之间测量) :
@Scheduled(fixedRate=5000)
public void doSomething() {
// something that should run periodically
}
对于固定延迟和固定速率的任务,您可以指定初始延迟,方法在第一次执行之前等待的毫秒数,如下面的 fixedRate
示例所示:
@Scheduled(initialDelay=1000, fixedRate=5000)
public void doSomething() {
// something that should run periodically
}
如果简单的周期调度表达能力不够,可以提供 cron 表达式 。下面的示例只在工作日运行:
还可以使用
zone
属性指定 cron 表达式解析的时区。
请注意,要调度的方法必须具有 void 返回,并且不能有任何参数。如果该方法需要与应用上下文中的其他对象交互,那么这些对象通常会通过依赖注入提供。
在 Spring Framework 4.3 中,任何作用域的 bean 都支持
@Scheduled
方法。确保在运行时没有初始化同一
@Scheduled
注解类的多个实例,除非确实希望将回调调度安排到每个这样的实例。与此相关的是,确保不要在 bean 类上使用@Configurable
,这些 bean 类使用@Scheduled
注解,并在容器中注册为常规 Spring bean 。否则,您将得到双重初始化 (一次通过容器,一次通过@Configurable
切面) ,结果是每个@Scheduled
方法被调用两次。
@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 AOP 手动设置 Spring 的AsyncExecutionInterceptor
。
限定(Qualifier)执行器使用 @Async
默认情况下,在方法上指定 @Async
时,使用的执行器是 在启用异步支持时配置的 ,如果使用 XML 或 AsyncConfigurer
实现 (如果有的话) ,则使用 “annotation-driven” 标签。但是,当您需要指示在执行给定方法时应该使用除缺省值以外的执行器时,可以使用 @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
属性提供的值用作池中线程名称的前缀。调度器标签相对简单。如果不提供池大小属性,则默认线程池只有一个线程。调度器没有其他配置选项。
executor
标签
创建一个 ThreadPoolTaskExecutor
实例:
<task:executor id="executor" pool-size="10"/>
与上一节中显示的调度器一样,为 id
属性提供的值用作池中线程名称的前缀。就池大小而言,executor
标签比 scheduler
标签支持更多的配置选项。首先,ThreadPoolTaskExecutor
的线程池本身更容易配置。执行器的线程池可以为核心和最大大小提供不同的值,而不仅仅是单一大小。如果您提供一个值,执行器有一个固定大小的线程池 (核心和最大大小相同) 。但是,executor
标签的池大小属性也接受 min-max
形式的范围。下面的示例将最小值设置为 5 ,最大值设置为 25 :
<task:executor
id="executorWithPoolSizeRange"
pool-size="5-25"
queue-capacity="100"/>
在前面的配置中,还提供了 queue-capacity
。还应该根据执行器的队列容量来考虑线程池的配置。有关池大小和队列容量之间关系的完整描述,请参阅 ThreadPoolExecutor
。其主要思想是,当提交任务时,如果当前活动线程的数量小于核心大小,执行器首先尝试使用空闲线程。如果已达到核心大小,则只要尚未达到其容量,就将任务添加到队列中。只有到那时,如果队列的容量已经达到,执行器才会创建超出核心大小的新线程。如果也已达到最大大小,则执行器将拒绝该任务。
默认情况下,队列是无界的,但这很少是理想的配置,因为如果在所有池线程都处于忙碌状态时向队列中添加了足够的任务,就会导致 OutOfMemoryError
。此外,如果队列是无界的,则最大大小根本没有影响。由于执行器总是在创建超出核心大小的新线程之前尝试队列,因此队列必须具有有限的容量,以使线程池增长到超出核心大小 (这就是为什么使用无界队列时固定大小的池是唯一合理的情况) 。
考虑上面提到的情况,当一个任务被拒绝时。默认情况下,当任务被拒绝时,线程池执行器抛出一个 TaskRejectedException
。但是,拒绝策略实际上是可配置的。使用默认拒绝策略 (即 AbortPolicy
实现) 时引发异常。对于在重负载下可以跳过某些任务的应用程序,您可以改为配置 DiscardPolicy
或者 DiscardOldestPolicy
。对于需要在重负载下限制提交的任务的应用程序,另一个适用的选项是 CallerRunsPolicy
。该策略不是抛出异常或丢弃任务,而是强制调用 submit
方法的线程运行任务本身。这个想法是,这样的调用程序在运行该任务时很忙,不能立即提交其他任务。因此,它提供了一种简单的方法来限制传入负载,同时维护线程池和队列的限制。通常,这允许执行者“追赶”它正在处理的任务,从而释放队列、池或两者上的一些容量。您可以从执行器元素上的 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 的 task
命名空间最强大的特性是支持配置在 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)
│ │ │ │ │ │
* * * * * *
这里有一些适用的规则:
- 字段可以是星号
*
,总是表示first-last
。对于 月中日(day of the month) 或 周中日(day of the week) 字段,使用问号?
可以用来代替星号 - 逗号
,
用于分隔列表中的项 - 两个以连字符
-
分隔的数字表示一个数字范围。指定范围包括在内 - 在一个范围 (或
*
) 之后,使用/
指定该数值在该范围内的间隔 - 英文名称也可用于 月中日(day of the month) 和 周中日(day of the week) 字段。使用特定日期或月份的前三个字母 (大小写无关紧要)
- 月中日(day of the month) 和 周中日(day of the week) 字段可以包含一个
L
字符,这个字符具有不同的含义- 在 月中日(day of the month) 字段中,
L
代表月的最后一天。如果后面跟着一个负偏移量 (即L-n
) ,则表示该月的第n
天到最后一天 - 在 周中日(day of the week) 字段中,
L
代表一周的最后一天。如果前面有一个数字或三个字母的名称 (dL
或DDDL
) ,它表示一个月中每周的最后一天 (d
或DDD
)
- 在 月中日(day of the month) 字段中,
- 月中日(day of the month) 字段可以是
nW
,n
表示月中最近的一个工作日。如果n
在星期六,结果是之前的星期五。如果n
在星期天,结果是之后的星期一,如果n
是1
在星期六,结果相同 (即:1W
表示该月的第一个工作日) - 如果 月中日(day of the month) 字段为
LW
,则表示该月的最后一个工作日 - 周中日(day of the week) 字段可以是
d#n
( 或DDD#n
) ,它表示该月的每周d
(或DDD
) 的第n
天
以下是一些例子:
Cron 表达式 | 描述 |
---|---|
0 0 * * * * |
每天的每一个小时 |
*/10 * * * * * |
每隔 10 秒 |
0 0 8-10 * * * |
每天 8 点,9 点, 10 点 |
0 0 6,19 * * * |
每天 6 点,19 点 |
0 0/30 8-10 * * * |
每天 8 点,8 点半,9 点,9 点半,10 点,10 点半 |
0 0 9-17 * * MON-FRI |
周一到周五,每天的 9 点到 17 点 整点 |
0 0 0 25 DEC ? |
每年圣诞节的午夜,12 月 25 日 0 点 |
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 |
每个月的第一个星期一的午夜 |
宏(Macros)
像 0 0 * * * *
这样的表达式对于人类来说很难解析,因此在出现 bug 时也很难修复。为了提高可读性,Spring 支持以下宏,它们表示常用的序列。您可以使用这些宏代替六位数值,例如: @Scheduled(cron = "@hourly")
Macros | 描述 |
---|---|
@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 Scheduler
相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
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
) 。超时(timeout
)是在作业数据映射中指定的。作业数据映射可以通过 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 (5)
*/
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>
上面的例子导致 doIt
方法被 exampleBusinessObject
方法调用,如下面的例子所示:
public class ExampleBusinessObject {
// properties and collaborators
public void doIt() {
// do the actual work
}
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>
通过使用 MethodInvokingJobDetailFactoryBean
,您不需要创建仅调用方法的一行作业。您只需要创建实际的业务对象和装配细节对象。
默认情况下,Quartz Jobs 是无状态的,这导致了作业之间相互干扰的可能性。如果为相同的 JobDetail
指定两个触发器,则有可能在第一个作业完成之前启动第二个触发器。如果 JobDetail
类实现了有状态接口,那么就不会发生这种情况。第二项工作在第一项工作完成之前不会开始。要使来自 MethodInvokingJobDetailFactoryBean
的作业为非并发作业,请将并发标志设置为 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
提供更多属性,例如作业详细信息使用的日历(calendar)、用于自定义 Quartz 的属性,以及其他属性。有关更多信息,请参见 SchedulerFactoryBean
。