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

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

  • 安装了 JDK 17+,已正确配置 JAVA_HOME

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Architecture

在本指南中,我们将公开一个 Rest API tasks 以展示每 10 秒运行一次 Quartz 任务所创建的任务列表。

Solution

我们建议您遵循接下来的部分中的说明,按部就班地创建应用程序。然而,您可以直接跳到完成的示例。

克隆 Git 存储库: git clone $${quickstarts-base-url}.git,或下载 $${quickstarts-base-url}/archive/main.zip[存档]。

解决方案位于 quartz-quickstart directory 中。

Creating the Maven project

首先,我们需要一个新项目。使用以下命令创建一个新项目:

CLI
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 指南。

Maven
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 访问的登陆页面

  • 针对 nativejvm 模式的 Dockerfile 示例文件

  • the application configuration file

Maven 项目还会导入 Quarkus Quartz 扩展。

如果您已配置好 Quarkus 项目,则可以通过在项目基目录中运行以下命令将 quartz 扩展添加到您的项目中:

CLI
quarkus extension add {add-extension-extensions}
Maven
./mvnw quarkus:add-extension -Dextensions='{add-extension-extensions}'
Gradle
./gradlew addExtension --extensions='{add-extension-extensions}'

这会将以下内容添加到构建文件中:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-quartz</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-quartz")

要使用 JDBC 存储,还需要 quarkus-agroal 扩展,它提供了数据源支持。

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。

默认情况下,除非找到 @Scheduled 业务方法,否则不启动调度器。可能需要强制启动调度器进行“纯粹”的程序化调度。另请参见 Quartz Configuration Reference

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-policyignore-misfire-policyfire-nowcron-trigger-do-nothing。间隔作业的有效失火策略为:smart-policyignore-misfire-policyfire-nowsimple-trigger-reschedule-now-with-existing-repeat-countsimple-trigger-reschedule-now-with-remaining-repeat-countsimple-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

使用以下内容运行应用程序:

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

几秒钟后,打开另一个终端并运行 curl localhost:8080/tasks,以验证我们至少创建了一个任务。

和往常一样,可以使用以下命令打包应用程序:

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

并使用 java -jar target/quarkus-app/quarkus-run.jar 执行。

你还可以按如下方式生成本机可执行文件:

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true

Packaging the application and run several instances

可以使用以下命令打包应用程序:

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

生成成功后,运行以下命令:

docker-compose up --scale tasks=2 --scale nginx=1 1
1 启动应用程序和负载均衡器的两个实例

几秒钟后,在另一个终端中,运行 curl localhost:8080/tasks,以验证任务仅在不同时刻和 10 秒间隔内创建。

你还可以按如下方式生成本机可执行文件:

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./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 配置注册 pluginsjob-listenerstrigger-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 管理的(平台)线程上运行。

Quartz Configuration Reference

Unresolved include directive in modules/ROOT/pages/quartz.adoc - include::../../../target/quarkus-generated-doc/config/quarkus-quartz.adoc[]