CLOVER🍀

That was when it all began.

JavaのジョブスケジューラーQuartzを試す

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

JavaのジョブスケジューラーといえばQuartzが有名だと思いますが、そういえばちゃんと触ったことがなかったので少し試してみることに
しました。

Quartz

Quartzのオフィシャルサイトは、こちら。

Quartz Enterprise Job Scheduler

Quartzとはなにか?ということですが、オフィシャルサイトの紹介文を見ると、こういったことが書いてあります。

もう少し踏み込んだ紹介は、こちらのページに書かれています。

Overview

ジョブの起動タイミングやジョブの永続化などについても触れられています。

ドキュメントは、こちらです。

Documentation

現時点での最新安定版は2.3.2のようですが、ドキュメントとしては2.3.0のもので止まっているようなので、今回はこちらを見ていくことに
します。

Quartz 2.3.0 Documentation

ドキュメントは以下のように分かれています。

ExampleはGitHubのリポジトリにも含まれています。

https://github.com/quartz-scheduler/quartz/tree/v2.3.2/distribution/examples/src/main/java/org/quartz/examples

Quick Start Guide、Tutorial、ExamplesのFirst Quartz Programと似たような印象を受けるコンテンツいくつかありますが、
まずはQuick Start Guideから試していこうと思います。

環境

今回の環境は、こちら。

$ 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-126-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>org.slf4j</groupId>
            <artifactId>slf4j-simple</artifactId>
            <version>1.7.36</version>
        </dependency>
    </dependencies>

QuartzとSLF4J-Simpleを使っています。

Quartzを使ったプログラムを書いてみる

では、Quick Start Guideに沿って進めてみましょう。

Quick Start Guide

必要に応じて、他のドキュメントも参照していきます。

最初にダウンロードページからディストリビューションをダウンロードして、展開するとトップディレクトリにquartz-xxx.jarファイルがあると
書かれていますが、実際にはなくてlibディレクトリの中身を見た方が良さそうでした。

libディレクトリ内のファイルで、サードパーティ製のものは必要に応じて選ぶ感じの書かれ方をしていましたが、今回のファイルの中は
こんな感じでした。

$ curl -OL http://www.quartz-scheduler.org/downloads/files/quartz-2.3.0-distribution.tar.gz
$ tar xf quartz-2.3.0-distribution.tar.gz
$ cd quartz-2.3.0-SNAPSHOT
$ ll lib
合計 1728
drwxr-xr-x 2 xxxxx xxxxx   4096 10月  7 01:30 ./
drwxrwxr-x 8 xxxxx xxxxx   4096 10月  7 01:30 ../
-rwxr-xr-x 1 xxxxx xxxxx 497865  2月  5  2019 c3p0-0.9.5.2.jar*
-rwxr-xr-x 1 xxxxx xxxxx 481535  2月  5  2019 log4j-1.2.16.jar*
-rwxr-xr-x 1 xxxxx xxxxx 693579  2月 28  2019 quartz-2.3.0-SNAPSHOT.jar*
-rwxr-xr-x 1 xxxxx xxxxx  34023  2月 28  2019 quartz-jobs-2.3.0-SNAPSHOT.jar*
-rwxr-xr-x 1 xxxxx xxxxx  29257  2月  5  2019 slf4j-api-1.7.7.jar*
-rwxr-xr-x 1 xxxxx xxxxx   8870  2月  5  2019 slf4j-log4j12-1.7.7.jar*

なお、この状態の依存関係で

        <dependency>
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.3.2</version>
        </dependency>

Mavenのdependency:treeで見ると

$ mvn dependency:tree

こうなります。

[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ quartz-getting-started ---
[INFO] org.littlewings:quartz-getting-started:jar:0.0.1-SNAPSHOT
[INFO] \- org.quartz-scheduler:quartz:jar:2.3.2:compile
[INFO]    +- com.mchange:c3p0:jar:0.9.5.4:compile
[INFO]    +- com.mchange:mchange-commons-java:jar:0.2.15:compile
[INFO]    +- com.zaxxer:HikariCP-java7:jar:2.4.13:compile
[INFO]    \- org.slf4j:slf4j-api:jar:1.7.7:compile

SLF4Jで使うロギングライブラリだけ、ちょっと足りないですね。なので、SLF4J-Simpleを今回は使いました。

Quick Start Guidでは、まずプロパティファイルを用意してQuartzの設定をすることになっています。

Quartz Quick Start Guide / Configuration

最初はプロパティファイルを用意せず、デフォルト値で実行してみようと思います。

設定に関してはリファレンスがしっかりしていて良いですね。

Configuration Reference

次に、Jobを定義します。Quartzでは、Jobインターフェースを実装したクラスを作成する必要があるようです。

src/main/java/org/littlewings/quartz/HelloJob.java

package org.littlewings.quartz;

import java.time.LocalDateTime;

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

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

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        logger.info("execute job[{}]", LocalDateTime.now());
    }
}

起動したら現在時間を表示するだけのJobです。このようにして定義したJobが、Quartzのスケジューラーから呼び出されることに
なるようです。

Jobの作成については、Cookbookにも記載があります。こちらは、Jobにデータを渡す方法も書かれていますが。

Quartz Cookbook / How-To: Defining a Job (with input data)

このあとでJobDetailとして作成したものを、「Job Instance」と呼ぶみたいですね。

なお、Quick Start GuideにはHelloJobというクラスは名前しか登場しません。最後にQuartzのexampleに含まれてるリポジトリのパスも
記載しておくので、そちらを見るとよいでしょう。

次にmainクラスおよびスケジューラーの定義を行いましょう。

src/main/java/org/littlewings/quartz/QuartsGettingStarted.java

package org.littlewings.quartz;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.concurrent.TimeUnit;

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

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

            JobDetail helloJob =
                    JobBuilder
                            .newJob(HelloJob.class)
                            .withIdentity("helloJob", "job-group1")
                            .build();

            Trigger trigger =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("trigger1", "trigger-group1")
                            .withSchedule(
                                    SimpleScheduleBuilder
                                            .simpleSchedule()
                                            .withIntervalInSeconds(3)
                                            .repeatForever()
                            )
                            .build();

            scheduler.scheduleJob(helloJob, trigger);

            scheduler.start();

            waitFor(Duration.ofSeconds(30L));
        } 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 scheduler = null;
        try {
            scheduler = StdSchedulerFactory.getDefaultScheduler();

Cookbookによる、Schedulerのインスタンス化と開始の記述はこちら。

How-To: Instantiating a Scheduler

Schedulerはstartを呼び出すことで開始し、shutdownで停止します。

            scheduler.start();

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

Scheduler#shutdownには引数があるものとないものがあり、引数を指定しない、またはfalseを指定すると現在のJobの実行の終了を
待たずに終了するようです。trueを指定すると、Jobの実行終了を待機すると。

Cookbookによる、Schedulerの停止の記述はこちら。実行中のJobの扱いも、こちらに書かれています。

How-To: Shutting Down a Scheduler

ちなみに、この部分ですが

            waitFor(Duration.ofSeconds(30L));

すぐにSchedulerをshutdownしてしまうとプログラムが終了してしまうので、指定したDurationだけ待機するようにしています。
今回は30秒待つことにしました。

    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
            }
        }
    }

Quick Start Guideでは、Thread#sleepで待機したりするといいよと書かれていますね。

(you will also need to allow some time for the job to be triggered and executed before calling shutdown() - for a simple example such as this, you might just want to add a Thread.sleep(60000) call).

JobDetailの作成。JobBuilderを使って作成します。

            JobDetail helloJob =
                    JobBuilder
                            .newJob(HelloJob.class)
                            .withIdentity("helloJob", "job-group1")
                            .build();

こちらのCookbookでは、このようなJobDetailの作成を「Defining a Job Instance」と呼んでいます。

Quartz Cookbook / How-To: Defining a Job (with input data)

JobBuilder#withIdentityで指定しているのは、Jobの名前と所属するグループです。

Overviewによると、Jobには名前を付け、グループでまとめることができるようです。

Jobs are given names by their creator and can also be organized into named groups.

Overview

次に、Triggerを作成します。

            Trigger trigger =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("trigger1", "trigger-group1")
                            .withSchedule(
                                    SimpleScheduleBuilder
                                            .simpleSchedule()
                                            .withIntervalInSeconds(3)
                                            .repeatForever()
                            )
                            .build();

Triggerは、指定したスケジュールや間隔などでJobを起動する役割を持つものです。

Jobs are scheduled to run when a given Trigger occurs.

Overview

今回は、3秒おきに実行し続けるスケジューリングとしました。

                                    SimpleScheduleBuilder
                                            .simpleSchedule()
                                            .withIntervalInSeconds(3)
                                            .repeatForever()

Triggerにも名前とグループを指定できます。

Quick Start GuideなどでJobDetailとTriggerを同じグループ名に所属させているので勘違いしていたのですが、あくまでこれはTriggerに対する
グループ指定であり、JobDetailとTriggerは別々の管理になっているようです。

riggers may also be given names and placed into groups, in order to easily organize them within the scheduler.

Overview

JobDetailをTriggerに紐付けるには、Schedulerへの登録時に行います。

            scheduler.scheduleJob(helloJob, trigger);

Cookbookでは、JobDetailとTriggerの作成、Schedulerまでの関連付けをジョブのスケジューリングとして記載しています。

How-To: Scheduling a Job

ここまでが、Schedulerを取得してJobの作成やその実行タイミングの定義でした。

            scheduler = StdSchedulerFactory.getDefaultScheduler();

            JobDetail helloJob =
                    JobBuilder
                            .newJob(HelloJob.class)
                            .withIdentity("helloJob", "job-group1")
                            .build();

            Trigger trigger =
                    TriggerBuilder
                            .newTrigger()
                            .withIdentity("trigger1", "trigger-group1")
                            .withSchedule(
                                    SimpleScheduleBuilder
                                            .simpleSchedule()
                                            .withIntervalInSeconds(3)
                                            .repeatForever()
                            )
                            .build();

            scheduler.scheduleJob(helloJob, trigger);

            scheduler.start();

では、実行してみましょう。

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

Quartzが、デフォルト設定で起動します。

[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.simpl.SimpleThreadPool - Job execution threads will use class loader of thread: org.littlewings.quartz.QuartsGettingStarted.main()
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized.
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'DefaultQuartzScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'DefaultQuartzScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED started.

30秒間、Jobが繰り返し実行されます。

[DefaultQuartzScheduler_Worker-1] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:13.360251471]
[DefaultQuartzScheduler_Worker-2] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:16.355957563]
[DefaultQuartzScheduler_Worker-3] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:19.356242236]
[DefaultQuartzScheduler_Worker-4] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:22.355981361]
[DefaultQuartzScheduler_Worker-5] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:25.356468678]
[DefaultQuartzScheduler_Worker-6] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:28.355907533]
[DefaultQuartzScheduler_Worker-7] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:31.356178477]
[DefaultQuartzScheduler_Worker-8] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:34.355574682]
[DefaultQuartzScheduler_Worker-9] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:37.355939616]
[DefaultQuartzScheduler_Worker-10] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:40.356887985]
[DefaultQuartzScheduler_Worker-1] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:33:43.356656751]

そして、shutdown。

[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutting down.
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED paused.
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Scheduler DefaultQuartzScheduler_$_NON_CLUSTERED shutdown complete.

OKですね。

プロパティファイルを作成してみる

デフォルト設定でも起動していましたが、最後にプロパティファイルも作成してみましょう。

クラスパス直下にquartz.propertiesというファイルを作成します。

src/main/resources/quartz.properties

org.quartz.scheduler.instanceName = MyScheduler
org.quartz.threadPool.threadCount = 3

今回はQuick Start Guideに記載されていた項目のうち、インスタンス名とスレッド数のみ指定。

もう1度実行。

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

少し、ログの出方が変わりました。

[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.impl.StdSchedulerFactory - Using default implementation for ThreadExecutor
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.SchedulerSignalerImpl - Initialized Scheduler Signaller of type: class org.quartz.core.SchedulerSignalerImpl
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Quartz Scheduler v.2.3.2 created.
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.simpl.RAMJobStore - RAMJobStore initialized.
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'MyScheduler' with instanceId 'NON_CLUSTERED'
  Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
  NOT STARTED.
  Currently in standby mode.
  Number of jobs executed: 0
  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads.
  Using job-store 'org.quartz.simpl.RAMJobStore' - which does not support persistence. and is not clustered.

[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'MyScheduler' initialized from default resource file in Quartz package: 'quartz.properties'
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler version: 2.3.2
[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Scheduler MyScheduler_$_NON_CLUSTERED started.

スケジューラー名がDefaultQuartzSchedulerだったのがMySchedulerになり、quartz.propertiesを使っていることも出力されていますね。

[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.core.QuartzScheduler - Scheduler meta-data: Quartz Scheduler (v2.3.2) 'MyScheduler' with instanceId 'NON_CLUSTERED'


[org.littlewings.quartz.QuartsGettingStarted.main()] INFO org.quartz.impl.StdSchedulerFactory - Quartz scheduler 'MyScheduler' initialized from default resource file in Quartz package: 'quartz.properties'

スレッド数の変更も反映され

  Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 3 threads.

Jobを実行するためのスレッド名も変わり、3つのスレッドを使いまわしていることがわかります。

[MyScheduler_Worker-1] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:05.473665584]
[MyScheduler_Worker-2] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:08.467310053]
[MyScheduler_Worker-3] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:11.467012194]
[MyScheduler_Worker-1] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:14.467538797]
[MyScheduler_Worker-2] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:17.466925646]
[MyScheduler_Worker-3] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:20.467433465]
[MyScheduler_Worker-1] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:23.467141547]
[MyScheduler_Worker-2] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:26.466755037]
[MyScheduler_Worker-3] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:29.467242403]
[MyScheduler_Worker-1] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:32.466972963]
[MyScheduler_Worker-2] INFO org.littlewings.quartz.HelloJob - execute job[2022-10-08T16:39:35.467666051]

こちらもOKですね。

オマケ

このQuick Start Guideに近いQuartzのExample1は、こちらに含まれています。

Example 1 - Your First Quartz Program

https://github.com/quartz-scheduler/quartz/tree/v2.3.2/distribution/examples/src/main/java/org/quartz/examples/example1

まとめ

Quartzを使ってみました。Quick Start Guideをなぞった程度のものですが、あんまりちゃんと見たことがなかったのでいろいろと知ることが
多かったですね。

これから、もう少し掘り下げていきたいなと思います。