任务执行与调度

TODAY Framework 分别通过 TaskExecutorTaskScheduler 接口提供了用于异步执行和任务调度的抽象。 Infra 还提供了这些接口的实现,这些实现支持线程池或在应用服务器环境中委托给 CommonJ。 最终,在通用接口背后使用这些实现,屏蔽了 Java SE 和 Jakarta EE 环境之间的差异。

Infra TaskExecutor 抽象

Executor 是 JDK 对线程池概念的命名。“executor” 这个名称是因为底层实现实际上并不保证是一个池。 Executor 可以是单线程的,甚至是同步的。Infra 的抽象隐藏了 Java SE 和 Jakarta EE 环境之间的实现细节。

Infra 的 TaskExecutor 接口与 java.util.concurrent.Executor 接口完全相同。 事实上,最初它存在的主要原因是为了在使用线程池时屏蔽对 Java 5 的需求。 该接口有一个方法(execute(Runnable task)),该方法根据线程池的语义和配置接受一个任务进行执行。

TaskExecutor 最初是为了给其他需要线程池的 Infra 组件提供一个抽象而创建的。 诸如 ApplicationEventMulticaster、JMS 的 AbstractMessageListenerContainer 和 Quartz 集成等组件都使用 TaskExecutor 抽象来池化线程。 但是,如果您的 bean 需要线程池行为,您也可以根据自己的需要使用此抽象。

TaskExecutor 类型

Infra 包含许多预构建的 TaskExecutor 实现。 您很可能永远不需要实现自己的。 Infra 提供的变体如下:

  • 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

  • DefaultManagedTaskExecutor: 此实现使用在 JSR-236 兼容的运行时环境(例如 Jakarta EE 应用服务器)中通过 JNDI 获取的 ManagedExecutorService,以此取代 CommonJ WorkManager。

从 5.0 开始,ThreadPoolTaskExecutor 通过 Infra 生命周期管理提供了暂停/恢复功能和优雅关闭功能。 SimpleAsyncTaskExecutor 还有一个新的 "virtualThreads" 选项,该选项与 JDK 21 的虚拟线程保持一致,并且 SimpleAsyncTaskExecutor 也具有优雅关闭功能。

使用 TaskExecutor

Infra 的 TaskExecutor 实现通常与依赖注入一起使用。 在以下示例中,我们定义了一个使用 ThreadPoolTaskExecutor 异步打印一组消息的 bean:

import infra.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="infra.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>

Infra TaskScheduler 抽象

除了 TaskExecutor 抽象之外,Infra 还有一个 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 实现

Infra 提供了 Trigger 接口的两种实现。最有趣的是 CronTrigger。 它支持基于 cron 表达式 的任务调度。 例如,以下任务被安排在每小时的 15 分运行,但仅限工作日的朝九晚五“工作时间”:

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

另一个实现是 PeriodicTrigger,它接受一个固定周期、一个可选的初始延迟值和一个布尔值,该布尔值指示周期应解释为固定速率还是固定延迟。 由于 TaskScheduler 接口已经定义了以固定速率或固定延迟调度任务的方法,因此应尽可能直接使用这些方法。 PeriodicTrigger 实现的价值在于,您可以在依赖 Trigger 抽象的组件中使用它。 例如,允许周期性触发器、基于 cron 的触发器甚至自定义触发器实现互换使用可能会很方便。 这样的组件可以利用依赖注入,以便您可以在外部配置此类 Trigger,从而轻松地修改或扩展它们。

TaskScheduler 实现

与 Infra TaskExecutor 抽象一样,TaskScheduler 安排的主要好处是应用程序的调度需求与部署环境解耦。 当部署到不应由应用程序本身直接创建线程的应用服务器环境时,此抽象级别特别相关。 对于此类场景,Infra 提供了一个 DefaultManagedTaskScheduler,它在 Jakarta EE 环境中委托给 JSR-236 ManagedScheduledExecutorService

只要不需要外部线程管理,更简单的替代方案是在应用程序内设置本地 ScheduledExecutorService,这可以通过 Infra ConcurrentTaskScheduler 进行适配。 为方便起见,Infra 还提供了一个 ThreadPoolTaskScheduler,它在内部委托给 ScheduledExecutorService,以提供类似于 ThreadPoolTaskExecutor 的常见 bean 风格配置。 这些变体对于宽松的应用服务器环境(特别是 Tomcat)中的本地嵌入式线程池设置也非常有效。

从 6.1 开始,ThreadPoolTaskScheduler 通过 Infra 生命周期管理提供了暂停/恢复功能和优雅关闭功能。 还有一个名为 SimpleAsyncTaskScheduler 的新选项,它与 JDK 21 的虚拟线程保持一致,使用单个调度器线程,但为每个计划的任务执行启动一个新线程 (除了固定延迟任务,它们都在单个调度器线程上运行,因此对于这个与虚拟线程对齐的选项,建议使用固定速率和 cron 触发器)。

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

Infra 为任务调度和异步方法执行都提供了注解支持。

启用调度注解

要启用对 @Scheduled@Async 注解的支持,您可以将 @EnableScheduling@EnableAsync 添加到您的一个 @Configuration 类中,如下例所示:

@Configuration
@EnableAsync
@EnableScheduling
public class AppConfig {
}

您可以为您的应用程序挑选相关的注解。例如,如果您只需要对 @Scheduled 的支持,则可以省略 @EnableAsync。 为了进行更细粒度的控制,您可以另外实现 SchedulingConfigurer 接口、AsyncConfigurer 接口或两者。 有关完整的详细信息,请参阅 SchedulingConfigurerAsyncConfigurer javadoc。

如果您更喜欢 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 注解连同触发器元数据一起添加到方法中。 例如,以下方法每五秒(5000 毫秒)以固定延迟调用一次,这意味着周期是从每个前次调用的完成时间开始测量的。

@Scheduled(fixedDelay = 5000)
public void doSomething() {
  // 应周期性运行的一些事情
}

默认情况下,毫秒将用作固定延迟、固定速率和初始延迟值的时间单位。 如果您想使用不同的时间单位,例如秒或分钟,可以通过 @Scheduled 中的 timeUnit 属性进行配置。

例如,前面的示例也可以写成如下形式。

@Scheduled(fixedDelay = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
  // 应周期性运行的一些事情
}

如果您需要固定速率执行,可以在注解中使用 fixedRate 属性。 以下方法每五秒调用一次(在每次调用的连续开始时间之间测量):

@Scheduled(fixedRate = 5, timeUnit = TimeUnit.SECONDS)
public void doSomething() {
  // 应周期性运行的一些事情
}

对于固定延迟和固定速率任务,您可以通过指示在第一次执行方法之前等待的时间量来指定初始延迟,如下面的 fixedRate 示例所示:

@Scheduled(initialDelay = 1000, fixedRate = 5000)
public void doSomething() {
  // 应周期性运行的一些事情
}

对于一次性任务,您可以通过指示在方法的预定执行之前等待的时间量来仅指定初始延迟:

@Scheduled(initialDelay = 1000)
public void doSomething() {
  // 应该只运行一次的事情
}

如果简单的定期调度不够表达,您可以提供一个 cron 表达式。 以下示例仅在工作日运行:

@Scheduled(cron="*/5 * * * * MON-FRI")
public void doSomething() {
  // 应该只在工作日运行的事情
}
您还可以使用 zone 属性来指定解析 cron 表达式的时区。

请注意,要调度的方法必须具有 void 返回值,并且不得接受任何参数。 如果该方法需要与应用程序上下文中的其他对象交互,则这些对象通常是通过依赖注入提供的。

@Scheduled 可以用作可重复注解。如果在同一个方法上找到多个调度声明,则每个声明都将被独立处理,并为每个声明触发单独的触发器。 因此,此类共置的调度可能会重叠并多次并行执行或紧接着执行。 请确保您指定的 cron 表达式等不会意外重叠。

从 TODAY Framework 4.3 开始,任何范围的 bean 都支持 @Scheduled 方法。

确保不要在运行时初始化同一个 @Scheduled 注解类的多个实例,除非您确实想为每个此类实例调度回调。 与此相关,请确保不要在使用 @Scheduled 注解并作为常规 Infra bean 注册到容器中的 bean 类上使用 @Configurable。 否则,您将获得双重初始化(一次通过容器,一次通过 @Configurable 切面),结果是每个 @Scheduled 方法被调用两次。

响应式方法或 Kotlin 挂起函数上的 @Scheduled 注解

从 TODAY Framework 6.1 开始,几种类型的响应式方法也支持 @Scheduled 方法:

  • 具有 Publisher 返回类型(或 Publisher 的任何具体实现)的方法,如下例所示:

@Scheduled(fixedDelay = 500)
public Publisher<Void> reactiveSomething() {
  // 返回 Publisher 的实例
}
  • 具有可以通过 ReactiveAdapterRegistry 的共享实例适配为 Publisher 的返回类型的方法,前提是该类型支持 延迟订阅,如下例所示:

@Scheduled(fixedDelay = 500)
public Single<String> rxjavaNonPublisher() {
  return Single.just("example");
}

CompletableFuture 类是一个典型的可以适配为 Publisher 但不支持延迟订阅的类型示例。 其在注册表中的 ReactiveAdapter 通过使 getDescriptor().isDeferred() 方法返回 false 来表示这一点。

所有这些类型的方法都必须声明为不带任何参数。如果是 Kotlin 挂起函数,则还必须存在 kotlinx.coroutines.reactor 桥接,以允许框架将挂起函数作为 Publisher 调用。

TODAY Framework 将获取一次被注解方法的 Publisher,并调度一个 Runnable,在该 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 订阅在前 5 秒内失败两次。 然后订阅开始成功,每 5 秒向标准输出打印一条消息:

@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 或关闭应用程序上下文时,TODAY Framework 会取消已调度的任务,这包括下一次对 Publisher 的调度订阅以及任何当前仍处于活动状态的过去订阅(例如,对于长时间运行的发布者甚至无限发布者)。

@Async 注解

您可以在方法上提供 @Async 注解,以便异步调用该方法。 换句话说,调用者在调用时立即返回,而方法的实际执行发生在已提交给 Infra TaskExecutor 的任务中。 在最简单的情况下,您可以将注解应用于返回 void 的方法,如下例所示:

@Async
void doSomething() {
  // 这将异步运行
}

与使用 @Scheduled 注解的方法不同,这些方法可以接受参数,因为它们是由调用者在运行时以“正常”方式调用的,而不是由容器管理的调度任务调用的。 例如,以下代码是 @Async 注解的合法应用:

@Async
void doSomething(String s) {
  // 这将异步运行
}

即使是返回值的方法也可以异步调用。但是,此类方法需要具有 Future 类型的返回值。 这仍然提供了异步执行的好处,以便调用者在对该 Future 调用 get() 之前可以执行其他任务。 以下示例显示了如何在返回值的方法上使用 @Async

@Async
Future<String> returnSomething(int i) {
  // 这将异步运行
}
@Async 方法不仅可以声明常规的 java.util.concurrent.Future 返回类型,还可以声明 Infra 的 infra.util.concurrent.ListenableFuture, 或者(从 Infra 4.2 开始)JDK 8 的 java.util.concurrent.CompletableFuture,以便与异步任务进行更丰富的交互,并立即与进一步的处理步骤组合。

您不能将 @Async 与生命周期回调(如 @PostConstruct)结合使用。 要异步初始化 Infra bean,您目前必须使用单独的初始化 Infra 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 等效项,因为此类方法首先应该设计用于异步执行,而不是在外部重新声明为异步。 但是,您可以结合自定义切入点,使用 Infra AOP 手动设置 Infra AsyncExecutionInterceptor

使用 @Async 限定执行器

默认情况下,在方法上指定 @Async 时,使用的执行器是 启用异步支持时配置的 那个, 即如果您使用 XML 则是“annotation-driven”元素,或者您的 AsyncConfigurer 实现(如果有)。 但是,当您需要指示在执行给定方法时应使用除默认执行器之外的执行器时,可以使用 @Async 注解的 value 属性。 以下示例显示了如何执行此操作:

@Async("otherExecutor")
void doSomething(String s) {
  // 这将由 "otherExecutor" 异步运行
}

在这种情况下,"otherExecutor" 可以是 Infra 容器中任何 Executor bean 的名称,也可以是与任何 Executor 关联的限定符的名称(例如,使用 <qualifier> 元素或 Infra @Qualifier 注解指定)。

使用 @Async 进行异常管理

@Async 方法具有 Future 类型的返回值时,很容易管理在方法执行期间抛出的异常,因为此异常在对 Future 结果调用 get 时抛出。 但是,对于 void 返回类型,异常是未捕获的且无法传输。您可以提供 AsyncUncaughtExceptionHandler 来处理此类异常。 以下示例显示了如何执行此操作:

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {

  @Override
  public void handleUncaughtException(Throwable ex, Method method, Object... params) {
    // 处理异常
  }
}

默认情况下,仅记录异常。您可以使用 AsyncConfigurer<task:annotation-driven/> XML 元素定义自定义 AsyncUncaughtExceptionHandler

task 命名空间

从 3.0 版开始,Infra 包含用于配置 TaskExecutorTaskScheduler 实例的 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 的文档。 主要思想是,提交任务时,如果当前活动线程数少于核心大小,则执行器首先尝试使用空闲线程。 如果已达到核心大小,则只要尚未达到队列容量,就会将任务添加到队列中。 只有当队列容量已满时,执行器才会创建超出核心大小的新线程。如果也达到了最大大小,则执行器拒绝该任务。

默认情况下,队列是无界的,但这很少是所需的配置,因为如果所有池线程都忙,而向该队列添加了足够多的任务,则可能导致 OutOfMemoryErrors。 此外,如果队列是无界的,则最大大小根本没有影响。由于执行器总是先尝试队列,然后再创建超出核心大小的新线程,因此队列必须具有有限的容量,以便线程池增长到超出核心大小(这就是为什么在使用无界队列时,固定大小的池是唯一合理的情况)。

考虑上面提到的任务被拒绝的情况。默认情况下,当任务被拒绝时,线程池执行器会抛出 TaskRejectedException。 但是,拒绝策略实际上是可配置的。使用默认拒绝策略(即 AbortPolicy 实现)时会抛出异常。 对于在高负载下可以跳过某些任务的应用程序,您可以配置 DiscardPolicyDiscardOldestPolicy。 对于需要在高负载下限制提交任务的应用程序,另一个效果很好的选项是 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' 元素

Infra 任务命名空间最强大的功能是支持配置要在 Infra 应用程序上下文中调度的任务。 这遵循类似于 Infra 中其他“method-invokers”的方法,例如 JMS 命名空间提供的用于配置消息驱动 POJO 的方法。 基本上,ref 属性可以指向任何 Infra 管理的对象,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-delayfixed-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 表达式

所有 Infra cron 表达式都必须符合相同的格式,无论您是在 @Scheduled 注解task:scheduled-tasks 元素 中使用它们,还是在其他地方使用。 格式良好的 cron 表达式(例如 * * * * * *)由六个空格分隔的时间和日期字段组成,每个字段都有自己的有效值范围:

 ┌───────────── 秒 (0-59)
 │ ┌───────────── 分 (0 - 59)
 │ │ ┌───────────── 时 (0 - 23)
 │ │ │ ┌───────────── 月份中的日 (1 - 31)
 │ │ │ │ ┌───────────── 月 (1 - 12) (或 JAN-DEC)
 │ │ │ │ │ ┌───────────── 星期中的日 (0 - 7)
 │ │ │ │ │ │          (0 或 7 是周日, 或 MON-SUN)
 │ │ │ │ │ │
 * * * * * *

有一些适用的规则:

  • 字段可以是星号 (*),它总是代表“first-last”。 对于月份中的日或星期中的日字段,可以使用问号 (?) 代替星号。

  • 逗号 (,) 用于分隔列表中的项目。

  • 用连字符 (-) 分隔的两个数字表示数字范围。 指定的范围是包含的。

  • 在范围(或 *)后跟 / 指定该范围内数字值的间隔。

  • 英文名称也可用于月和星期中的日字段。 使用特定日或月的前三个字母(大小写无关)。

  • 月份中的日和星期中的日字段可以包含 L 字符,它具有不同的含义。

    • 在月份中的日字段中,L 代表 月份的最后一天。 如果后跟负偏移量(即 L-n),则表示 月份的倒数第 n 天

    • 在星期中的日字段中,L 代表 星期的最后一天。 如果前缀有数字或三个字母的名称(dLDDDL),则表示 该月的最后一个星期 d(或 DDD)

  • 月份中的日字段可以是 nW,代表 离月份第 n 天最近的工作日。 如果 n 是星期六,则结果是前一个星期五。 如果 n 是星期日,则结果是后一个星期一,如果 n1 并且是星期六(即: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 和晚上 7:00

0 0/30 8-10 * * *

每天 8:00、8:30、9:00、9:30、10:00 和 10:30

0 0 9-17 * * MON-FRI

工作日朝九晚五的整点

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 * * * * 之类的表达式对人类来说很难解析,因此在出现错误时很难修复。 为了提高可读性,Infra 支持以下宏,它们代表常用的序列。 您可以使用这些宏代替六位数值,例如:@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 使用 TriggerJobJobDetail 对象来实现各种作业的调度。 有关 Quartz 背后的基本概念,请参阅 Quartz 网站。 为了方便起见,Infra 提供了几个类,简化了在基于 Infra 的应用程序中使用 Quartz。

使用 JobDetailFactoryBean

Quartz JobDetail 对象包含运行作业所需的所有信息。 Infra 提供了一个 JobDetailFactoryBean,它为 XML 配置目的提供了 bean 风格的属性。 考虑以下示例:

<bean name="exampleJob" class="infra.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 {
    // 做实际工作
  }
}

作业数据映射中的所有其他属性也可供您使用。

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

使用 MethodInvokingJobDetailFactoryBean

通常,您只需要在特定对象上调用一个方法。 通过使用 MethodInvokingJobDetailFactoryBean,您可以完全做到这一点,如下例所示:

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

前面的示例导致在 exampleBusinessObject 方法上调用 doIt 方法,如下例所示:

public class ExampleBusinessObject {

  // 属性和协作者

  public void doIt() {
    // 做实际工作
  }
}
<bean id="exampleBusinessObject" class="examples.ExampleBusinessObject"/>

通过使用 MethodInvokingJobDetailFactoryBean,您无需创建仅调用方法的一行作业。 您只需要创建实际的业务对象并连接详细信息对象。

默认情况下,Quartz 作业是无状态的,导致作业之间可能相互干扰。 如果为同一个 JobDetail 指定两个触发器,则第二个作业可能会在第一个作业完成之前开始。 如果 JobDetail 类实现了 Stateful 接口,则不会发生这种情况:第二个作业在第一个作业完成之前不会开始。

要使 MethodInvokingJobDetailFactoryBean 生成的作业成为非并发的,请将 concurrent 标志设置为 false,如下例所示:

<bean id="jobDetail" class="infra.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
  <property name="targetObject" ref="exampleBusinessObject"/>
  <property name="targetMethod" value="doIt"/>
  <property name="concurrent" value="false"/>
</bean>
默认情况下,作业将以并发方式运行。

使用触发器和 SchedulerFactoryBean 连接作业

我们已经创建了作业详细信息和作业。我们还回顾了允许您在特定对象上调用方法的便捷 bean。 当然,我们仍然需要调度作业本身。这是通过使用触发器和 SchedulerFactoryBean 完成的。 Quartz 中有多种触发器可用,Infra 提供了两个具有便捷默认值的 Quartz FactoryBean 实现:CronTriggerFactoryBeanSimpleTriggerFactoryBean

触发器需要被调度。Infra 提供了一个 SchedulerFactoryBean,它公开了要设置为属性的触发器。 SchedulerFactoryBean 使用这些触发器调度实际作业。

以下清单同时使用了 SimpleTriggerFactoryBeanCronTriggerFactoryBean

<bean id="simpleTrigger" class="infra.scheduling.quartz.SimpleTriggerFactoryBean">
  <!-- 参见上面的方法调用作业示例 -->
  <property name="jobDetail" ref="jobDetail"/>
  <!-- 10 秒 -->
  <property name="startDelay" value="10000"/>
  <!-- 每 50 秒重复一次 -->
  <property name="repeatInterval" value="50000"/>
</bean>

<bean id="cronTrigger" class="infra.scheduling.quartz.CronTriggerFactoryBean">
  <property name="jobDetail" ref="exampleJob"/>
  <!-- 每天早上 6 点运行 -->
  <property name="cronExpression" value="0 0 6 * * ?"/>
</bean>

前面的示例设置了两个触发器,一个每 50 秒运行一次,启动延迟为 10 秒,另一个每天早上 6 点运行。 为了完成所有设置,我们需要设置 SchedulerFactoryBean,如下例所示:

<bean class="infra.scheduling.quartz.SchedulerFactoryBean">
  <property name="triggers">
    <list>
      <ref bean="cronTrigger"/>
      <ref bean="simpleTrigger"/>
    </list>
  </property>
</bean>

SchedulerFactoryBean 还有更多可用属性,例如作业详细信息使用的日历、用于自定义 Quartz 的属性以及 Infra 提供的 JDBC 数据源。 有关更多信息,请参阅 SchedulerFactoryBean javadoc。

SchedulerFactoryBean 还可以识别类路径中的 quartz.properties 文件(基于 Quartz 属性键),就像常规 Quartz 配置一样。 请注意,许多 SchedulerFactoryBean 设置与属性文件中的常见 Quartz 设置交互;因此,不建议在两个级别都指定值。 例如,如果您打算依赖 Infra 提供的数据源,请不要设置 "org.quartz.jobStore.class" 属性,或者指定 infra.scheduling.quartz.LocalDataSourceJobStore 变体,它是标准 org.quartz.impl.jdbcjobstore.JobStoreTX 的完全替代品。