Scheduler Reference Guide

现代应用程序通常需要定期运行特定任务。Quarkus 中有以下两个调度扩展。 quarkus-scheduler`扩展带来了 API 和一个轻量级的内存内调度器实现。 `quarkus-quartz`扩展从 `quarkus-scheduler`扩展中实现了 API,并且包含了一个基于 Quartz 库的调度器实现。只有对于高级调度用例(比如持久任务和集群)才需要 `quarkus-quartz

如果向项目添加了 `quarkus-quartz`依赖项,来自 `quarkus-scheduler`扩展的轻量级调度器实现将自动禁用。

Scheduled Methods

使用 @io.quarkus.scheduler.Scheduled`进行注解的方法将自动安排调用。经过调度的函数不得是抽象方法或私有方法。它可以是静态或非静态方法。可以对经过调度的函数使用拦截器绑定进行注解,比如 `@jakarta.transaction.Transactional`和 `@org.eclipse.microprofile.metrics.annotation.Counted

如果有某个 bean 类没有作用域,并且声明至少一个使用 @Scheduled`进行注解的非静态方法,那么将使用 `@Singleton

此外,进行注解的方法必须返回 void,并且不声明参数或声明一个 `io.quarkus.scheduler.ScheduledExecution`类型的参数。

该注解可重复,因此一个方法可以多次调度。

Inheritance of metadata

子类绝不会继承在超类中声明的 `@Scheduled`方法的元数据。比如,假设类 `org.amce.Foo`由类 `org.amce.Bar`扩展。如果 `Foo`声明一个使用 `@Scheduled`进行注解的非静态方法,那么 `Bar`将不会继承经过调度的函数的元数据。在以下示例中,仅对 `Foo`的实例调用 `everySecond()`函数。

class Foo {

   @Scheduled(every = "1s")
   void everySecond() {
     // ..do something
   }
}

@Singleton
class Bar extends Foo {
}

CDI events

当发生特定事件时,某些 CDI 事件将同步、异步触发。

Type Event description

io.quarkus.scheduler.SuccessfulExecution

已成功完成一次预定作业的执行。

io.quarkus.scheduler.FailedExecution

已在发生异常的情况下完成一次预定作业的执行。

io.quarkus.scheduler.SkippedExecution

已跳过一次预定作业的执行。

io.quarkus.scheduler.SchedulerPaused

The scheduler was paused.

io.quarkus.scheduler.SchedulerResumed

The scheduler was resumed.

io.quarkus.scheduler.ScheduledJobPaused

已暂停一次预定作业。

io.quarkus.scheduler.ScheduledJobResumed

已恢复一次预定作业。

Triggers

触发器由 `@Scheduled#cron()`或 `@Scheduled#every()`属性定义。如果两个都已指定,则 cron 表达式优先。如果没有指定,则构建将因 `IllegalStateException`失败。

CRON

CRON 触发器由类 cron 的表达式定义。比如,`"0 15 10 * * ?"`每天上午 10:15 触发。

CRON Trigger Example
@Scheduled(cron = "0 15 10 * * ?")
void fireAt1015AmEveryDay() { }

CRON 表达式中使用的语法由 quarkus.scheduler.cron-type`属性控制。这些值可以是 `cron4jquartzunix`和 `spring。默认情况下使用 quartz

`cron`属性支持 Property Expressions,包括默认值和嵌套 Property Expressions。(请注意,仍然支持“{property.path}”样式的表达式,但这些表达式不能提供 Property Expressions 的全部功能。)

CRON Config Property Example
@Scheduled(cron = "${myMethod.cron.expr}")
void myMethod() { }

如果你希望禁用一个特定的已调度方法,则可以将它的 cron 表达式设置成 "off"`或 `"disabled"

application.properties
myMethod.cron.expr=disabled

Property Expressions 允许你定义一个默认值,如果属性未配置,将使用该值。

CRON Config Property Example with default 0 0 15 ? * MON *
@Scheduled(cron = "${myMethod.cron.expr:0 0 15 ? * MON *}")
void myMethod() { }

如果属性 myMethod.cron.expr 未定义或为 null,将使用默认值 (0 0 15 ? * MON *)。

Time Zones

cron 表达式是在默认时区的上下文中计算的。然而,也可以将 cron 表达式与特定时区关联。

Time Zone Example
@Scheduled(cron = "0 15 10 * * ?", timeZone = "Europe/Prague") 1
void myMethod() { }
1 时区 ID 使用 java.time.ZoneId#of(String) 解析。

timeZone 属性支持 Property Expressions,包括默认值和 nestedProperty 表达式。

Time Zone Configuration Property Example
@Scheduled(cron = "0 15 10 * * ?", timeZone = "{myMethod.timeZone}")
void myMethod() { }

Intervals

时间间隔触发器定义了调用之间的间隔。间隔表达式基于 ISO-8601 持续时间格式 PnDTnHnMn.nS,并使用 java.time.Duration#parse(CharSequence) 解析 @Scheduled#every() 的值。但是,如果表达式以数字开头并以 d 结尾,将自动添加 P 前缀。如果表达式仅以数字开头,将自动添加 PT 前缀。因此,例如,可以使用 15m 代替 PT15M,并解析为“15 分钟”。

Interval Trigger Example
@Scheduled(every = "15m")
void every15Mins() { }

小于 1 秒的值可能不受底层调度程序实现支持。在这种情况下,将在生成和应用程序启动期间记录一条警告消息。

every 属性支持 Property Expressions,包括默认值和 nestedProperty 表达式。(请注意,"{property.path}" 样式表达式仍然受支持,但不提供 Property 表达式的全部功能。)

Interval Config Property Example
@Scheduled(every = "${myMethod.every.expr}")
void myMethod() { }

可以通过将间隔值设置为 "off""disabled" 来禁用间隔。例如,可以使用具有默认值 "off" 的 Property 表达式,如果其 Config Property 尚未设置,则禁用触发器。

Interval Config Property Example with a Default Value
@Scheduled(every = "${myMethod.every.expr:off}")
void myMethod() { }

Identity

默认情况下,将为每个已计划的方法生成一个唯一标识符。此标识符用于日志消息、调试期间以及作为某些 io.quarkus.scheduler.Scheduler 方法的参数。因此,指定显式标识符的可能性可能很有用。

Identity Example
@Scheduled(identity = "myScheduledMethod")
void myMethod() { }

identity 属性支持 Property Expressions,包括默认值和 nestedProperty 表达式。(请注意,"{property.path}" 样式表达式仍然受支持,但不提供 Property 表达式的全部功能。)

Interval Config Property Example
@Scheduled(identity = "${myMethod.identity.expr}")
void myMethod() { }

Delayed Execution

@Scheduled 提供了两种方法来延迟触发器应该开始触发的时刻。

@Scheduled#delay()@Scheduled#delayUnit() 一同形成初始延迟。

@Scheduled(every = "2s", delay = 2, delayUnit = TimeUnit.HOUR) 1
void everyTwoSeconds() { }
1 触发器在应用程序启动后两小时首次触发。

最终值始终四舍五入到整秒。

@Scheduled#delayed() 是上面属性的文本替代形式。间隔表达式基于 ISO-8601 持续时间格式 PnDTnHnMn.nS,并使用 java.time.Duration#parse(CharSequence) 解析值。但是,如果表达式以数字开头并以 d 结尾,将自动添加 P 前缀。如果表达式仅以数字开头,将自动添加 PT 前缀。因此,例如,可以使用 15s 代替 PT15S,并解析为“15 秒”。

@Scheduled(every = "2s", delayed = "2h")
void everyTwoSeconds() { }

如果 @Scheduled#delay() 设置为大于零的值,则忽略 @Scheduled#delayed() 的值。

@Scheduled#delay() 的主要优点是该值是可配置的。delay 属性支持 Property Expressions,包括默认值和 nestedProperty 表达式。(请注意,"{property.path}" 样式表达式仍然受支持,但不提供 Property 表达式的全部功能。)

@Scheduled(every = "2s", delayed = "${myMethod.delay.expr}") 1
void everyTwoSeconds() { }
1 config 属性 myMethod.delay.expr 用于设置延迟。

Concurrent Execution

默认情况下,可以同时执行计划的方法。尽管如此,也可以通过 @Scheduled#concurrentExecution() 指定处理并发执行的策略。

import static io.quarkus.scheduler.Scheduled.ConcurrentExecution.SKIP;

@Scheduled(every = "1s", concurrentExecution = SKIP) 1
void nonConcurrent() {
  // we can be sure that this method is never executed concurrently
}
1 Concurrent executions are skipped.

当跳过计划方法的执行时,会触发类型为 io.quarkus.scheduler.SkippedExecution 的 CDI 事件。

请注意,仅考虑同一应用程序实例中的执行。此功能不适用于集群。

Conditional Execution

你可以通过 @Scheduled#skipExecutionIf() 定义跳过任何计划方法执行的逻辑。指定类必须实现 io.quarkus.scheduler.Scheduled.SkipPredicate,如果 test() 方法的结果为 true,则跳过执行。此类必须表示 CDI Bean 或声明公共无参构造函数。对于 CDI,指定类在其 Bean 类型集中必须有且仅有一个 Bean,否则构建失败。此外,Bean 的作用域必须在作业执行期间处于活动状态。如果作用域为 @Dependent,则 Bean 实例专属特定计划方法,并且应用程序关闭时会被销毁。

class Jobs {

   @Scheduled(every = "1s", skipExecutionIf = MyPredicate.class) 1
   void everySecond() {
     // do something every second...
   }
}

@Singleton 2
class MyPredicate implements SkipPredicate {

   @Inject
   MyService service;

   boolean test(ScheduledExecution execution) {
       return !service.isStarted(); 3
   }
}
1 使用 MyPredicate.class 的 Bean 实例来评估是否应跳过执行。指定类在其 Bean 类型集中必须有且仅有一个 Bean,否则构建失败。
2 Bean 的作用域必须在执行期间处于活动状态。
3 MyService.isStarted() 返回 true 之前,Jobs.everySecond() 将被跳过。

请注意,这等效于以下代码:

class Jobs {

   @Inject
   MyService service;

   @Scheduled(every = "1s")
   void everySecond() {
     if (service.isStarted()) {
        // do something every second...
     }
   }
}

其主要思想是将跳过执行的逻辑保持在计划业务方法之外,以便可以轻松地重复使用和重构它。

当跳过计划方法的执行时,会触发类型为 io.quarkus.scheduler.SkippedExecution 的 CDI 事件。

要在应用程序启动/关闭时跳过计划执行,可以使用 io.quarkus.scheduler.Scheduled.ApplicationNotRunning 跳过谓词。

Non-blocking Methods

默认情况下,在阻塞任务的主执行器上执行计划方法。因此,这样一种被设计为在 Vert.x 事件循环中运行的技术(例如 Hibernate Reactive)不能在方法主体中使用。出于这个原因,返回 java.util.concurrent.CompletionStage<Void>io.smallrye.mutiny.Uni<Void>,或使用 @io.smallrye.common.annotation.NonBlocking 注释的计划方法相反在 Vert.x 事件循环中执行。

class Jobs {

   @Scheduled(every = "1s")
   Uni<Void> everySecond() { 1
     // ...do something async
   }
}
1 返回类型 Uni&lt;Void&gt; 指示计划程序在 Vert.x 事件循环上执行该方法。

How to use multiple scheduler implementations

在某些情况下,选择用于执行计划方法的计划程序实现可能是有用的。但是,默认情况下,所有计划方法仅使用一个 Scheduler 实现。例如,quarkus-quartz 扩展提供支持群集的实现,但它也从游戏中移除了简单的内存中实现。现在,如果启用了群集,则无法定义计划方法在单个节点上本地执行。不过,如果你将 quarkus.scheduler.use-composite-scheduler 配置属性设置为 true,那么将改用复合 Scheduler。这意味着多个计划程序实现将并行运行。此外,可以使用 @Scheduled#executeWith() 选择用于执行计划方法的特定实现。

class Jobs {

   @Scheduled(cron = "0 15 10 * * ?") 1
   void fireAt10AmEveryDay() { }

   @Scheduled(every = "1s", executeWith = Scheduled.SIMPLE) 2
   void everySecond() { }
}
1 如果存在 quarkus-quartz 扩展,则此方法将使用 Quartz 特定的计划程序执行。
2 如果设置了 quarkus.scheduler.use-composite-scheduler=true,则此方法将使用 quarkus-scheduler 扩展提供的简单的内存中实现执行。

Scheduler

Quarkus 提供了类型为 io.quarkus.scheduler.Scheduler 的内置 Bean,可以注入它并使用它来暂停/恢复计划程序和由特定 Scheduled#identity() 标识的单个计划方法。

Scheduler Injection Example
import io.quarkus.scheduler.Scheduler;

class MyService {

   @Inject
   Scheduler scheduler;

   void ping() {
      scheduler.pause(); 1
      scheduler.pause("myIdentity"); 2
      if (scheduler.isRunning()) {
         throw new IllegalStateException("This should never happen!");
      }
      scheduler.resume("myIdentity"); 3
      scheduler.resume(); 4
      scheduler.getScheduledJobs(); 5
      Trigger jobTrigger = scheduler.getScheduledJob("myIdentity"); 6
      if (jobTrigger != null && jobTrigger.isOverdue()){ 7
        // the job is late to the party.
      }
   }
}
1 Pause all triggers.
2 按标识暂停特定计划方法
3 按标识恢复特定计划方法
4 Resume the scheduler.
5 列出计划程序中的所有作业。
6 按标识获取特定计划作业的触发器元数据。
7 您可以用 quarkus.scheduler.overdue-grace-period 配置 isOverdue() 的宽限期

排程器或已排程作业暂停/恢复时,一个 CDI 事件将同步和异步触发。有效负载分别为 io.quarkus.scheduler.SchedulerPausedio.quarkus.scheduler.SchedulerResumedio.quarkus.scheduler.ScheduledJobPausedio.quarkus.scheduler.ScheduledJobResumed

Programmatic Scheduling

注入的 io.quarkus.scheduler.Scheduler 也可以用于通过编程方式排程作业。

Programmatic Scheduling
import io.quarkus.scheduler.Scheduler;

@ApplicationScoped
class MyJobs {

    @Inject
    Scheduler scheduler;

    void addMyJob() { 1
        scheduler.newJob("myJob")
            .setCron("0/5 * * * * ?")
            .setTask(executionContext -> { 2
                // do something important every 5 seconds
            })
            .schedule(); 3
    }

    void removeMyJob() {
        scheduler.unscheduleJob("myJob"); 4
    }
}
1 这是对标注有 @Scheduled(identity = "myJob", cron = "0/5 * * * * ?") 方法的编程替代。
2 业务逻辑在回调中定义。
3 作业在调用 JobDefinition#schedule() 方法后排程。
4 通过编程方式添加的作业也可以移除。

默认情况下,排程器不会启动,除非发现了 @Scheduled 业务方法。您可能需要通过 quarkus.scheduler.start-mode=forced 强制启动排程器以进行“纯粹”编程排程。

如果存在 Quartz extension,并且使用了 DB 存储类型,则无法将任务实例传递给作业定义,而必须使用任务类。Quartz API 也可以用于以编程方式排程作业。

在某些情况下,可能需要更为精细的方法,这就是 Quarkus 也公开可作为 CDI bean 注入的 java.util.concurrent.ScheduledExecutorServicejava.util.concurrent.ExecutorService 的原因。但是,这些执行器由其他 Quarkus 扩展使用,因此应谨慎使用。此外,用户绝不允许手动关闭这些执行器。

class JobScheduler {

   @Inject
   ScheduledExecutorService executor;

   void everySecondWithDelay() {
       Runnable myRunnable = createMyRunnable();
       executor.scheduleAtFixedRate(myRunnable, 3, 1, TimeUnit.SECONDS);
   }
}

Scheduled Methods and Testing

在运行测试时,通常希望禁用调度程序。可以通过运行时配置属性 quarkus.scheduler.enabled 禁用调度程序。如果设置为 false,则即使应用程序包含已排程方法也不会启动调度程序。您甚至可以为特定 Test Profiles 禁用调度程序。

Metrics

如果将 quarkus.scheduler.metrics.enabled 设置为 true 并且存在度量扩展,则会开箱即用地发布一些基本指标。

如果存在 Micrometer extension,则会自动向所有 @Scheduled 方法添加一个 @io.micrometer.core.annotation.Timed 拦截器绑定(除非它已经存在),并且会注册一个名为 scheduled.methodsio.micrometer.core.instrument.Timer 和一个名为 scheduled.methods.runningio.micrometer.core.instrument.LongTaskTimer。声明类和 @Scheduled 方法的完全限定名称用作标签。

如果存在 SmallRye Metrics extension,则会自动向所有 @Scheduled 方法添加一个 @org.eclipse.microprofile.metrics.annotation.Timed 拦截器绑定(除非它已经存在),并且会为每个 @Scheduled 方法创建一个 org.eclipse.microprofile.metrics.Timer。该名称由声明类的完全限定名称和 @Scheduled 方法的名称组成。计时器有一个标签 scheduled=true

OpenTelemetry Tracing

如果将 quarkus.scheduler.tracing.enabled 设置为 true,并且存在 OpenTelemetry extension,则使用 @Scheduled 注解定义或以编程方式排程的每个作业执行都会自动创建一个以作业的 Identity 命名的范围。

Run @Scheduled methods on virtual threads

@Scheduled 注释的方法也可以用 @RunOnVirtualThread 注释。在这种情况下,方法在虚拟线程上调用。

该方法必须返回 void,并且您的 Java 运行时必须为虚拟线程提供支持。阅读 the virtual thread guide 了解更多详情。

Configuration Reference

Unresolved include directive in modules/ROOT/pages/scheduler-reference.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-scheduler.adoc[]