调度器参考指南
现代应用程序通常需要定期运行特定任务。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 事件。
类型 | 事件描述 |
---|---|
|
计划任务成功完成执行。 |
|
计划任务执行失败并抛出异常。 |
|
计划任务执行被跳过。 |
|
调度器已被暂停。 |
|
调度器已恢复。 |
|
计划任务已被暂停。 |
|
计划任务已恢复。 |
1.3. 触发器
触发器由 @Scheduled#cron()
或 @Scheduled#every()
属性定义。如果两者都指定了,cron 表达式将优先。如果都没有指定,则构建会因 IllegalStateException
而失败。
1.3.1. 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.path}” 风格的表达式,但它们不提供属性表达式的全部功能。)
@Scheduled(cron = "${myMethod.cron.expr}")
void myMethod() { }
如果您想禁用特定的计划方法,可以将 cron 表达式设置为 "off"
或 "disabled"
。
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.SchedulerPaused 、io.quarkus.scheduler.SchedulerResumed 、io.quarkus.scheduler.ScheduledJobPaused 和 io.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.ScheduledExecutorService
和 java.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.methods
的 io.micrometer.core.instrument.Timer
和一个名称为 scheduled.methods.running
的 io.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.enabled
为 true
并且存在 OpenTelemetry 扩展,那么每个作业执行(无论是使用 @Scheduled
注解定义的还是以编程方式计划的)都会自动创建一个以作业的 标识 命名的 span。
8. 在虚拟线程上运行 @Scheduled 方法
用 @Scheduled
注解的方法也可以用 @RunOnVirtualThread
注解。在这种情况下,方法将在虚拟线程上调用。
方法必须返回 void
,并且您的 Java 运行时必须提供对虚拟线程的支持。有关更多详细信息,请阅读 虚拟线程指南。
9. 配置参考
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
---|---|---|
|
|
|
如果存在指标扩展并且此值为 true,则将启用计划任务指标。 环境变量: 显示更多 |
布尔值 |
|
控制是否启用跟踪。如果设置为 true 并且存在 OpenTelemetry 扩展,则将启用跟踪,从而为每个计划任务创建自动 Spans。 环境变量: 显示更多 |
布尔值 |
|
默认情况下,只使用一个 调度器实现的启动将取决于 环境变量: 显示更多 |
布尔值 |
|
布尔值 |
|
|
如果下一个执行时间超过此期限,计划任务将被标记为过期。 环境变量: 显示更多 |
|
|
调度器可以在不同的模式下启动。默认情况下,除非找到 环境变量: 显示更多 |
|
|
关于 Duration 格式
要编写持续时间值,请使用标准的 您还可以使用简化的格式,以数字开头
在其他情况下,简化格式将被转换为
|