Scheduling Periodic Tasks with Quartz
现代应用程序通常需要定期运行特定的任务。在本指南中,您将学习如何使用 Quartz 扩展来调度周期性集群任务。 :iokays-category: quarkus :iokays-path: modules/ROOT/pages/_includes/extension-status.adoc :keywords: Quarkus, 中文文档, 编程技术
该技术被认为是 {extension-status}。 有关可能状态的完整列表,请查看我们的 FAQ entry. |
如果只需要运行内存中调度程序,请使用 Scheduler 扩展。 |
- Prerequisites
- Architecture
- Solution
- Creating the Maven project
- Creating the Task Entity
- Creating a scheduled job
- Updating the application configuration file
- Creating a REST resource and a test
- Creating Quartz Tables
- Configuring the load balancer
- Setting Application Deployment
- Running the database
- Run the application in Dev Mode
- Packaging the application and run several instances
- Configuring the Instance ID
- Registering Plugin and Listeners
- Run scheduled methods on virtual threads
- Quartz Configuration Reference
Prerequisites
如要完成本指南,您需要:
-
Roughly 15 minutes
-
An IDE
-
安装了 JDK 17+,已正确配置
JAVA_HOME
-
Apache Maven ${proposed-maven-version}
-
如果你想使用 Quarkus CLI, 则可以选择使用
-
如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately
Solution
我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。
克隆 Git 存储库: git clone $${quickstarts-base-url}.git
,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。
解决方案位于 quartz-quickstart
directory 中。
Creating the Maven project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
--no-code
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 --gradle
或 --gradle-kotlin-dsl
选项。
有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
-DprojectGroupId={create-app-group-id} \
-DprojectArtifactId={create-app-artifact-id} \
-DnoCode
cd {create-app-artifact-id}
要创建一个 Gradle 项目,添加 -DbuildTool=gradle
或 -DbuildTool=gradle-kotlin-dsl
选项。
适用于 Windows 用户:
-
如果使用 cmd,(不要使用反斜杠
\
,并将所有内容放在同一行上) -
如果使用 Powershell,将
-D
参数用双引号引起来,例如"-DprojectArtifactId={create-app-artifact-id}"
它生成:
-
the Maven structure
-
一个可通过
http://localhost:8080
访问的登陆页面 -
针对
native
和jvm
模式的Dockerfile
示例文件 -
the application configuration file
Maven 项目还会导入 Quarkus Quartz 扩展。
如果您已配置好 Quarkus 项目,则可以通过在项目基目录中运行以下命令将 quartz
扩展添加到您的项目中:
quarkus extension add {add-extension-extensions}
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
./gradlew addExtension --extensions='{add-extension-extensions}'
这会将以下内容添加到构建文件中:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-quartz</artifactId>
</dependency>
implementation("io.quarkus:quarkus-quartz")
要使用 JDBC 存储,还需要 |
Creating the Task Entity
在 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 声明实体 |
Creating a scheduled job
在 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 在数据库中持久化任务。 |
Scheduling Jobs Programmatically
注入的 io.quarkus.scheduler.Scheduler
可以用于 schedule a job programmatically。但是,也可以直接利用 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 archive,它们也是 container-managed bean。 |
默认情况下,除非找到 |
Updating the application configuration file
编辑 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://localhost/quarkus_test
# Hibernate configuration
quarkus.hibernate-orm.database.generation=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
。
Creating a REST resource and a test
创建 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 响应和至少一个已创建的任务 |
Creating Quartz Tables
添加一个名为 src/main/resources/db/migration/V2.0.0__QuarkusQuartzTasks.sql
的 SQL 迁移文件,其内容是从链接中复制的文件内容:$${quickstarts-base-url}/blob/main/quartz-quickstart/src/main/resources/db/migration/V2.0.0_QuarkusQuartzTasks.sql[V2.0.0_QuarkusQuartzTasks.sql]。
Configuring the load balancer
在根目录中,创建包含以下内容的 nginx.conf
文件:
user nginx;
events {
worker_connections 1000;
}
http {
server {
listen 8080;
location / {
proxy_pass http://tasks:8080; 1
}
}
}
1 | 将所有流量路由到我们的任务应用程序中 |
Setting Application Deployment
在根目录中,创建包含以下内容的 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 | Define the tasks service |
2 | 定义 nginx 负载均衡器,以将传入流量路由到适当的节点 |
3 | 定义运行数据库的配置 |
Running the database
在单独的终端中,运行以下命令:
docker-compose up postgres 1
1 | 使用 docker-compose.yml 文件中提供的配置选项启动数据库实例 |
Run the application in Dev Mode
使用以下内容运行应用程序:
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
Packaging the application and run several instances
可以使用以下命令打包应用程序:
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 集群”的应用程序应是相同的,否则可能会出现不可预测的结果。
Configuring the Instance 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 生成器,而不是指定显式标识符。
Registering Plugin and Listeners
您可以通过 Quarkus 配置注册 plugins
、job-listeners
和 trigger-listeners
。
以下示例以 Job [{1}.{0}] execution complete and reports: {8}
定义的属性 jobSuccessMessage
注册名为 jobHistory
的插件 org.quartz.plugins.history.LoggingJobHistoryPlugin
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());
}
}
Run scheduled methods on virtual threads
用 @Scheduled
注释的方法也可以用 @RunOnVirtualThread
注释。在这种情况下,方法在虚拟线程上调用。
该方法必须返回 void
,并且您的 Java 运行时必须为虚拟线程提供支持。阅读 the virtual thread guide 了解更多详情。
该功能无法与 run-blocking-method-on-quartz-thread
选项结合使用。如果设置了 run-blocking-method-on-quartz-thread
,则计划方法将在一根由 Quartz 管理的(平台)线程上运行。