Spring

Spring Retryの使い方:Maven依存・@Retryable・@RecoverとSpring Boot 4での扱い【2.0.13実測】

Spring Retryの使い方:Maven依存・@Retryable・@RecoverとSpring Boot 4での扱い【2.0.13実測】

Spring Retry(org.springframework.retry:spring-retry)は、メソッドに @Retryable を付けるだけで失敗時の再実行を足せるライブラリです。ただし2.0.13を最後に開発が止まり、2026年7月27日にリポジトリがアーカイブされました。Spring Boot 4ではpom.xmlにバージョンを書かないとビルドが止まります。

この記事では、Spring Boot 3.5.16と4.1.2(Spring Framework 7.0.10)、JDK 25で実際に動かした結果をもとに、依存の書き方、@Retryable と @Recover の挙動、Spring Framework 7の @Retryable へ移行するときの差分を説明します。

まとめ

  • spring-retryの最終版は2.0.13(2026-06-08)。READMEは「no longer maintained as an open-source project」と宣言し、Spring Framework 7への移行を案内しています。
  • Spring Boot 3.5.16のBOMを使い、管理バージョンを上書きしていなければ、バージョン省略時に2.0.13が解決されます。Spring Boot 4ではBOMから外れたため、<version>2.0.13</version> の明記が必須です。
  • @EnableRetry を使うならAOP依存も要ります(Boot 3は spring-boot-starter-aop、Boot 4は spring-boot-starter-aspectj)。無いと起動時に NoClassDefFoundError で落ちます。
  • maxAttempts = 3 は初回を含めて3回呼びます。Spring Framework 7の maxRetries = 3 は4回呼ぶので、移行時は1減らします。
  • 新規開発とBoot 4への移行では、Spring Framework 7の @Retryable と @EnableResilientMethods を使うのが筋です。spring-retry 2.0.13はBoot 4上でも動きましたが、今後バグ修正は出ません。

Spring Retryの現状:2.0.13で開発終了・リポジトリはアーカイブ済み

GitHubのリポジトリは spring-projects から spring-attic/spring-retry へ移り、アーカイブ(読み取り専用)になりました。READMEは2026年7月27日の更新で「Spring Retry is no longer maintained as an open-source project」「This project has been superseded by Spring Framework 7 and is now archived」と書き換えられています。最後のリリースは2.0.13(GitHub Releasesで2026-06-08公開)で、Maven Centralのmaven-metadata.xmlでも2.0.13が最新です。

Spring自身のプロジェクトも離れています。Spring Batch 6.0の変更点は、新しいチャンク処理のリトライが「Spring Framework 7で導入されたリトライ機能に基づく」と書き、Spring Integration 7.0の変更点は「The spring-retry dependency, together with all its API usage have been replaced by retry API in Spring Framework Core module」と明記しています。後継はSpring Framework 7.0の Resilience Features(org.springframework.resilience と org.springframework.core.retry)です。

既存のBoot 3系アプリで動いているspring-retryを今すぐ外す必要はありません。判断の分かれ目は、Spring Bootを4へ上げるかどうかです。

spring-retryのMaven・Gradle依存設定

Spring Boot 3.5.16:BOMによるspring-retry 2.0.13の依存管理

Spring Boot 3.5.16を親にしたpom.xmlでは、spring-retryのバージョンを書かずに済みます。@EnableRetry はAspectJのアノテーションクラスを使うので、spring-boot-starter-aop も一緒に入れます。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>3.5.16</version>
</parent>

<dependencies>
  <dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
  </dependency>
</dependencies>

mvn dependency:list で解決結果を見ると、spring-retry:jar:2.0.13 と aspectjweaver:jar:1.9.25.1 が入っていました。なお、Spring Boot 3.5系のOSS版リリースは3.5.16(2026-06-25)が最後で、OSSサポート期間はすでに終わっています(Spring Bootのサポート期間)。

Spring Boot 4.x:バージョン省略はビルド停止

Spring Boot 4.0以降のBOMにはspring-retryの管理エントリがありません。4.1.2を親にしてバージョンを省くと、mvn validate の段階で次のエラーになりました。

[ERROR] 'dependencies.dependency.version' for org.springframework.retry:spring-retry:jar is missing. @ line 9, column 17

Boot 4でも使い続けるなら、バージョンを明記し、AOPスターターは改称後の名前で書きます。Spring Boot 4.0の移行ガイドに「spring-boot-starter-aop … has been renamed to spring-boot-starter-aspectj」とあり、Maven Central上の spring-boot-starter-aop も4.0.0-M2で止まっています。

<dependency>
  <groupId>org.springframework.retry</groupId>
  <artifactId>spring-retry</artifactId>
  <version>2.0.13</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aspectj</artifactId>
</dependency>

この構成で、後述のBoot 3.5と同じテストコードを動かしたところ、Spring Framework 7.0.10上でも試行回数・@Recover・例外の種類が6パターンすべて同じ結果になりました。動くことは確認できましたが、spring-retryが7.0系を前提にテストされているわけではなく、今後の修正も出ません。Boot 4の新機能と一緒に使うのは、移行までのつなぎと考えてください。Boot 4の変更点の全体はSpring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説【2026年最新】にまとめています。

AOP依存が無いときの起動エラー

spring-retryだけを入れてAOPスターターを忘れると、アプリケーションが起動しません。Boot 4.1.2で spring-boot-starter-aspectj を外したときのログは次のとおりです。

Error creating bean with name 'org.springframework.aop.config.internalAutoProxyCreator': org/aspectj/lang/annotation/Pointcut
Caused by: java.lang.NoClassDefFoundError: org/aspectj/lang/annotation/Pointcut

このエラーにspring-retryの名前は出てきません。@EnableRetry を足した直後に Pointcut が見つからないと言われたら、AOPスターターの不足を疑います。

Gradleでの書き方

次のGradle(Kotlin DSL)例は、Spring Boot 4.1.2のBOMを適用済みであることが前提です。BOM管理外のspring-retryにはバージョンを明記し、BOMが管理するAspectJスターターは省略します。

dependencies {
    implementation("org.springframework.retry:spring-retry:2.0.13")
    implementation("org.springframework.boot:spring-boot-starter-aspectj")
}

@EnableRetryによる有効化と@Retryableの試行回数

@EnableRetry は、Spring Bootのメインクラスか任意の @Configuration クラスに1つ付ければアプリケーション全体で有効になります。次のコードは、IOException のときだけ100ミリ秒おきに再実行する例です。

import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@SpringBootApplication
@EnableRetry
public class App {
  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }
}

@Service
class Remote {
  static final AtomicInteger calls = new AtomicInteger();

  @Retryable(retryFor = IOException.class, maxAttempts = 3, backoff = @Backoff(delay = 100))
  public String fetch() throws IOException {
    calls.incrementAndGet();
    throw new IOException("timeout");
  }
}

Boot 3.5.16(Spring Framework 6.2.19)で fetch() を呼ぶと、結果は calls=3 で、最後に元の java.io.IOException: timeout がそのまま呼び出し元へ投げられました。Retryable.javaのJavadocも maxAttempts を「the maximum number of attempts (including the first failure), defaults to 3」と説明しています。リトライ回数ではなく総試行回数です。

なお、カウンタをインスタンスフィールドにすると、テストで getBean() したプロキシから読んだときに NullPointerException になりました。SpringのCGLIBプロキシと委譲先のBeanは別インスタンスであり、通常はObjenesisがプロキシのコンストラクターを呼ばずに生成するためです。Beanの状態は、プロキシ経由で呼べるgetterで確認します(この記事のコードは回数を数えるだけのテスト用なので、カウンタをstaticで持っています)。

Spring Retryの@Retryable・@Backoffの既定値

属性を何も書かない @Retryable は、すべての例外を対象に3回試行し、待機は1000ミリ秒の固定です(Backoff.javaの「With no explicit settings the default is a fixed delay of 1000ms」)。IllegalStateException を投げるメソッドに付けたところ、3回呼ばれました。multiplier と maxDelay の既定は0で、明示しない限り指数バックオフにはなりません。

属性 既定値 指定例
maxAttempts 3(初回を含む) maxAttempts = 5
retryFor 空(全例外) retryFor = IOException.class
noRetryFor 空 noRetryFor = FileNotFoundException.class
@Backoff delay 1000ms delay = 500
@Backoff multiplier 0(使わない) multiplier = 2.0
@Backoff maxDelay 0(使わない) maxDelay = 10000
@Backoff random false random = true

2.0から value・include・exclude は @Deprecated になり、retryFor・noRetryFor に置き換わりました。ネットの古い記事にある @Retryable(value = ...) はコンパイルできますが警告が出ます。

retryFor・noRetryForによる対象例外の指定

retryFor = IOException.class のメソッドが IllegalArgumentException を投げると、1回だけ呼ばれて元の例外が返りました(calls=1)。入力値の誤りや認証エラーのように、何度呼んでも結果が変わらない例外は retryFor に入れないのが原則です。

noRetryFor は、親クラスを retryFor に入れたうえで、その子の一部だけを外すときに効きます。たとえば retryFor = IOException.class, noRetryFor = FileNotFoundException.class です。retryFor に具体的な型を並べている状態で無関係な型を noRetryFor に足しても、未列挙の例外は最初から対象外なので何も変わりません。

@Recoverによる試行終了後の回復処理

試行回数を使い切ったあとに既定値を返したいときは、同じクラスに @Recover メソッドを置きます。戻り値の型を @Retryable 側と揃え、第1引数に例外、続けて元メソッドの引数を並べます。recover 属性でメソッド名を指定すると、候補が複数あっても取り違えません。

@Service
class Recovering {
  @Retryable(retryFor = IOException.class, maxAttempts = 3,
      backoff = @Backoff(delay = 100), recover = "fallback")
  public String fetchWithRecover() throws IOException {
    throw new IOException("timeout");
  }

  @Recover
  public String fallback(IOException e) {
    return "fallback(" + e.getMessage() + ")";
  }
}

実行結果は fallback(timeout) で、本体は3回呼ばれていました。

Cannot locate recovery methodが出る条件

戻り値の型が合わない @Recover を置くと、元の例外ではなく別の例外になります。String を返す @Retryable に対し、Integer を返す @Recover だけを置いた結果がこれです。

org.springframework.retry.ExhaustedRetryException: Cannot locate recovery method
cause=java.io.IOException: timeout

クラスに @Recover が1つでもあると、回復処理の登録自体は行われ、型の合う候補が見つからなければこの例外が投げられます。ただし、notRecoverable に指定した例外は、@Recover があっても回復処理を経由せずに送出されます。stateful = true の場合も、試行途中の例外が呼び出し元へ再送出されます。呼び出し側で IOException をcatchしていたコードは、@Recover を足した途端にすり抜けるので注意してください。直し方は、回復メソッドの戻り値、例外型、元メソッドに対応する引数を適合させることです。候補が複数ある場合は、適合するメソッド名を recover 属性に指定します。

RetryTemplateによる処理単位の再試行

メソッド全体ではなく、処理の一部だけを再試行したいときは RetryTemplate を使います。プロキシを経由しないので、同じクラス内からでも効きます。

RetryTemplate template = RetryTemplate.builder()
    .maxAttempts(3)
    .fixedBackoff(100)
    .retryOn(IOException.class)
    .build();

String result = template.execute(context -> {
  if (context.getRetryCount() < 2) {
    throw new IOException("timeout");
  }
  return "ok after " + context.getRetryCount() + " retries";
});

Boot 3.5.16で実行すると ok after 2 retries が返り、ラムダは3回呼ばれました。context.getRetryCount() は初回が0で、再試行のたびに1ずつ増えます。

@Retryableが効かないときの確認順

最初に確認するのは、同じクラスの別メソッドから呼んでいないかです。Remote に fetch() を呼ぶだけの outer() を足して外から outer() を呼ぶと、calls=1 で再試行されませんでした。Springのプロキシは外から呼ばれたときにしか割り込めないためで、仕組みはAOP(アスペクト指向プログラミング)とは?Spring Bootでの使い方・実行順序と自己呼び出しで解説しています。

症状 原因 直し方
1回で例外が返る 同じクラス内から呼んでいる 別Beanに分ける
1回で例外が返る newで作ったインスタンス DIで受け取る
1回で例外が返る 例外がretryFor外 retryForを見直す
1回で例外が返る メソッド内でcatchしている 例外を外へ投げる
起動時にNoClassDefFoundError AOP依存なし aop/aspectjスターター
ExhaustedRetryException @Recoverの型不一致 戻り値と引数を揃える

privateメソッドは対象外です。finalメソッドはCGLIBによるクラスベースのプロキシでは対象外ですが、JDK動的プロキシ経由のインターフェースメソッド呼び出しには、この制約は当てはまりません。Spring Framework 7の設定に切り替えた後に起きる「効かない」は、次の章で扱います。

Spring Framework 7の@Retryableへの移行差分

Spring Framework 7.0には、spring-retryとほぼ同じ名前の org.springframework.resilience.annotation.Retryable が入りました。名前は似ていても属性と回数の数え方が違うので、置換だけでは済みません。

Spring RetryとSpring Framework 7の設定属性の対応

spring-retry 2.0.13 Spring Framework 7.0 注意点
@EnableRetry @EnableResilientMethods AOPスターター不要
maxAttempts = 3 maxRetries = 2 初回を含まない
retryFor includes 既定は全例外
noRetryFor excludes 同上
@Backoff(delay) delay 既定1000ms
@Backoff(multiplier) multiplier 既定1.0
@Backoff(random) jitter 単純置換不可・揺らぎ幅を再設計
@Recover なし 呼び出し側でcatch
(なし) timeout 7.0.2で追加

既定値は7.0.10のRetryable Javadocで、maxRetries が3、delay が1000、jitter が0です。@Recover に当たる仕組みは無く、回数を使い切ると元の例外がそのまま投げられます。@Recover を多用しているコードほど、移行は書き換えが増えます。

maxRetries = 3の実測結果:初回を含む4回の試行

Boot 4.1.2で、AOPスターターを入れずに spring-boot-starter だけで次のコードを動かしました。依存一覧にaspectjweaverは含まれていませんでしたが、再試行は動きました。

import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.resilience.annotation.EnableResilientMethods;
import org.springframework.resilience.annotation.Retryable;
import org.springframework.stereotype.Service;

@SpringBootApplication
@EnableResilientMethods
public class App {
  public static void main(String[] args) {
    SpringApplication.run(App.class, args);
  }
}

@Service
class Remote {
  static final AtomicInteger calls = new AtomicInteger();

  @Retryable(includes = IOException.class, maxRetries = 3, delay = 100)
  public String fetch() throws IOException {
    calls.incrementAndGet();
    throw new IOException("timeout");
  }
}
条件 呼び出し回数 最後の例外
maxRetries = 3 4 IOException
属性なし(既定) 4 IllegalStateException
includes外の例外 1 IllegalArgumentException
同じクラス内から呼ぶ 1 IOException
@EnableResilientMethodsなし 1 IOException
spring-retryの@Retryableを付けた 1 IOException

注意したいのは最後の2行です。@EnableResilientMethods を付け忘れても、@EnableRetry を外してspring-retryのアノテーションが残っていても、エラーも警告も出ずに1回で終わります。移行後はimport文が org.springframework.resilience.annotation.Retryable になっているかをgrepで確かめ、再試行の回数をテストで固定しておくのが確実です。

Spring Framework 7のRetryTemplateと例外処理

命令的に書く場合は org.springframework.core.retry.RetryTemplate に RetryPolicy を渡します。

RetryTemplate template = new RetryTemplate(RetryPolicy.builder()
    .maxRetries(2)
    .delay(Duration.ofMillis(50))
    .includes(IOException.class)
    .build());

try {
  template.execute(() -> callRemote());
} catch (RetryException e) {
  // e.getCause() が最後の例外、e.getSuppressed() がそれ以前の例外
}

常に失敗する処理で試すと、呼び出しは3回、投げられたのは RetryException(causeが最後の IOException、suppressedが2件)でした。RetryException はチェック例外なので、spring-retryの RetryTemplate から置き換えるとcatch節の書き直しが必要です。

spring-retry・Spring Framework 7・Resilience4jの選び方

どれを選んでも、再試行してよいのは同じ要求を繰り返しても結果が変わらない処理だけです。決済や登録のPOSTのように二重実行が問題になる処理は、冪等キーを用意してから再試行を付けます。外部APIとの連携でタイムアウトや再試行の設計から相談したい場合は、API開発・システム連携の支援もご覧ください。

よくある質問

pom.xmlにspring-retryのバージョンを書く必要はありますか?

Spring Boot 3.5.16のBOMを適用している場合、spring-retryは2.0.13として管理されるため、個別のバージョン指定は不要です。Spring Boot 4.0以降はBOMから外れたため、<version>2.0.13</version> を書かないと 'dependencies.dependency.version' ... is missing でビルドが止まります。

Spring Boot 4でspring-retryは使えますか?

Boot 4.1.2(Spring Framework 7.0.10)で、バージョン明記と spring-boot-starter-aspectj の追加により動作しました。ただし2.0.13で開発が終わり、リポジトリもアーカイブ済みです。Boot 4へ上げるならSpring Framework 7の @Retryable へ移すことを勧めます。

maxAttempts = 3を指定すると何回リトライしますか?

初回を含めて3回呼ぶので、再試行は2回です。Spring Framework 7の maxRetries = 3 は初回を含まないため、4回呼ばれます。

@Recoverが呼ばれず Cannot locate recovery method が出るのはなぜですか?

@Recover メソッドの戻り値の型か引数の並びが @Retryable 側と合っていないためです。クラスに @Recover が1つでもあると、合う候補が無いときに ExhaustedRetryException が投げられます。recover 属性で名前を指定する場合も、指定先の戻り値・例外型・引数が適合している必要があります。

@EnableRetryはどのクラスに付ければよいですか?

@SpringBootApplication のメインクラスか、任意の @Configuration クラスに1つ付ければ全Beanに効きます。@Retryable を付けたクラスごとに付ける必要はありません。Spring Framework 7へ移行した後は @EnableResilientMethods に置き換えます。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

RELATED POSTS 関連記事

目次