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 类没有作用域,并且声明至少一个使用 |
此外,进行注解的方法必须返回 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 |
---|---|
|
已成功完成一次预定作业的执行。 |
|
已在发生异常的情况下完成一次预定作业的执行。 |
|
已跳过一次预定作业的执行。 |
|
The scheduler was paused. |
|
The scheduler was resumed. |
|
已暂停一次预定作业。 |
|
已恢复一次预定作业。 |
Triggers
触发器由 `@Scheduled#cron()`或 `@Scheduled#every()`属性定义。如果两个都已指定,则 cron 表达式优先。如果没有指定,则构建将因 `IllegalStateException`失败。
CRON
CRON 触发器由类 cron 的表达式定义。比如,`"0 15 10 * * ?"`每天上午 10:15 触发。
@Scheduled(cron = "0 15 10 * * ?")
void fireAt1015AmEveryDay() { }
CRON 表达式中使用的语法由 quarkus.scheduler.cron-type`属性控制。这些值可以是 `cron4j
、quartz
、unix`和 `spring
。默认情况下使用 quartz
。
`cron`属性支持 Property Expressions,包括默认值和嵌套 Property Expressions。(请注意,仍然支持“{property.path}”样式的表达式,但这些表达式不能提供 Property Expressions 的全部功能。)
@Scheduled(cron = "${myMethod.cron.expr}")
void myMethod() { }
如果你希望禁用一个特定的已调度方法,则可以将它的 cron 表达式设置成 "off"`或 `"disabled"
。
myMethod.cron.expr=disabled
Property Expressions 允许你定义一个默认值,如果属性未配置,将使用该值。
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 表达式与特定时区关联。
@Scheduled(cron = "0 15 10 * * ?", timeZone = "Europe/Prague") 1
void myMethod() { }
1 | 时区 ID 使用 java.time.ZoneId#of(String) 解析。 |
timeZone
属性支持 Property Expressions,包括默认值和 nestedProperty 表达式。
@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 分钟”。
@Scheduled(every = "15m")
void every15Mins() { }
小于 1 秒的值可能不受底层调度程序实现支持。在这种情况下,将在生成和应用程序启动期间记录一条警告消息。
every
属性支持 Property Expressions,包括默认值和 nestedProperty 表达式。(请注意,"{property.path}"
样式表达式仍然受支持,但不提供 Property 表达式的全部功能。)
@Scheduled(every = "${myMethod.every.expr}")
void myMethod() { }
可以通过将间隔值设置为 "off"
或 "disabled"
来禁用间隔。例如,可以使用具有默认值 "off"
的 Property 表达式,如果其 Config Property 尚未设置,则禁用触发器。
@Scheduled(every = "${myMethod.every.expr:off}")
void myMethod() { }
Identity
默认情况下,将为每个已计划的方法生成一个唯一标识符。此标识符用于日志消息、调试期间以及作为某些 io.quarkus.scheduler.Scheduler
方法的参数。因此,指定显式标识符的可能性可能很有用。
@Scheduled(identity = "myScheduledMethod")
void myMethod() { }
identity
属性支持 Property Expressions,包括默认值和 nestedProperty 表达式。(请注意,"{property.path}"
样式表达式仍然受支持,但不提供 Property 表达式的全部功能。)
@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()
的主要优点是该值是可配置的。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. |
当跳过计划方法的执行时,会触发类型为 |
请注意,仅考虑同一应用程序实例中的执行。此功能不适用于集群。 |
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...
}
}
}
其主要思想是将跳过执行的逻辑保持在计划业务方法之外,以便可以轻松地重复使用和重构它。
当跳过计划方法的执行时,会触发类型为 |
要在应用程序启动/关闭时跳过计划执行,可以使用 |
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<Void> 指示计划程序在 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()
标识的单个计划方法。
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 事件将同步和异步触发。有效负载分别为 |
Programmatic Scheduling
注入的 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 | 通过编程方式添加的作业也可以移除。 |
默认情况下,排程器不会启动,除非发现了 |
如果存在 Quartz extension,并且使用了 DB 存储类型,则无法将任务实例传递给作业定义,而必须使用任务类。Quartz API 也可以用于以编程方式排程作业。 |
在某些情况下,可能需要更为精细的方法,这就是 Quarkus 也公开可作为 CDI bean 注入的 java.util.concurrent.ScheduledExecutorService
和 java.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.methods
的 io.micrometer.core.instrument.Timer
和一个名为 scheduled.methods.running
的 io.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 了解更多详情。