Spring BootとQuartzスケジューラの統合ガイド

Javaにおけるタスクスケジューリング手法の比較

Javaで定期的な処理を実装する方法は複数存在します。それぞれの特徴を整理します。

  • Timer: シンプルで導入が容易ですが、単一スレッドでタスクを直列実行するため、1つのタスクが遅延や例外を起こすと後続タスクに影響します。複雑なスケジューリングには不向きです。
  • ScheduledExecutor: Java 5で導入されたスレッドプールベースの仕組みです。各タスクはプール内のスレッドで並列実行されるため、相互影響を受けません。ただし、複雑なスケジュールを実装するにはCalendarクラスと組み合わせる必要があり、やや煩雑です。
  • Spring Scheduler: Springが提供する簡易スケジューラです。実行時間の指定は可能ですが、タスクキューやスレッドプールの制御力は限定的で、小規模なバッチ処理に適しています。
  • JCronTab: Unix系crontab形式の構文でタスクを定義できるライブラリです。ファイル、データベース、XMLでの永続化、メール通知機能、高い拡張性を備えています。
  • Quartz: 強力なスケジューリング機能を持ち、Springとの連携が容易です。障害時にもタスク状態を保持する永続化、柔軟なトリガー設定、分散・クラスタリング対応が特徴です。

Quartzの概要

Quartzはオープンソースのタスクスケジューリングフレームワークで、Javaで実装されています。Timerと比較して、ジョブの永続化(状態保持)やジョブ管理機能を備えています。

Quartzの主要構成要素

  • Scheduler: タスクスケジューリングの中心的な制御を行うインターフェースです。
  • Job: 実際のビジネスロジックを実装するインターフェースで、executeメソッドに処理を記述します。
  • JobDetail: Jobのインスタンスを定義するオブジェクトで、Quartzでは通常JobDetailを指して「Job」と呼びます。
  • Trigger: ジョブの実行タイミングを定義するオブジェクトです。
  • JobBuilder / TriggerBuilder: JobDetailおよびTriggerインスタンスを生成するためのビルダークラスです。

Spring BootとQuartzの統合手順

1. 依存関係の追加

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

2. 設定ファイルの記述(application.yml)

spring:
  datasource:
    druid:
      url: jdbc:mysql://127.0.0.1:3306/my_test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
      username: root
      password: admin_123
      async-init: true
      initial-size: 5
      min-idle: 5
      max-active: 50
      max-wait: 6000
      pool-prepared-statements: false
      max-open-prepared-statements: 20
      validation-query: select 1
      test-on-borrow: false
      test-on-return: false
      test-while-idle: true
      time-between-eviction-runs-millis: 60000
      min-evictable-idle-time-millis: 300000
      max-evictable-idle-time-millis: 172800000
      keep-alive: true
      time-between-log-stats-millis: 86400000
      filter:
        stat:
          enabled: true
          log-slow-sql: true
          slow-sql-millis: 3000
      driver-class-name: com.mysql.jdbc.Driver
      type: com.alibaba.druid.pool.DruidDataSource
  quartz:
    job-store-type: jdbc
    wait-for-jobs-to-complete-on-shutdown: false
    overwrite-existing-jobs: true
    auto-startup: true
    startup-delay: 0s
    jdbc:
      initialize-schema: always
    properties:
      org:
        quartz:
          scheduler:
            instanceName: QuartzScheduler
            instanceId: AUTO
          jobStore:
            class: org.springframework.scheduling.quartz.LocalDataSourceJobStore
            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
            tablePrefix: QRTZ_
            isClustered: true
            dataSource: quartz
            clusterCheckinInterval: 10000
            useProperties: false
          threadPool:
            class: org.quartz.simpl.SimpleThreadPool
            threadCount: 10
            threadPriority: 5
            threadsInheritContextClassLoaderOfInitializingThread: true

3. データベーステーブルの作成

Quartzは11のシステムテーブルを必要とします。設定でinitialize-schema: alwaysを指定すると、アプリ起動時に自動生成されます。生成後はneverに変更してください。または、Quartz公式サイトから提供されるSQLスクリプトを直接実行してテーブルを作成することも可能です。

テーブル名説明
QRTZ_BLOB_TRIGGERSBlob型で保存されたトリガー
QRTZ_CALENDARSカレンダー情報(時間範囲の指定に使用)
QRTZ_CRON_TRIGGERSCron式のトリガー
QRTZ_FIRED_TRIGGERS実行済みトリガー
QRTZ_JOB_DETAILSJobDetail情報
QRTZ_JOB_LISTENERSジョブリスナー
QRTZ_LOCKS悲観ロック情報
QRTZ_PAUSED_TRIGGER_GRPS一時停止中のトリガー
QRTZ_SCHEDULER_STATEスケジューラ状態
QRTZ_SIMPLE_TRIGGERSシンプルトリガー情報
QRTZ_TRIGGER_LISTENERSトリガーリスナー

4. サンプル実装

Serviceインターフェース

public interface QuartzScheduleService {
    String registerCronJob(String taskName, String cronExpression, String jobClassPath);
    String removeCronJob(String taskName, String taskGroup, String triggerName, String triggerGroup);
    String runJobOnce(String taskName, String jobClassPath);
}

Service実装クラス

@Service
@Slf4j
public class QuartzScheduleServiceImpl implements QuartzScheduleService {
    @Autowired
    private Scheduler scheduler;

    private static final String DEFAULT_GROUP = "default_job_group";
    private static final String DEFAULT_TRIGGER_GROUP = "default_trigger_group";
    private static final String TRIGGER_PREFIX = "Trigger_";

    @Override
    public String registerCronJob(String taskName, String cronExpression, String jobClassPath) {
        try {
            JobKey jobKey = JobKey.jobKey(taskName, DEFAULT_GROUP);
            if (scheduler.checkExists(jobKey)) {
                log.warn("ジョブは既に存在します: {}", jobKey);
                return "EXISTS";
            }

            JobDetail jobDetail = JobBuilder.newJob(resolveJobClass(jobClassPath).getClass())
                    .withIdentity(jobKey)
                    .build();

            CronScheduleBuilder cronBuilder = CronScheduleBuilder.cronSchedule(cronExpression);

            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(TriggerKey.triggerKey(TRIGGER_PREFIX + taskName, DEFAULT_TRIGGER_GROUP))
                    .withSchedule(cronBuilder)
                    .build();

            scheduler.scheduleJob(jobDetail, trigger);
            scheduler.start();
            return "SUCCESS";
        } catch (Exception e) {
            log.error("ジョブ登録失敗: ", e);
            return "FAIL";
        }
    }

    @Override
    public String removeCronJob(String taskName, String taskGroup, String triggerName, String triggerGroup) {
        try {
            JobKey jobKey = JobKey.jobKey(taskName, taskGroup);
            TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, triggerGroup);

            if (scheduler.getTrigger(triggerKey) == null) {
                log.info("トリガーが見つかりません: triggerName={}, triggerGroup={}", triggerName, triggerGroup);
                return "SUCCESS";
            }

            scheduler.pauseTrigger(triggerKey);
            scheduler.unscheduleJob(triggerKey);
            scheduler.deleteJob(jobKey);

            log.info("ジョブを削除しました: jobName={}, jobGroup={}", taskName, taskGroup);
            return "SUCCESS";
        } catch (SchedulerException e) {
            log.error("ジョブ削除失敗: ", e);
            return "FAIL";
        }
    }

    @Override
    public String runJobOnce(String taskName, String jobClassPath) {
        try {
            JobKey jobKey = JobKey.jobKey(taskName, DEFAULT_GROUP);
            JobDetail jobDetail = JobBuilder.newJob(resolveJobClass(jobClassPath).getClass())
                    .withIdentity(jobKey)
                    .build();

            Trigger trigger = TriggerBuilder.newTrigger()
                    .withIdentity(TriggerKey.triggerKey(TRIGGER_PREFIX + taskName, DEFAULT_TRIGGER_GROUP))
                    .build();

            scheduler.scheduleJob(jobDetail, trigger);
            scheduler.start();
            return "SUCCESS";
        } catch (Exception e) {
            log.error("即時実行失敗: ", e);
            return "FAIL";
        }
    }

    private Job resolveJobClass(String className) throws Exception {
        Class<?> clazz = Class.forName(className);
        return (Job) clazz.getDeclaredConstructor().newInstance();
    }
}

ビジネスジョブ

@Component
@Slf4j
public class SampleTask implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        log.info("=== 業務処理の実行開始 ===");
        log.info("Job名: {}", context.getJobDetail().getKey().getName());
        log.info("Jobグループ: {}", context.getJobDetail().getKey().getGroup());
        log.info("トリガー名: {}", context.getTrigger().getKey().getName());
        log.info("トリガーグループ: {}", context.getTrigger().getKey().getGroup());
        log.info("前回の実行時刻: {}", context.getPreviousFireTime());
        log.info("今回の実行時刻: {}", context.getFireTime());
        log.info("次回の実行時刻: {}", context.getNextFireTime());
        log.info("スケジュール時刻: {}", context.getScheduledFireTime());
    }
}

リクエストボディ

@Data
public class TaskRequest {
    private String taskName;
    private String cronExpression;
    private String taskGroup;
    private String triggerName;
    private String triggerGroup;
}

RESTコントローラ

@RestController
@RequestMapping("/schedule")
public class ScheduleController {
    @Autowired
    private QuartzScheduleService scheduleService;

    @PostMapping("/create")
    public String createTask(@RequestBody TaskRequest request) {
        return scheduleService.registerCronJob(
                request.getTaskName(),
                request.getCronExpression(),
                "com.example.quartz.SampleTask"
        );
    }

    @PostMapping("/delete")
    public String deleteTask(@RequestBody TaskRequest request) {
        return scheduleService.removeCronJob(
                request.getTaskName(),
                request.getTaskGroup(),
                request.getTriggerName(),
                request.getTriggerGroup()
        );
    }

    @PostMapping("/run-once")
    public String runOnce(@RequestBody TaskRequest request) {
        return scheduleService.runJobOnce(
                request.getTaskName(),
                "com.example.quartz.SampleTask"
        );
    }
}

5. その他の操作用メソッド

// トリガーの一時停止
scheduler.pauseTrigger(TriggerKey.triggerKey("triggerName", "triggerGroup"));

// トリガーの再開
scheduler.resumeTrigger(TriggerKey.triggerKey("triggerName", "triggerGroup"));

タグ: Spring Boot Quartz Java タスクスケジューリング JobDetail

6月2日 17:48 投稿