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

なお、「起動時に一度だけ実行したい」だけなら@ScheduledよりCommandLineRunnerの方が向いています。起動時処理の実装方法については Spring BootのCommandLineRunner・ApplicationRunnerで起動時処理を実行する方法 で解説しています。

Spring Bootでスケジューリング(定期実行)を実装する手順

@ScheduledアノテーションはSpring Frameworkが提供する機能で、指定したスケジュールに従ってメソッドを実行してくれます。特に追加でライブラリなどの導入は不要なので、気軽に利用できます。
Spring Bootでスケジューリングを実装するのに必要なのは、@EnableSchedulingを付けたクラスを1つ用意し、定期実行したいメソッドに@Scheduledを付けることだけです。本記事はSpring Boot 3.x(Spring Framework 6.x)を前提に説明します。

手順は次の3ステップです。

  1. @Configurationクラス(またはメインクラス)に@EnableSchedulingを付けて、スケジューリング機能を有効にする
  2. @ComponentなどでBean登録したクラスのメソッドに@Scheduledを付けて、fixedRate・fixedDelay・cronのいずれかで実行タイミングを指定する
  3. アプリを起動し、ログで意図した間隔・時刻に実行されているか確認する

@EnableSchedulingはアプリ内で1か所あれば十分です。複数のクラスに付けても動きますが、どこで有効にしているか分かりにくくなるので、設定クラスに1つだけ置くのがおすすめです。
なお、@Scheduledは引数なしのメソッドに付けるのが基本で、戻り値は通常voidにします。Springコンテナが定期的に呼び出すため、呼び出し元から値を受け取る前提では書けない点を押さえておきましょう。

使い方

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

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式の詳細な書き方については、詳細をまとめた記事や便利なツールがあったりするので、調べてみると良いでしょう。

fixedRateとfixedDelayの違い(fixedDelay vs fixedRate)

上記で触れた以外にも、@ScheduledアノテーションにはfixedDelay、initialDelayなどの属性があります。中でも質問が多いのがfixedRateとfixedDelayの違いで、ポイントは「間隔をどこから数えるか」です。fixedRateは前回の実行「開始」から、fixedDelayは前回の実行「完了」から次の実行までの時間を数えます。処理に時間がかかるタスクほど、この差が効いてきます。

属性間隔の起点処理時間の影響向いている用途
fixedRate前回の実行開始受けない(処理が長いと次の実行が詰まる)一定周期で動かしたい計測・ポーリング
fixedDelay前回の実行完了受ける(完了後に必ず間隔が空く)処理時間が読めない重めのバッチ

例えば処理に3秒かかるタスクを5秒間隔で動かすと、fixedRateでは開始が0秒・5秒・10秒と一定ですが、fixedDelayでは0秒・8秒・16秒と後ろにずれていきます。

// 前回の「開始」から5秒後に次を開始する
@Scheduled(fixedRate = 5000)
public void pollStatus() { ... }

// 前回の「完了」から5秒空けて次を開始する
@Scheduled(fixedDelay = 5000)
public void heavyBatch() { ... }

なお、@Scheduledのタスクはデフォルトでは1スレッドで順番に動くため、fixedRateで指定した間隔より処理が長引いても並行して走るわけではありません。次の実行は前回の完了を待ってから即座に始まるので、間隔が実質的に崩れます。重い処理にはfixedDelayを選ぶ方が安全です。

initialDelayとtimeUnit

initialDelayは、最初のメソッド実行までの遅延時間をミリ秒単位で指定します。起動直後はDB接続やキャッシュの準備が終わっていないこともあるので、initialDelay = 10000のように少し待たせると安全です。
また、Spring 5.3以降ならtimeUnit属性でミリ秒以外の単位も指定できます。

@Scheduled(fixedDelay = 30, initialDelay = 10, timeUnit = TimeUnit.SECONDS)
public void syncCache() { ... }

zoneでタイムゾーンを指定する

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

@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 の使い分けとcron式の書き方

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

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

Springのcron式は6フィールド

@Scheduledのcron式はLinuxのcrontabと違い、先頭に「秒」が付いた6フィールドです。左から「秒 分 時 日 月 曜日」の順で、crontabの5フィールドをそのまま貼り付けると起動時にエラーになるので注意しましょう。

よく使う書き方は次のとおりです。

やりたいことcron式
毎分0秒0 * * * * *
10分ごと0 */10 * * * *
毎日3:000 0 3 * * *
平日の9:000 0 9 * * MON-FRI
毎月末の23:300 30 23 L * *
毎月第2月曜の10:000 0 10 * * MON#2

L(月末・最終曜日)、W(最寄りの平日)、#(第n曜日)といったQuartz風の記号はSpring 5.3以降で使えます。
また、@hourly・@daily・@weekly・@monthly・@yearlyといったマクロも指定でき、@Scheduled(cron = "@daily")と書けば毎日0:00に実行されます。

cron式はプロパティに外出しして、環境ごとに切り替えるのが実務では定番です。開発環境だけ短い間隔で回したい、といった要望にもコードを変えずに対応できます。

@Scheduled(cron = "${app.report.cron:0 0 3 * * *}", zone = "Asia/Tokyo")
public void generateDailyReport() { ... }

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

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

@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を調整し、シャットダウン時に中断されないよう設定しておくと運用事故を減らせます。
setWaitForTasksToCompleteOnShutdownが効くのは、アプリ側のシャットダウン処理が正しく走ることが前提です。Kubernetesでのデプロイ時に実行中のタスクを取りこぼさない設定については Spring Bootのグレースフルシャットダウンとゼロダウンタイムデプロイを実現する方法 で解説しています。

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

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

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

「実行ロジック」と「いつ実行するか」を分離しておくと、テストも保守も簡単になります。
さらに踏み込むなら、スケジューラ側は「対象の検出とイベント発行」だけに留め、後続処理はApplicationEventのリスナーに任せる設計も有効です。業務処理がイベント単位に分かれるので、テストも書きやすくなります。イベント駆動の実装とテスト方法については Spring BootのApplicationEventでモジュール間を疎結合にする方法 で解説しています。

まとめ

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