编辑此页面

调度器参考指南

现代应用程序通常需要定期运行特定任务。Quarkus 中有两个调度器扩展。quarkus-scheduler 扩展提供了 API 和一个轻量级的内存调度器实现。quarkus-quartz 扩展实现了 quarkus-scheduler 扩展的 API,并包含一个基于 Quartz 库的调度器实现。只有在更高级的调度用例(如持久化任务和集群)时,您才需要 quarkus-quartz

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

1. 计划方法

@io.quarkus.scheduler.Scheduled 注解的方法会自动安排调用。计划方法不能是抽象的或私有的。它可以是静态的,也可以是非静态的。计划方法可以被拦截器绑定注解,例如 @jakarta.transaction.Transactional@org.eclipse.microprofile.metrics.annotation.Counted

如果有一个没有作用域但声明了至少一个带有 @Scheduled 注解的非静态方法的 Bean 类,则使用 @Singleton

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

该注解是可重复的,因此一个方法可以被计划调用多次。

1.1. 元数据继承

子类永远不会继承父类中声明的 @Scheduled 方法的元数据。例如,假设 org.amce.Foo 类被 org.amce.Bar 类扩展。如果 Foo 声明了一个带有 @Scheduled 注解的非静态方法,那么 Bar 不会继承计划方法的元数据。在下面的示例中,everySecond() 方法仅在 Foo 实例上调用。

class Foo {

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

@Singleton
class Bar extends Foo {
}

1.2. CDI 事件

在特定事件发生时,会同步和异步地触发一些 CDI 事件。

类型 事件描述

io.quarkus.scheduler.SuccessfulExecution

计划任务成功完成执行。

io.quarkus.scheduler.FailedExecution

计划任务执行失败并抛出异常。

io.quarkus.scheduler.SkippedExecution

计划任务执行被跳过。

io.quarkus.scheduler.SchedulerPaused

调度器已被暂停。

io.quarkus.scheduler.SchedulerResumed

调度器已恢复。

io.quarkus.scheduler.ScheduledJobPaused

计划任务已被暂停。

io.quarkus.scheduler.ScheduledJobResumed

计划任务已恢复。

1.3. 触发器

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

1.3.1. CRON

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

CRON 触发器示例
@Scheduled(cron = "0 15 10 * * ?")
void fireAt1015AmEveryDay() { }

CRON 表达式中使用的语法由 quarkus.scheduler.cron-type 属性控制。可能的值为 cron4jquartzunixspring。默认使用 quartz

cron 属性支持 属性表达式,包括默认值和嵌套属性表达式。(请注意,仍支持 “{property.path}” 风格的表达式,但它们不提供属性表达式的全部功能。)

CRON 配置属性示例
@Scheduled(cron = "${myMethod.cron.expr}")
void myMethod() { }

如果您想禁用特定的计划方法,可以将 cron 表达式设置为 "off""disabled"

application.properties
myMethod.cron.expr=disabled

属性表达式允许您定义一个默认值,在属性未配置时使用。

带默认值 0 0 15 ? * MON * 的 CRON 配置属性示例
@Scheduled(cron = "${myMethod.cron.expr:0 0 15 ? * MON *}")
void myMethod() { }

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

1.3.1.1. 时区

cron 表达式在默认时区的上下文中进行评估。但是,也可以将 cron 表达式与特定时区关联。

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

timeZone 属性支持 属性表达式,包括默认值和嵌套属性表达式。

时区配置属性示例
@Scheduled(cron = "0 15 10 * * ?", timeZone = "${myMethod.timeZone}")
void myMethod() { }

1.3.2. 时间间隔

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

间隔触发器示例
@Scheduled(every = "15m")
void every15Mins() { }
底层调度器实现可能不支持小于一秒的值。在这种情况下,会在构建和应用程序启动期间记录警告消息。

every 属性支持 属性表达式,包括默认值和嵌套属性表达式。(请注意,仍支持 “{property.path}” 风格的表达式,但它们不提供属性表达式的全部功能。)

间隔配置属性示例
@Scheduled(every = "${myMethod.every.expr}")
void myMethod() { }

可以通过将值设置为 "off""disabled" 来禁用间隔。例如,可以使用带有默认值 "off" 的属性表达式,在未设置其配置属性时禁用触发器。

带默认值的间隔配置属性示例
@Scheduled(every = "${myMethod.every.expr:off}")
void myMethod() { }

1.4. 标识

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

标识示例
@Scheduled(identity = "myScheduledMethod")
void myMethod() { }

identity 属性支持 属性表达式,包括默认值和嵌套属性表达式。(请注意,仍支持 “{property.path}” 风格的表达式,但它们不提供属性表达式的全部功能。)

间隔配置属性示例
@Scheduled(identity = "${myMethod.identity.expr}")
void myMethod() { }

1.5. 触发器延迟启动

@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.path}” 风格的表达式,但它们不提供属性表达式的全部功能。)

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

1.6. 延迟执行

@Scheduled#executionMaxDelay() 可用于延迟计划方法的每次执行。该值表示触发器激活和计划方法执行之间的最大延迟。实际延迟是在 0 和指定最大延迟之间的随机数。

该值使用 DurationConverter#parseDuration(String) 进行解析。它可以是属性表达式,在这种情况下,调度器会尝试使用已配置的值:@Scheduled(executionMaxDelay = "${myJob.maxDelay}")。此外,属性表达式可以指定默认值:@Scheduled(executionMaxDelay = "${myJob.maxDelay}:500ms}")

@Scheduled(every = "2s", executionMaxDelay = "500ms") (1)
void everyTwoSeconds() { }
1 延迟将在 0 到 500 毫秒之间。因此,everyTwoSeconds() 执行之间的间隔将大致在 1.5 秒到 2.5 秒之间。

1.7. 并发执行

默认情况下,计划方法可以并发执行。但是,可以通过 @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 并发执行被跳过。
当计划方法的执行被跳过时,会触发一个类型为 io.quarkus.scheduler.SkippedExecution 的 CDI 事件。
请注意,仅考虑同一应用程序实例内的执行。此功能不旨在跨集群工作。

1.8. 条件执行

您可以使用 @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 Jobs.everySecond() 将被跳过,直到 MyService.isStarted() 返回 true

请注意,这等同于以下代码

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 跳过谓词。

1.9. 非阻塞方法

默认情况下,计划方法在主执行器上执行阻塞任务。因此,不能在方法体内部使用为在 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<Void> 指示调度器在 Vert.x 事件循环上执行该方法。

1.10. 如何使用多个调度器实现

在某些情况下,选择用于执行计划方法的调度器实现可能很有用。但是,默认情况下,所有计划方法都只使用一个 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 扩展提供的简单内存实现执行。

2. 调度器

Quarkus 提供了一个类型为 io.quarkus.scheduler.Scheduler 的内置 Bean,可以注入并用于暂停/恢复调度器和通过特定 Scheduled#identity() 标识的单个计划方法。

调度器注入示例
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 暂停所有触发器。
2 按标识暂停特定计划方法
3 按标识恢复特定计划方法
4 恢复调度器。
5 列出调度器中的所有作业。
6 按标识获取特定计划作业的触发器元数据。
7 您可以使用 quarkus.scheduler.overdue-grace-period 配置 isOverdue() 的宽限期
当调度器或计划作业被暂停/恢复时,会同步和异步地触发 CDI 事件。负载分别为 io.quarkus.scheduler.SchedulerPausedio.quarkus.scheduler.SchedulerResumedio.quarkus.scheduler.ScheduledJobPausedio.quarkus.scheduler.ScheduledJobResumed

3. 调度长时间运行的任务

执行长时间运行的任务可能会产生类似以下的警告消息

WARN  [io.ver.cor.imp.BlockedThreadChecker] (vertx-blocked-thread-checker) Thread Thread[vert.x-worker-thread-1,5,main] has been blocked for 81879 ms, time limit is 60000 ms: io.vertx.core.VertxException: Thread blocked

发生这种情况是因为默认工作线程池来自 Vert.x,它会防止线程被阻塞太长时间。

Vert.x 工作线程可以被阻塞的时间量也 是可配置的

因此,执行长任务的正确方法是将它们从计划方法卸载到自定义执行器服务。以下是一个适用于我们不希望频繁执行的长任务的设置示例

@ApplicationScoped
public class LongRunner implements Runnable {

    private ExecutorService executorService;

    @PostConstruct
    void init() {
        executorService = Executors.newThreadPerTaskExecutor(Executors.defaultThreadFactory()); (1)
    }

    @PreDestroy
    void destroy() {
        executorService.shutdown(); (2)
    }


    @Scheduled(cron = "{my.schedule}")
    public void update() {
        executorService.execute(this); (3)
    }

    @Override
    public void run() { (4)
        // perform the actual task here
    }
}
1 创建合适的执行器。在这种情况下,每个计划任务创建一个新线程,并在任务完成后停止。
2 @PreDestroy 回调用于关闭执行器服务。
3 计划方法仅将作业委托给自定义执行器 - 这可以防止 Vert.x 线程被阻塞。
4 Bean 实现 Runnable,这是我们可以直接作为参数传递给执行器服务的格式。

4. 编程调度

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

编程调度
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 扩展 并且使用了 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);
   }
}

5. 计划方法和测试

在运行测试时禁用调度器通常是可取的。可以通过运行时配置属性 quarkus.scheduler.enabled 来禁用调度器。如果设置为 false,即使应用程序包含计划方法,调度器也不会启动。您甚至可以为特定的 测试配置文件 禁用调度器。

6. 指标

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

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

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

7. OpenTelemetry 追踪

如果设置了 quarkus.scheduler.tracing.enabledtrue 并且存在 OpenTelemetry 扩展,那么每个作业执行(无论是使用 @Scheduled 注解定义的还是以编程方式计划的)都会自动创建一个以作业的 标识 命名的 span。

8. 在虚拟线程上运行 @Scheduled 方法

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

方法必须返回 void,并且您的 Java 运行时必须提供对虚拟线程的支持。有关更多详细信息,请阅读 虚拟线程指南

9. 配置参考

构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖

配置属性

类型

默认

在 CRON 表达式中使用的语法。

环境变量:QUARKUS_SCHEDULER_CRON_TYPE

显示更多

cron4jquartzunixspringspring53

quartz

如果存在指标扩展并且此值为 true,则将启用计划任务指标。

环境变量:QUARKUS_SCHEDULER_METRICS_ENABLED

显示更多

布尔值

false

控制是否启用跟踪。如果设置为 true 并且存在 OpenTelemetry 扩展,则将启用跟踪,从而为每个计划任务创建自动 Spans。

环境变量:QUARKUS_SCHEDULER_TRACING_ENABLED

显示更多

布尔值

false

默认情况下,只使用一个 Scheduler 实现。如果设置为 true,则使用一个复合 Scheduler,它委托给所有正在运行的实现。

调度器实现的启动将取决于 quarkus.scheduler.start-mode 的值,即,除非找到相关的 io.quarkus.scheduler.Scheduled 业务方法,否则不会启动调度器。

环境变量:QUARKUS_SCHEDULER_USE_COMPOSITE_SCHEDULER

显示更多

布尔值

false

是否启用调度器。

环境变量:QUARKUS_SCHEDULER_ENABLED

显示更多

布尔值

true

如果下一个执行时间超过此期限,计划任务将被标记为过期。

环境变量:QUARKUS_SCHEDULER_OVERDUE_GRACE_PERIOD

显示更多

Duration 

1S

调度器可以在不同的模式下启动。默认情况下,除非找到 io.quarkus.scheduler.Scheduled 业务方法,否则不会启动调度器。

环境变量:QUARKUS_SCHEDULER_START_MODE

显示更多

normal调度器不会启动,除非找到 io.quarkus.scheduler.Scheduled 业务方法。, forced即使没有找到计划的业务方法,调度器也会启动。这对于“纯”编程调度是必需的。, haltedforced 模式类似,但调度器在调用 Scheduler#resume() 之前不会开始触发作业。这对于执行一些需要在调度器启动前完成的初始化逻辑很有用。

normal除非找到 {@link io.quarkus.scheduler.Scheduled} 业务方法,否则不会启动调度器。

关于 Duration 格式

要编写持续时间值,请使用标准的 java.time.Duration 格式。有关更多信息,请参阅 Duration#parse() Java API 文档

您还可以使用简化的格式,以数字开头

  • 如果该值仅为一个数字,则表示以秒为单位的时间。

  • 如果该值是一个数字后跟 ms,则表示以毫秒为单位的时间。

在其他情况下,简化格式将被转换为 java.time.Duration 格式以进行解析

  • 如果该值是一个数字后跟 hms,则在其前面加上 PT

  • 如果该值是一个数字后跟 d,则在其前面加上 P

相关内容