CLOVER🍀

That was when it all began.

Quartzで、指定した時間に1回だけ起動するJobを作成する

これは、なにをしたくて書いたもの?

Quartzを使っていて、指定した時間に1度だけ起動するJobを定義したいなと思いまして。

指定した間隔で起動し続けるJobばかり扱っていたので、どうしたらいいんだろうと思ったのですが、割と単純でした。

1回だけ起動するJobを定義する

TriggerBuilder#withScheduleでどう表現したらいいんだろうとか思っていたのですが、Cookbookにそのまま書いてありました。
正確には、Triggerでコントロールするわけですが。

以下が登録したらすぐに起動し、繰り返し実行は行わないTriggerです。

// Define a Trigger that will fire "now", and not repeat
Trigger trigger = newTrigger()
    .withIdentity("trigger1", "group1")
    .startNow()
    .build();

How-To: Scheduling a Job

TriggerBuilder#withScheduleで起動スケジュールを指定しなければ良いのです。

あとは、TutorialのTriggerに関する記述を見ると、startTimeで最初にTriggerが有効になるタイミングを説明しています。

The “startTime” property indicates when the trigger’s schedule first comes into affect. The value is a java.util.Date object that defines a moment in time on a given calendar date. For some trigger types, the trigger will actually fire at the start time, for others it simply marks the time that the schedule should start being followed. This means you can store a trigger with a schedule such as “every 5th day of the month” during January, and if the startTime property is set to April 1st, it will be a few months before the first firing.

Lesson 4: More About Triggers

CookbookのドキュメントではTriggerBuilder#startNowを使っているので、開始時間が「即時」ですね。ちなみにTriggerBuilder#startNowを
使う場合は、明示的に書かなくてもデフォルト値がそもそも同じ意味になっています。

TriggerBuilder#startAtを使うことで、指定の時間から有効になるTriggerを定義することができます。今回は、こちらが焦点です。

それから、Triggerの期限も指定できるようですね。

The “endTime” property indicates when the trigger’s schedule should no longer be in effect. In other words, a trigger with a schedule of “every 5th day of the month” and with an end time of July 1st will fire for it’s last time on June 5th.

では、試していきたいと思います。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.4 2022-07-19
OpenJDK Runtime Environment (build 17.0.4+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.4+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.4, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-131-generic", arch: "amd64", family: "unix"

準備

Maven依存関係など。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.11</version>
        </dependency>
    </dependencies>

ログ出力にはLogbackを使用するので、設定ファイルも用意しておきます。

src/main/resources/logback.xml

<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="STDOUT"/>
    </root>
</configuration>

アプリケーションを作成する

まずは、Jobを定義します。

src/main/java/org/littlewings/quartz/oneshot/PrintMessageJob.java

package org.littlewings.quartz.oneshot;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PrintMessageJob implements Job {
    Logger logger = LoggerFactory.getLogger(PrintMessageJob.class);

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        logger.info("[{} / {}] Hello Job!!", context.getJobDetail().getKey(), context.getTrigger().getKey());
    }
}

実行したらメッセージを表示するJobですが、この時にJobDetailのキーとTriggerのキーを出力するようにしました。

mainクラス。

src/main/java/org/littlewings/quartz/oneshot/App.java

package org.littlewings.quartz.oneshot;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.impl.StdSchedulerFactory;

public class App {
    public static void main(String... args) {
        Scheduler scheduler = null;
        try {
            scheduler = StdSchedulerFactory.getDefaultScheduler();

            scheduler.start();

            JobDetail printMessageJob =
                    JobBuilder
                            .newJob(PrintMessageJob.class)
                            .withIdentity("printMessageJob1", "job-group1")
                            .build();

            Trigger triggerStartNow1 =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartNow1", "trigger-group1")
                            .build();

            Trigger triggerStartNow2 =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartNow2", "trigger-group1")
                            .startNow()
                            .build();

            Trigger triggerStartDelay1 =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartDelay1", "trigger-group1")
                            .startAt(Date.from(LocalDateTime.now().plusSeconds(5L).atZone(ZoneId.of("Asia/Tokyo")).toInstant()))
                            .build();

            Trigger triggerStartDelay2 =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartDelay2", "trigger-group1")
                            .startAt(Date.from(LocalDateTime.now().plusSeconds(10L).atZone(ZoneId.of("Asia/Tokyo")).toInstant()))
                            .build();

            Trigger triggerStartBeforeTime =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartBeforeTime", "trigger-group1")
                            .startAt(Date.from(LocalDateTime.now().minusSeconds(60L).atZone(ZoneId.of("Asia/Tokyo")).toInstant()))
                            .build();

            scheduler.scheduleJob(
                    printMessageJob,
                    Set.of(
                            triggerStartNow1,
                            triggerStartNow2,
                            triggerStartDelay1,
                            triggerStartDelay2,
                            triggerStartBeforeTime
                    ),
                    true
            );

            waitFor(Duration.ofSeconds(20L));
        } catch (SchedulerException e) {
            e.printStackTrace();
        } finally {
            if (scheduler != null) {
                try {
                    scheduler.shutdown(true);
                } catch (SchedulerException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    static void waitFor(Duration waitTime) {
        LocalDateTime startTime = LocalDateTime.now();
        while (ChronoUnit.SECONDS.between(startTime, LocalDateTime.now()) < waitTime.toSeconds()) {
            try {
                TimeUnit.SECONDS.sleep(3L);
            } catch (InterruptedException e) {
                // no-op
            }
        }
    }
}

とりあえず、Schedulerを開始します。

            scheduler = StdSchedulerFactory.getDefaultScheduler();

            scheduler.start();

JobDetailを作成。

            JobDetail printMessageJob =
                    JobBuilder
                            .newJob(PrintMessageJob.class)
                            .withIdentity("printMessageJob1", "job-group1")
                            .build();

このJobDetailに関連付けるTriggerを作成していきます。

まずは、即時起動するTrigger。

            Trigger triggerStartNow1 =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartNow1", "trigger-group1")
                            .build();

            Trigger triggerStartNow2 =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartNow2", "trigger-group1")
                            .startNow()
                            .build();

違いはTriggerBuilder#startNowを呼んでいるかどうかですが、startTimeのデフォルト値はnew Date()なので、開始タイミングという意味では
両者は等価です。

Schedulerに登録次第、すぐに開始します。

続いて、TriggerBuilder#startAtで指定した時間に起動するTrigger。今回は、5秒後と10秒後に起動するTriggerを用意。

            Trigger triggerStartDelay1 =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartDelay1", "trigger-group1")
                            .startAt(Date.from(LocalDateTime.now().plusSeconds(5L).atZone(ZoneId.of("Asia/Tokyo")).toInstant()))
                            .build();

            Trigger triggerStartDelay2 =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartDelay2", "trigger-group1")
                            .startAt(Date.from(LocalDateTime.now().plusSeconds(10L).atZone(ZoneId.of("Asia/Tokyo")).toInstant()))
                            .build();

あと、過去の時間にするとどうなるかというのも確認してみましょう。

            Trigger triggerStartBeforeTime =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("triggerStartBeforeTime", "trigger-group1")
                            .startAt(Date.from(LocalDateTime.now().minusSeconds(60L).atZone(ZoneId.of("Asia/Tokyo")).toInstant()))
                            .build();

(たぶんMisfire扱いになっているからだと思うのですが)答えとしては、即時起動になります。

これらのTriggerを、一括でJobDetailに関連付けてスケジューリング。

            scheduler.scheduleJob(
                    printMessageJob,
                    Set.of(
                            triggerStartNow1,
                            triggerStartNow2,
                            triggerStartDelay1,
                            triggerStartDelay2,
                            triggerStartBeforeTime
                    ),
                    true
            );

終了まで、20秒待ちます。

            waitFor(Duration.ofSeconds(20L));

確認する

では、動作確認してみましょう。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.quartz.oneshot.App

結果はこちら。

2022-11-03 17:03:06.440 [DefaultQuartzScheduler_Worker-3] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartBeforeTime] Hello Job!!
2022-11-03 17:03:06.440 [DefaultQuartzScheduler_Worker-1] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartNow1] Hello Job!!
2022-11-03 17:03:06.440 [DefaultQuartzScheduler_Worker-2] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartNow2] Hello Job!!
2022-11-03 17:03:11.435 [DefaultQuartzScheduler_Worker-4] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartDelay1] Hello Job!!
2022-11-03 17:03:16.434 [DefaultQuartzScheduler_Worker-5] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartDelay2] Hello Job!!

TriggerBuilder#startAtで過去時間を指定したものは即時起動、

2022-11-03 17:03:06.440 [DefaultQuartzScheduler_Worker-3] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartBeforeTime] Hello Job!!

未指定、またはTriggerBuilder#startNowを指定したものも即時起動、

2022-11-03 17:03:06.440 [DefaultQuartzScheduler_Worker-1] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartNow1] Hello Job!!
2022-11-03 17:03:06.440 [DefaultQuartzScheduler_Worker-2] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartNow2] Hello Job!!

TriggerBuilder#startAtで5秒、10秒遅らせて起動させるようにしたもの。

2022-11-03 17:03:11.435 [DefaultQuartzScheduler_Worker-4] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartDelay1] Hello Job!!
2022-11-03 17:03:16.434 [DefaultQuartzScheduler_Worker-5] INFO  o.l.quartz.oneshot.PrintMessageJob - [job-group1.printMessageJob1 / trigger-group1.triggerStartDelay2] Hello Job!!

わかりやすい結果になりました。

これで、今回確認したいことはOKです。

まとめ

Quartzで、指定した時間に1回だけ起動するJobを作成してみました。

割とSimpleTriggerやCronTriggerなどに目が行きがちだったので、盲点といえば盲点でしたね。

使い方として覚えておきましょう。