本ページはプロモーションが含まれています

Java入門

Quartz入門!Javaで複雑なスケジュール管理を実現する方法

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

システム開発の現場で、決まった時間に処理を動かしたい場面は必ず訪れます。私は過去10年以上のエンジニア生活の中で、数多くのバッチ処理を設計してきました。

駆け出しの頃は、単純なループ処理で実装してしまい、サーバー再起動でスケジュールが消えてしまうトラブルに見舞われたことがあります。夜中に呼び出されて対応したのは、今では苦い思い出です。

この記事を読めば、Javaでのスケジュール管理の悩みが解消されます。標準機能では手が届かない複雑な要件を、Quartzというフレームワークで解決する手法が身につくでしょう。

Quartzとは何か【Javaのスケジューラ定番フレームワーク】

Quartz(クォーツ)は、Javaアプリケーションに統合可能な、高機能なジョブスケジューリングライブラリです。

結論から述べると、Javaで商用レベルのスケジュール管理を行うならQuartz一択と言えます。理由は、オープンソースでありながら、企業の基幹システムにも耐えうる堅牢性と柔軟性を兼ね備えているからです。

具体的には、数万件のジョブを同時に管理したり、サーバーが落ちても処理を再開させる仕組みを持っています。単純なタイマー機能を超えた、ジョブ管理のデファクトスタンダードとして広く利用されています。

Quartzの特徴(cron管理・遅延実行・再実行制御など)

Quartz最大の特徴は、きめ細やかな制御ができる点にあります。

例えば、「毎月25日の朝9時に実行、ただし土日の場合は翌月曜日にずらす」といった複雑なカレンダー制御が可能です。これを自前で実装しようとすると、膨大な工数とバグのリスクを抱えることになります。

また、処理が失敗した際に自動でリトライさせたり、予定時刻に実行できなかったジョブをあとから実行するか無視するかといった「遅延実行(Misfire)」の制御も設定だけで完結します。

Java標準のScheduledExecutorとの違い

Javaには標準でScheduledExecutorServiceという機能がありますが、Quartzとは役割が明確に異なります。

標準機能は、あくまでメモリ上で動く簡易的なタイマーです。アプリケーションを再起動すれば、登録していたスケジュールはすべて消えてしまいます。

対してQuartzは、データベースと連携してスケジュール情報を永続化できます。これにより、アプリケーションの再起動やクラッシュが発生しても、次回起動時にスケジュールを復元し、止まっていた処理を正しく再開させられます。

Quartzが使われる代表的なユースケース

Quartzは、以下のような「失敗が許されない」あるいは「複雑な条件」が必要なシーンで活躍します。

  • 月次請求処理: 毎月決まった日に大量のデータを集計し、請求書を発行する。
  • メール配信予約: ユーザーが指定した未来の日時にメールを送信する。
  • データ連携: 夜間のシステム負荷が低い時間帯に、外部システムからデータを取り込む。
  • 不要データ削除: 一定期間経過したログファイルや一時データを定期的に掃除する。

Quartzの基本構造を理解しよう

Quartzを使いこなすには、主要な3つの登場人物を理解する必要があります。

これらは「何を(Job)」「いつ(Trigger)」「管理する(Scheduler)」という役割分担が明確です。この構造さえ頭に入れば、設計は驚くほどスムーズになります。

Job(実行したい処理)

Jobは、実際に実行させたいビジネスロジックそのものです。

具体的には、Jobというインターフェースを実装したクラスを作成し、その中に「メールを送る」「計算する」といった処理を記述します。Quartzはこのクラスを呼び出す役割に徹するため、処理の中身には関与しません。

Trigger(実行タイミングの設定)

Triggerは、Jobを実行する「引き金」です。

「今すぐ実行」「10秒後に実行」「毎日朝9時に実行」といったスケジュールの定義をここで行います。一つのJobに対して複数のTriggerを紐付けることも可能です。例えば、同じレポート作成処理を「毎月末」と「手動リクエスト時」の両方で動かすといった柔軟な運用が実現できます。

Scheduler(JobとTriggerを管理する司令塔)

Schedulerは、登録されたJobとTriggerを統括する管理コンテナです。

Triggerの設定時間を監視し、時間になると関連付けられたJobをインスタンス化して実行させます。開発者はこのSchedulerに対して、「このJobをこのTriggerで動かして」と指示を出すだけで済みます。

CronTriggerとSimpleTriggerの違い

Triggerには主に2つの種類があり、用途に応じて使い分けます。

  • SimpleTrigger: 「1回だけ実行」や「一定間隔で〇回繰り返す」といった単純なスケジュールに使います。
  • CronTrigger: 「毎週月曜日の10時」のように、カレンダーに基づいた複雑なスケジュールに使います。UNIX系OSで使われるcron式と同様の記法が利用可能です。

Quartzの導入と設定(Javaプロジェクトに組み込む方法)

実際にJavaプロジェクトへQuartzを導入する手順を見ていきましょう。

ここでは、ビルドツールとして広く使われているMavenやGradleを用いた設定方法と、最小限のコードで動作させる手順を解説します。

Maven / GradleでQuartzを追加する

まずはライブラリをプロジェクトに追加します。

Mavenの場合、pom.xmlに以下の依存関係を記述してください。バージョンはプロジェクトの要件に合わせて最新のものを選びましょう。

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

Gradleを使用している場合は、build.gradleに以下のように記述します。

implementation 'org.quartz-scheduler:quartz:2.3.2'

最小構成のサンプルコード(Job+Trigger)

次に、実際に動くコードを書いてみましょう。まずは実行させるJobクラスを作成します。

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;

public class HelloJob implements Job {
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("Hello, Quartz! 処理を実行しました。");
    }
}

続いて、このJobをスケジュール登録して実行するメインクラスです。

import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;

public class QuartzExample {
    public static void main(String[] args) {
        try {
            // 1. Schedulerの取得
            Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();

            // 2. Schedulerの開始
            scheduler.start();

            // 3. Jobの定義
            JobDetail job = JobBuilder.newJob(HelloJob.class)
                .withIdentity("myJob", "group1")
                .build();

            // 4. Triggerの定義(5秒ごとに実行)
            Trigger trigger = TriggerBuilder.newTrigger()
                .withIdentity("myTrigger", "group1")
                .startNow()
                .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(5)
                    .repeatForever())
                .build();

            // 5. スケジュール登録
            scheduler.scheduleJob(job, trigger);

            // 確認のため少し待機(実際はアプリケーションが動き続ける)
            Thread.sleep(20000);

            // 6. 終了
            scheduler.shutdown();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

このコードを実行すると、コンソールに5秒おきにメッセージが表示されます。これがQuartzの最も基本的な形です。

Cron式で柔軟にスケジュールを組む方法

実務ではSimpleTriggerよりも、CronTriggerを使う頻度が高いでしょう。

Cron式は、秒、分、時、日、月、曜日、年(オプション)の順でスペース区切りで記述します。QuartzのCron式は非常に柔軟です。

例を見てみましょう。

  • 0 0 12 * * ? : 毎日お昼の12時に実行
  • 0 15 10 ? * MON-FRI : 平日(月~金)の10時15分に実行
  • 0 0/5 * * * ? : 5分ごとに実行

コードでは以下のように指定します。

Trigger cronTrigger = TriggerBuilder.newTrigger()
    .withIdentity("cronTrigger", "group1")
    .withSchedule(CronScheduleBuilder.cronSchedule("0 0 12 * * ?"))
    .build();

Quartzの便利機能を深掘りする

基本が分かったところで、より実践的な機能について解説します。ここを理解することで、複雑なビジネス要件にも対応できるようになります。

JobDataMapでパラメータを渡す

Jobを実行する際、外部からパラメータを渡したい場合があります。

例えば、同じ「メール送信Job」でも、宛先や件名を動的に変えたい場合などです。この時に使うのがJobDataMapです。

// Job定義時にデータをセット
JobDetail job = JobBuilder.newJob(EmailJob.class)
    .usingJobData("emailAddress", "test@example.com")
    .usingJobData("retryCount", 3)
    .build();

Jobクラス側では、以下のようにデータを受け取ります。

public void execute(JobExecutionContext context) {
    JobDataMap dataMap = context.getJobDetail().getJobDataMap();
    String email = dataMap.getString("emailAddress");
    int retry = dataMap.getInt("retryCount");
    // ロジック...
}

Misfire(実行遅延)が起きた時の動き

サーバーがダウンしていたり、スレッドが枯渇していて予定時刻に処理が開始できなかった状態を「Misfire(ミスファイア)」と呼びます。

Quartzでは、このMisfire発生時にどう振る舞うかを設定できます。

  • FIRE_NOW: 気づいた時点ですぐに実行する。
  • IGNORE_MISFIRES: 遅れた分は無視して、次のスケジュールを待つ。
  • RESCHEDULE_NEXT_WITH_EXISTING_COUNT: 遅れた分をスキップし、回数指定などを維持して次へ進む。

これらはTriggerの設定時に指定します。重要なバッチであれば「即時実行」、ログ収集などであれば「スキップ」など、業務要件に合わせて選択します。

StatefulJobで重複実行を防ぐ方法

処理に時間がかかり、次の実行タイミングが来てしまうことがあります。

デフォルトでは、Quartzは前の処理が終わっていなくても、時間になれば新しいJobインスタンスを作成して並行実行します。しかし、データの整合性を保つために「前の処理が終わるまで次は待たせたい」場合があります。

その際は、Jobクラスに@DisallowConcurrentExecutionアノテーションを付与します。

@DisallowConcurrentExecution
public class HeavyBatchJob implements Job {
    // ...
}

これを付けるだけで、同一Job定義の重複実行を自動的に防いでくれます。

永続化設定(JDBC JobStore)とは?

Quartzの真価は、スケジュール情報をデータベース(DB)に保存できる点にあります。これを「JDBC JobStore」と呼びます。

メモリ上(RAMJobStore)での管理では、アプリケーション停止とともに予約情報が消えますが、DB管理にすることで以下のメリットが生まれます。

  • 障害復旧: サーバーが落ちても、再起動時に未実行のジョブを検知して実行できる。
  • クラスタリング: 複数のサーバーでQuartzを動かし、負荷分散や冗長化が可能になる。

設定はquartz.propertiesファイルで行い、DBにはQuartz専用のテーブルをいくつか作成する必要があります(SQLスクリプトは配布物に同梱されています)。

Quartzを本番環境で使う時の注意点

開発環境で動いたからといって、そのまま本番環境へ投入するのは危険です。長期運用を見据えた設計が必要になります。

スケジューラ停止や再起動時の挙動

アプリケーションを停止する際、実行中のジョブをどう扱うかを決めておく必要があります。

scheduler.shutdown(true)を呼び出すと、実行中のジョブが完了するのを待ってから停止します。これを「グレースフルシャットダウン」と呼びます。一方、引数をfalseにすると即座に停止しますが、データ不整合のリスクがあります。

デプロイ時などは、必ず完了を待つ設定にすることをお勧めします。

DB管理と負荷の考え方(JobStore選択)

JDBC JobStoreを使用する場合、DBへの負荷を考慮しなければなりません。

大量のトリガーが短期間に発火すると、DBへのアクセスが集中し、ロック待ちが発生してパフォーマンスが低下することがあります。数千件以上のジョブを扱う場合は、Quartz用のDBを業務データ用DBとは物理的に分けるなどの対策が有効です。

また、スレッドプールのサイズ(同時に実行できるジョブの数)も重要です。デフォルトは10ですが、サーバーのスペックに合わせて適切に調整してください。

ログと例外処理のベストプラクティス

Jobの中で発生した例外(エラー)は、適切にハンドリングしないと闇に葬られます。

Jobクラス内のexecuteメソッドでは、必ずtry-catchブロックで全体を囲み、エラーログを出力するようにしましょう。また、JobExecutionExceptionをスローする際に、setRefireImmediately(true)を設定すると、Quartzに「直ちに再実行してほしい」と伝えることも可能です。

単発のエラーで見過ごさず、運用担当者が気づける仕組みを入れておくことが大切です。

QuartzとSpringの連携(Spring Bootでラクに運用)

現代のJava開発において、Spring Framework(特にSpring Boot)を利用しないケースは稀でしょう。QuartzはSpringと非常に相性が良いです。

Spring Boot Starter Quartzを使うメリット

Spring Bootにはspring-boot-starter-quartzというスターターキットが用意されています。

これを使うと、面倒なschedulerの初期化や、quartz.propertiesの読み込み、DB接続設定などを自動で行ってくれます。開発者はJobの中身とスケジュールの定義に集中できるため、生産性が劇的に向上します。

SpringのDIでJobを管理する方法

素のQuartzでは、Jobクラスのインスタンス化はQuartzが行うため、Springの@Autowiredなどが使えません。

しかし、Spring統合機能を使えば、JobクラスもSpringの管理下(Bean)に置くことができます。これにより、Jobの中からServiceクラスやRepositoryクラスを自由に呼び出すことができ、複雑な業務ロジックもきれいに実装できます。

@Scheduledとの比較(どっちを使えばいいか)

Springには簡易的な@Scheduledアノテーションもあります。

  • @Scheduled: メソッドにアノテーションを付けるだけで動く。手軽だが、DB永続化やクラスタリング機能はない。
  • Quartz: 設定は少し増えるが、永続化、クラスタリング、詳細な制御が可能。

選択基準としては、サーバー再起動でスケジュールが飛んでも良い軽い処理なら@Scheduled、絶対に実行保証が必要な業務バッチならQuartzを選ぶのが正解です。

Quartzを使うべきケース/使わないべきケース

何でもQuartzを使えば良いというわけではありません。適切な道具を選ぶ目を持つことが重要です。

Quartzが向いているシステム

  • エンタープライズシステム: 信頼性が最優先される金融、流通、通信などの基幹システム。
  • 複雑なスケジュール: 「第3営業日」など、カレンダーロジックが必要な場合。
  • ジョブの動的追加: 管理画面からユーザーが自由にスケジュールを登録・変更する機能がある場合。

Quartzがオーバースペックになるケース

  • 単純な定期実行: 「10分おきにキャッシュをクリアする」程度なら、ScheduledExecutor@Scheduledで十分です。
  • マイクロサービス: 各サービスが個別にQuartzを持つと管理が複雑になります。

代替手段(AWS EventBridge、Spring @Scheduledなど)

クラウド環境、特にAWSを利用しているなら、Amazon EventBridge Schedulerも強力な選択肢です。

サーバーレスでスケジュール管理ができ、サーバーの維持管理コストがかかりません。バッチ処理自体をAWS Lambdaなどで実装し、キックする役割をEventBridgeに任せる構成は、モダンなアーキテクチャとして人気があります。

インフラに依存せず、アプリケーション内で完結させたい場合にQuartzを採用しましょう。

まとめ:Quartzを理解すれば複雑なバッチ処理も簡単に設計できる

今回は、Javaの強力なスケジューラであるQuartzについて解説しました。

結論として、業務システムで確実なスケジュール管理を行うなら、Quartzの導入を強くお勧めします。

理由は、Quartzが以下の3つの重要な課題を解決してくれるからです。

  1. 永続化: サーバー再起動でもスケジュールを守る。
  2. 柔軟性: 複雑なcron式やカレンダー制御に対応できる。
  3. 拡張性: Spring Bootとの連携やクラスタリングが容易。

最初は設定項目が多く感じるかもしれませんが、一度雛形を作ってしまえば、これほど頼りになる相棒はいません。単純なループ処理での実装は卒業し、Quartzを使って堅牢なシステム構築を目指しましょう。

まずは手元の開発環境で、最小構成のサンプルコードを動かすところから始めてみてはいかがでしょうか。その小さな一歩が、安定したシステム運用の第一歩になります。

  • この記事を書いた人
  • 最新記事

トム

・都内自社開発IT企業勤務/javaのバックエンドエンジニア
/java歴10年以上 ・首都圏在住30代
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎

-Java入門