使用 Quartz 调度周期性任务
先决条件
要完成本指南,您需要
-
大约 15 分钟
-
一个 IDE
-
已安装 JDK 17+ 并正确配置了
JAVA_HOME
-
Apache Maven 3.9.9
-
Docker 和 Docker Compose 或 Podman,以及 Docker Compose
-
如果您想使用它,可以选择 Quarkus CLI
-
如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置
解决方案
我们建议您按照以下章节中的说明,逐步创建应用程序。但是,您可以直接转到完整的示例。
克隆 Git 存储库:git clone https://github.com/quarkusio/quarkus-quickstarts.git
,或下载 存档。
解决方案位于 quartz-quickstart
目录中。
创建 Maven 项目
首先,我们需要一个新项目。使用以下命令创建一个新项目
对于 Windows 用户
-
如果使用 cmd,(不要使用反斜杠
\
并将所有内容放在同一行上) -
如果使用 Powershell,请将
-D
参数用双引号括起来,例如"-DprojectArtifactId=quartz-quickstart"
它生成
-
Maven 结构
-
一个可以在
https://:8080
上访问的欢迎页面 -
用于
native
和jvm
模式的示例Dockerfile
文件 -
应用程序配置文件
Maven 项目还导入了 Quarkus Quartz 扩展。
如果您已配置好 Quarkus 项目,可以通过在项目根目录中运行以下命令将 quartz
扩展添加到您的项目中:
quarkus extension add quartz
./mvnw quarkus:add-extension -Dextensions='quartz'
./gradlew addExtension --extensions='quartz'
这会将以下内容添加到您的构建文件中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-quartz</artifactId>
</dependency>
implementation("io.quarkus:quarkus-quartz")
要使用 JDBC 存储,还需要提供数据源支持的 |
创建任务实体
在 org.acme.quartz
包中,创建 Task
类,内容如下:
package org.acme.quartz;
import jakarta.persistence.Entity;
import java.time.Instant;
import jakarta.persistence.Table;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@Entity
@Table(name="TASKS")
public class Task extends PanacheEntity { (1)
public Instant createdAt;
public Task() {
createdAt = Instant.now();
}
public Task(Instant time) {
this.createdAt = time;
}
}
1 | 使用 Panache 声明实体。 |
创建计划任务
在 org.acme.quartz
包中,创建 TaskBean
类,内容如下:
package org.acme.quartz;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.transaction.Transactional;
import io.quarkus.scheduler.Scheduled;
@ApplicationScoped (1)
public class TaskBean {
@Transactional
@Scheduled(every = "10s", identity = "task-job") (2)
void schedule() {
Task task = new Task(); (3)
task.persist(); (4)
}
}
1 | 在 application 作用域中声明 bean。 |
2 | 使用 @Scheduled 注解指示 Quarkus 每 10 秒运行一次此方法,并为此作业设置唯一标识符。 |
3 | 创建一个新的 Task 并记录当前开始时间。 |
4 | 使用 Panache 将任务持久化到数据库。 |
以编程方式调度作业
可以注入 io.quarkus.scheduler.Scheduler
来 以编程方式调度作业。但是,也可以直接利用 Quartz API。您可以在任何 bean 中注入底层的 org.quartz.Scheduler
。
package org.acme.quartz;
@ApplicationScoped
public class TaskBean {
@Inject
org.quartz.Scheduler quartz; (1)
void onStart(@Observes StartupEvent event) throws SchedulerException {
JobDetail job = JobBuilder.newJob(MyJob.class)
.withIdentity("myJob", "myGroup")
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity("myTrigger", "myGroup")
.startNow()
.withSchedule(
SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(10)
.repeatForever())
.build();
quartz.scheduleJob(job, trigger); (2)
}
@Transactional
void performTask() {
Task task = new Task();
task.persist();
}
// A new instance of MyJob is created by Quartz for every job execution
public static class MyJob implements Job {
@Inject
TaskBean taskBean;
public void execute(JobExecutionContext context) throws JobExecutionException {
taskBean.performTask(); (3)
}
}
}
1 | 注入底层的 org.quartz.Scheduler 实例。 |
2 | 使用 Quartz API 调度新作业。 |
3 | 从作业调用 TaskBean#performTask() 方法。如果作业属于 bean 存档,它们也是 容器管理的 bean。 |
默认情况下,除非找到 @Scheduled 业务方法,否则调度程序不会启动。您可能需要强制启动调度程序以进行“纯”编程调度。另请参阅 Quartz 配置参考。 |
更新应用程序配置文件
编辑 application.properties
文件并添加以下配置:
# Quartz configuration
quarkus.quartz.clustered=true (1)
quarkus.quartz.store-type=jdbc-cmt (2)
quarkus.quartz.misfire-policy.task-job=ignore-misfire-policy (3)
# Datasource configuration.
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.jdbc.url=jdbc:postgresql:///quarkus_test
# Hibernate configuration
quarkus.hibernate-orm.schema-management.strategy=none
quarkus.hibernate-orm.log.sql=true
quarkus.hibernate-orm.sql-load-script=no-file
# flyway configuration
quarkus.flyway.connect-retries=10
quarkus.flyway.table=flyway_quarkus_history
quarkus.flyway.migrate-at-start=true
quarkus.flyway.baseline-on-migrate=true
quarkus.flyway.baseline-version=1.0
quarkus.flyway.baseline-description=Quartz
1 | 指示调度程序将在集群模式下运行。 |
2 | 使用数据库存储来持久化与作业相关的信息,以便它们可以在节点之间共享。 |
3 | 可以为每个作业配置其遗漏策略。task-job 是作业的标识符。 |
cron 作业的有效遗漏策略包括:smart-policy
、ignore-misfire-policy
、fire-now
和 cron-trigger-do-nothing
。间隔作业的有效遗漏策略包括:smart-policy
、ignore-misfire-policy
、fire-now
、simple-trigger-reschedule-now-with-existing-repeat-count
、simple-trigger-reschedule-now-with-remaining-repeat-count
、simple-trigger-reschedule-next-with-existing-count
和 simple-trigger-reschedule-next-with-remaining-count
。
创建 REST 资源和测试
创建 org.acme.quartz.TaskResource
类,内容如下:
package org.acme.quartz;
import java.util.List;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/tasks")
public class TaskResource {
@GET
public List<Task> listAll() {
return Task.listAll(); (1)
}
}
1 | 从数据库检索已创建任务的列表。 |
您还可以选择创建一个 org.acme.quartz.TaskResourceTest
测试,内容如下:
package org.acme.quartz;
import io.quarkus.test.junit.QuarkusTest;
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
public class TaskResourceTest {
@Test
public void tasks() throws InterruptedException {
Thread.sleep(1000); // wait at least a second to have the first task created
given()
.when().get("/tasks")
.then()
.statusCode(200)
.body("size()", is(greaterThanOrEqualTo(1))); (1)
}
}
1 | 确保我们获得 200 响应并且至少创建了一个任务。 |
创建 Quartz 表
添加一个名为 src/main/resources/db/migration/V2.0.0__QuarkusQuartzTasks.sql
的 SQL 迁移文件,其内容从 V2.0.0__QuarkusQuartzTasks.sql 文件复制。
配置负载均衡器
在根目录中,创建一个 nginx.conf
文件,内容如下:
user nginx;
events {
worker_connections 1000;
}
http {
server {
listen 8080;
location / {
proxy_pass http://tasks:8080; (1)
}
}
}
1 | 将所有流量路由到我们的任务应用程序。 |
设置应用程序部署
在根目录中,创建一个 docker-compose.yml
文件,内容如下:
version: '3'
services:
tasks: (1)
image: quarkus-quickstarts/quartz:1.0
build:
context: ./
dockerfile: src/main/docker/Dockerfile.${QUARKUS_MODE:-jvm}
environment:
QUARKUS_DATASOURCE_URL: jdbc:postgresql://postgres/quarkus_test
networks:
- tasks-network
depends_on:
- postgres
nginx: (2)
image: nginx:1.17.6
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- tasks
ports:
- 8080:8080
networks:
- tasks-network
postgres: (3)
image: postgres:14.1
container_name: quarkus_test
environment:
- POSTGRES_USER=quarkus_test
- POSTGRES_PASSWORD=quarkus_test
- POSTGRES_DB=quarkus_test
ports:
- 5432:5432
networks:
- tasks-network
networks:
tasks-network:
driver: bridge
1 | 定义任务服务。 |
2 | 定义 nginx 负载均衡器以将传入流量路由到适当的节点。 |
3 | 定义运行数据库的配置。 |
在开发模式下运行应用程序
使用以下命令运行应用程序
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
几秒钟后,打开另一个终端并运行 curl localhost:8080/tasks
以验证是否至少创建了一个任务。
与往常一样,可以使用以下命令打包应用程序
quarkus build
./mvnw install
./gradlew build
并使用 java -jar target/quarkus-app/quarkus-run.jar
执行。
您还可以使用以下命令生成原生可执行文件
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
打包应用程序并运行多个实例
可以使用以下命令打包应用程序:
quarkus build
./mvnw install
./gradlew build
构建成功后,运行以下命令:
docker-compose up --scale tasks=2 --scale nginx=1 (1)
1 | 启动应用程序的两个实例和一个负载均衡器。 |
几秒钟后,在另一个终端中运行 curl localhost:8080/tasks
,以验证任务仅在不同时间点以 10 秒的间隔创建。
您还可以使用以下命令生成原生可执行文件
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
部署者负责清除/删除先前状态,即陈旧的作业和触发器。此外,组成“Quartz 集群”的应用程序应相同,否则可能会出现不可预测的结果。 |
配置实例 ID
默认情况下,调度程序配置了一个简单的实例 ID 生成器,使用机器主机名和当前时间戳,因此在集群模式下运行时,您无需担心为每个节点设置适当的 instance-id
。但是,您可以通过设置配置属性引用或使用其他生成器来自行定义特定的 instance-id
。
quarkus.quartz.instance-id=${HOST:AUTO} (1)
1 | 这将展开 HOST 环境变量,如果 HOST 未设置,则使用 AUTO 作为默认值。 |
以下示例配置了名为 hostname
的生成器 org.quartz.simpl.HostnameInstanceIdGenerator
,因此您可以使用其名称作为 instance-id
。该生成器仅使用机器主机名,在提供节点唯一名称的环境中可能很合适。
quarkus.quartz.instance-id=hostname
quarkus.quartz.instance-id-generators.hostname.class=org.quartz.simpl.HostnameInstanceIdGenerator
部署者负责定义适当的实例标识符。此外,组成“Quartz 集群”的应用程序应包含唯一的实例标识符,否则可能会出现不可预测的结果。建议使用适当的实例 ID 生成器,而不是指定显式标识符。 |
注册插件和监听器
您可以通过 Quarkus 配置注册 plugins
、job-listeners
和 trigger-listeners
。
以下示例注册了名为 jobHistory
的插件 org.quartz.plugins.history.LoggingJobHistoryPlugin
,并将属性 jobSuccessMessage
定义为 Job [{1}.{0}] execution complete and reports: {8}
。
quarkus.quartz.plugins.jobHistory.class=org.quartz.plugins.history.LoggingJobHistoryPlugin
quarkus.quartz.plugins.jobHistory.properties.jobSuccessMessage=Job [{1}.{0}] execution complete and reports: {8}
您还可以使用注入的 org.quartz.Scheduler
以编程方式注册监听器。
public class MyListenerManager {
void onStart(@Observes StartupEvent event, org.quartz.Scheduler scheduler) throws SchedulerException {
scheduler.getListenerManager().addJobListener(new MyJogListener());
scheduler.getListenerManager().addTriggerListener(new MyTriggerListener());
}
}
在虚拟线程上运行计划方法
用 @Scheduled
注解标记的方法也可以用 @RunOnVirtualThread
注解标记。在这种情况下,方法将在虚拟线程上调用。
该方法必须返回 void
,并且您的 Java 运行时必须支持虚拟线程。有关更多详细信息,请阅读 虚拟线程指南。
此功能不能与 run-blocking-method-on-quartz-thread 选项结合使用。如果设置了 run-blocking-method-on-quartz-thread ,则计划方法将在 Quartz 管理的(平台)线程上运行。 |
Quartz 配置参考
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
---|---|---|
布尔值 |
|
|
调度程序实例与集群中的其他实例进行检查的频率(以毫秒为单位)。 如果使用 环境变量: 显示更多 |
long |
|
要使用的存储类型。 使用 要创建 Quartz 表,您可以通过 Flyway 扩展执行模式迁移,使用与您的数据库匹配的 SQL 脚本,该脚本可从 Quartz 存储库 中选择。 环境变量: 显示更多 |
|
|
要使用的线程池实现的类名。 需要注意的是,Quartz 线程不用于执行计划方法,而是默认使用常规 Quarkus 线程池。另请参阅 环境变量: 显示更多 |
字符串 |
|
要使用的数据源的名称。 如果使用 使用 环境变量: 显示更多 |
字符串 |
|
Quartz 作业存储表的表名前缀。 如果使用 环境变量: 显示更多 |
字符串 |
|
用于在“LOCKS”表中选择一行并锁定该行的 SQL 字符串。 如果使用 如果未设置,则应用 Quartz 的默认值,其中 一个示例 SQL 字符串: 环境变量: 显示更多 |
字符串 |
|
允许用户为自定义 JDBC 驱动程序代理指定完全限定的类名。 此属性是可选的,如果留空,Quarkus 将自动选择合适的默认驱动程序代理实现。 请注意,任何自定义实现都必须是现有 Quarkus 实现的子类,例如 环境变量: 显示更多 |
字符串 |
|
指示 JDBCJobStore 在 BLOB 列中序列化 JobDataMaps。 如果使用 如果设置为 如果设置为 环境变量: 显示更多 |
布尔值 |
|
字符串 |
|
|
Quartz 实例的标识符,该标识符必须在所有作为同一逻辑调度程序工作的调度程序之间是唯一的。使用默认值 环境变量: 显示更多 |
字符串 |
|
调度程序允许在计划触发时间之前获取和触发触发器的时间量(以毫秒为单位)。 环境变量: 显示更多 |
long |
|
调度程序节点一次允许获取(用于触发)的最大触发器数量。 环境变量: 显示更多 |
整数 |
|
调度程序线程池的大小。这将初始化池中的工作线程数量。 需要注意的是,Quartz 线程不用于执行计划方法,而是默认使用常规 Quarkus 线程池。另请参阅 环境变量: 显示更多 |
整数 |
|
整数 |
|
|
|
||
Quarkus 将等待当前正在运行的作业完成的最大时间。如果值为 环境变量: 显示更多 |
|
|
此作业的 Quartz 遗漏策略。 环境变量: 显示更多 |
|
|
此作业的 Quartz 遗漏策略。 环境变量: 显示更多 |
|
|
应直接传递给 Quartz 的属性。在此处使用完整的配置属性键,例如 此处设置的属性完全不受支持:由于 Quarkus 通常不了解这些属性及其用途,因此不能保证它们能正常工作,即使它们正常工作,在升级到更新版本的 Quarkus(即使只是微型/补丁版本)时也可能发生变化。 在回退到不受支持的属性之前,请考虑使用受支持的配置属性。如果没有,请务必提交功能请求,以便为 Quarkus 添加受支持的配置属性,更重要的是,可以定期测试该配置属性。 环境变量: 显示更多 |
Map<String,String> |
|
当设置为 启用此选项后,阻塞的计划方法不会在重复的上下文上运行。 环境变量: 显示更多 |
布尔值 |
|
类型 |
默认 |
|
此作业的 Quartz 遗漏策略。 环境变量: 显示更多 |
|
|
类型 |
默认 |
|
配置的类名。 环境变量: 显示更多 |
字符串 |
必需 |
传递给类的属性。 环境变量: 显示更多 |
Map<String,String> |
|
类型 |
默认 |
|
配置的类名。 环境变量: 显示更多 |
字符串 |
必需 |
传递给类的属性。 环境变量: 显示更多 |
Map<String,String> |
|
类型 |
默认 |
|
配置的类名。 环境变量: 显示更多 |
字符串 |
必需 |
传递给类的属性。 环境变量: 显示更多 |
Map<String,String> |
|
类型 |
默认 |
|
配置的类名。 环境变量: 显示更多 |
字符串 |
必需 |
传递给类的属性。 环境变量: 显示更多 |
Map<String,String> |
关于 Duration 格式
要写入时长值,请使用标准的 您还可以使用简化的格式,以数字开头
在其他情况下,简化格式将被转换为
|