Spring Bootで@Scheduledアノテーションを使おう


Last updated on

皆さんはSpring Bootで定期的に実行したい処理、例えばログローテーションやデータのバックアップなど、ありますでしょうか?
そんな時に使えるのが、@Scheduledアノテーションです。Springには、Spring Batchのようなバッチ処理フレームワークもありますが、簡単な定期実行処理には@Scheduledアノテーションが便利です。
この記事では、@Scheduledアノテーションの使い方を、具体的なコード例を交えながら解説します。

@Scheduledアノテーションとは?

@ScheduledアノテーションはSpring Frameworkが提供する機能で、指定したスケジュールに従ってメソッドを実行してくれます。特に追加でライブラリなどの導入は不要なので、気軽に利用できます。
必要なのは、@EnableSchedulingアノテーションを有効にしたクラスを定義することだけです。
これは、Spring Bootがスケジュールされたタスクを検出して管理するために必要になります。

使い方

例えば、以下のようなクラスを考えましょう。

import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
@EnableScheduling
public class ScheduledTask {

    @Scheduled(fixedRate = 5000) // 5秒ごとに実行
    public void reportCurrentTime() {
        System.out.println("現在時刻: " + LocalDateTime.now());
    }

    @Scheduled(cron = "0 0 * * * *") // 毎時0分に実行
    public void executeEveryHour() {
        System.out.println("毎時0分に実行されるタスクです。");
    }
}

このコードでは、@ComponentアノテーションによってSpringコンテナに管理されるScheduledTaskクラスを定義しています。
@EnableSchedulingアノテーションにより、スケジュールされたタスクの機能が有効になります。

reportCurrentTime()メソッドは、@Scheduled(fixedRate = 5000)によって5秒ごとに実行されます。fixedRate属性は、前回のメソッド実行開始から次の実行開始までの時間をミリ秒単位で指定します。

executeEveryHour()メソッドは、@Scheduled(cron = "0 0 * * * *")によって毎時0分に実行されます。cron属性はcron式を使用し、より柔軟なスケジュール設定が可能です。 cron式は、秒、分、時、日、月、曜日の順で指定します。 "0 0 * * * *" は、「毎時0分0秒に実行する」という意味になります。
cron式の詳細な書き方については、詳細をまとめた記事や便利なツールがあったりするので、調べてみると良いでしょう。

その他の属性

上記で触れた以外にも、@ScheduledアノテーションにはfixedDelayinitialDelayなどの属性があり、これらを使うことで、より詳細なスケジュール設定を行うことができます。fixedDelayは、前回のメソッド実行完了から次の実行開始までの時間をミリ秒単位で指定しますが、fixedRateと異なり、メソッドの実行時間が考慮されます。initialDelayは、最初のメソッド実行までの遅延時間をミリ秒単位で指定します。

また、タイムゾーンを指定するzone属性もあります。デフォルトでは、システムのデフォルトタイムゾーンが使用されますが、必要に応じて指定することができます。 確実に日本時間で動かしたい、といった場合には以下のように指定しておくと良いでしょう。

@Scheduled(cron = "0 0 * * * *", zone = "Asia/Tokyo")

実務でつまずきやすいポイント

@Scheduledは簡単に使える一方で、本番運用では次の落とし穴に注意が必要です。

1. 例外で処理が止まったことに気づけない

定期タスク内で例外が発生すると、ログだけ出て見逃されるケースがあります。
最低限、以下を徹底すると運用が安定します。

  • 処理開始/終了ログを出す
  • 失敗時にエラーログへスタックトレースを残す
  • 必要なら通知(Slack/メール)を送る

2. 処理時間 > 実行間隔でタスクが詰まる

fixedRateを短くしすぎると、処理時間が追い付かずにリソースを圧迫します。
時間がかかる処理はfixedDelayを使うか、キューイングで非同期化する方が安全です。

3. 複数インスタンスで重複実行される

Kubernetesなどでアプリを複数台にすると、同じ@Scheduledが台数分実行されます。
「1回だけ実行したい」タスクは、DBロックや分散ロック(例: ShedLock)を併用しましょう。

以下は、重複実行を避けたい時のイメージです。

@Scheduled(cron = "0 */10 * * * *", zone = "Asia/Tokyo")
@SchedulerLock(name = "dailyAggregateTask", lockAtMostFor = "PT5M", lockAtLeastFor = "PT30S")
public void aggregateDailyMetrics() {
    // 集計処理
}

fixedRate / fixedDelay / cron の使い分け

判断に迷ったら、次の基準で選ぶと失敗しにくいです。

  • fixedRate: 一定間隔で実行したい計測系タスク向け
  • fixedDelay: 前回完了後に間隔を空けたい重めのタスク向け
  • cron: 「毎日3:00」「平日9:00」など業務時刻に合わせたいタスク向け

定期処理は「いつ動くか」より「失敗時にどう復旧するか」が運用品質を左右します。
初期実装の段階で、リトライ方針と通知方針まで決めておくのがおすすめです。

スレッドプールを明示して安定運用する

@Scheduledの実行基盤はデフォルト設定のままでも動きますが、タスクが増えると競合しやすくなります。
実務では、スケジューラ専用のスレッドプールを明示するのが安全です。

@Configuration
@EnableScheduling
public class SchedulingConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(4);
        scheduler.setThreadNamePrefix("scheduled-task-");
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        scheduler.setAwaitTerminationSeconds(30);
        return scheduler;
    }
}

タスク数や実行時間に応じてpoolSizeを調整し、シャットダウン時に中断されないよう設定しておくと運用事故を減らせます。

定期処理をテストするときの考え方

@Scheduledそのものを待ってテストすると時間がかかるため、実務では次の形が扱いやすいです。

  • スケジュール対象の業務処理を通常メソッドへ分離する
  • テストではその通常メソッドを直接呼ぶ
  • スケジュール設定の検証は最小限(起動時確認)に留める

「実行ロジック」と「いつ実行するか」を分離しておくと、テストも保守も簡単になります。

まとめ

@Scheduledアノテーションは、定期的なタスクの実行を簡潔に記述できる強力なツールです。cron式をマスターすれば、様々なスケジュールに対応できるようになります。
ぜひ、自分のアプリケーションで活用してみてください。
そして、必要に応じてエラー処理やログ出力なども追加し、堅牢なシステム構築を目指しましょう。