Spring Retryの依存関係と@Retryable/@Recover実装|Spring Framework 7移行の判断基準
Spring Retryは、一時的な失敗で終わった処理を宣言的に再実行するためのライブラリです。ネットワークの瞬断やデータベースの接続断のように、時間をおけば成功する可能性がある失敗に対して、呼び出し側のコードを触らずに再試行を挟めます。ただし2026年時点でこのプロジェクトはメンテナンスのみの状態に入り、Spring Framework 7が後継の役割を引き継ぎました。この記事では依存関係の書き方から各アノテーションの実装、そして移行を判断するための材料までを一次情報にもとづいて整理します。
まとめ
Spring Retryの公式リポジトリは2026年時点でプロジェクト状態を「Maintenance Only」と明示し、Spring Framework 7に置き換えられた旨と新機能の受け付けを終了した旨を掲げています。最新版は2.0.13(2026年6月8日リリース、UTC基準)で、バグ修正は続いていますが機能追加は入りません。
実装で最も効くのは依存関係の書き方です。Spring Boot 3系はBOMがspring-retryのバージョンを管理するため座標だけで解決しますが、Spring Boot 4系のBOMは管理対象から外したため、使い続けるならバージョンの明記が要ります。宣言的な@RetryableがAOPのランタイム依存を別途必要とする点も、動かないときの定番の原因です。
そしてSpring Framework 7へ移行するなら、回数を指定する属性の意味が変わることを押さえてください。spring-retryのmaxAttemptsは初回を含む総試行回数ですが、Spring Framework 7のmaxRetriesは初回を含まない再試行回数です。数値をそのまま移すと呼び出し回数が1回増えます。
以下では、依存関係の記述、3つのアノテーションの実装規約、リトライが動かないときの切り分け、そして移行時の差分の順に見ていきます。
Spring Retryの現在地:メンテナンスのみでSpring Framework 7が後継
公式リポジトリのREADME冒頭には、このプロジェクトがSpring Framework 7に置き換えられ、機能追加をもう受け付けないことが警告として置かれています。移行先としてSpring Frameworkのレジリエンス機能が案内されており、事実上の役割終了が宣言された状態です。
この方針転換は依存関係の管理にも表れています。Spring Boot 3.5.16のBOMにはspring-retryのバージョン管理エントリ(2.0.13)が残っていますが、Spring Boot 4.0.0以降のBOMからは該当エントリが消えました。Spring Boot 4.0.0は2025年11月20日にGA、同梱されるSpring Frameworkは7.0.1です。Spring Framework 7.0.0のGAはその1週間前の2025年11月13日でした。
では今すぐ捨てるべきかというと、そうではありません。新規にSpring Boot 4系で組むなら、外部依存を1つ減らせるSpring Framework 7の組み込み機能を選ぶのが妥当です。一方、既存のSpring Boot 3系アプリでspring-retryが安定して動いているなら、急いで書き換える理由は薄いといえます。@Recoverによる回復処理の仕組みはSpring Framework 7側に用意がなく、移行すると自前で例外を捕まえ直す必要が出るためです。判断の分かれ目はSpring Boot 4へ上げるかどうかであり、リトライ実装単体で移行を決めるものではありません。Spring Boot 4での変更点全体はSpring Boot 4とは?最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説【2026年最新】で確認できます。
依存関係の追加:MavenとGradleでの座標とバージョン指定の要否
成果物の座標はorg.springframework.retry:spring-retryです。バージョンを書くべきかどうかは、使っているSpring Bootのバージョンによって変わります。
Spring Boot 3.xはBOM管理でバージョン記述が不要
Spring Boot 3系のspring-boot-dependenciesは、spring-retryのバージョンをプロパティとして管理しています。3.5.16では2.0.13が指定されているため、pom.xmlには座標だけを書けば解決します。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
Gradleでも同様に、Spring Bootのプラグインが依存管理を効かせている前提でバージョンを省略できます。
implementation 'org.springframework.retry:spring-retry'
Spring Boot 4.xと非Bootはバージョンの明記が必須
Spring Boot 4.0.0、4.0.7、4.1.0のいずれのBOMにもspring-retryの管理エントリはありません。バージョンを省略したままSpring Boot 4へ上げると、Mavenはモデル検証の段階で次のように停止します。
'dependencies.dependency.version' for org.springframework.retry:spring-retry:jar is missing
使い続けるならバージョンを明示してください。
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>2.0.13</version>
</dependency>
Spring Bootを使わないプレーンなSpringアプリケーションでも同じくバージョンの明記が要ります。なお2.0系はpom.xmlでJava 17をリリースターゲットに指定してビルドされているため、Java 16以下のJVMに載せると起動時にUnsupportedClassVersionErrorで落ちます。旧JVMに残る制約があるなら1.3系という選択肢はありますが、1.3.xブランチの最終コミットは2023年10月19日で更新が止まっており、脆弱性修正も期待できません。新規採用は避け、まずJVM側の更新を検討してください。
@Retryableに追加で要るAOP依存
宣言的な書き方はプロキシ経由で動くため、spring-retry本体だけではAOPクラスが足りません。READMEの「Additional Dependencies」節では、Spring BootではAOPスターターをランタイム依存として追加する方法が案内されています。
runtimeOnly 'org.springframework.boot:spring-boot-starter-aop'
Spring Bootを使わない場合はAspectJのweaverモジュールを直接ランタイム依存に加えます。この依存が抜けているとアノテーションを付けても何も起こらないため、動かないときに最初に疑う箇所です。
@EnableRetryによる有効化と適用範囲
アノテーションを解釈させるには、設定クラスに@EnableRetryを1つ付けます。付ける場所は@Configurationが付いたクラスで、Spring Bootならメインクラスに置く形が最も手数が少なくて済みます。付与はアプリケーションに1箇所で足り、リトライ対象のクラスごとに書く必要はありません。
@SpringBootApplication
@EnableRetry
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
この設定はリトライ用のプロキシを組み立てるスイッチであり、有効化しないと@Retryableは単なる注釈として無視されます。合わせてリスナーを登録すれば、リトライの発生や終了のタイミングでログを差し込めます。
@Retryableの属性設計:retryForとmaxAttempts、バックオフ
@Retryableは、付けたメソッドの呼び出しに再試行を挟むアノテーションです。属性を何も指定しなければ、あらゆる例外を対象に初回を含めて3回まで呼び出し、間隔は1秒になります。実務では対象例外と回数を絞り込むところから設計します。
例外の絞り込みはretryFor/noRetryForで書く
対象例外の指定に使う属性名は2.0で変わりました。従来のvalue、include、excludeはソース上で非推奨の印が付き、2.0以降はretryForとnoRetryForが正式な指定方法です。ネット上の記事は2.0より前に書かれたものが多く、includeのまま紹介している例が今も見つかりますが、新規に書くなら新しい属性名を使ってください。
@Retryable(
retryFor = RestClientException.class,
noRetryFor = HttpClientErrorException.class)
public String fetchInventory(String sku) {
return restClient.get().uri("/inventory/{sku}", sku)
.retrieve().body(String.class);
}
この書き方は、REST呼び出しの例外を広く拾いつつ4xx系だけを外す指定です。HttpClientErrorExceptionはリクエスト側に原因がある失敗なので、何度呼び直しても結果は変わりません。例外の分類は継承関係の近いものが優先されるため、親クラスをretryForで拾い、その一部の子クラスをnoRetryForで外す組み合わせが実効的に働きます。逆にretryForへ具体的な例外型を並べただけの場合、そこに挙がっていない例外は初めから対象外なので、noRetryForを足しても指定は効きません。
回数と待ち時間の指定:maxAttemptsとbackoff
maxAttemptsのjavadocは「初回の失敗を含む最大試行回数、既定は3」と定義しています。つまり3を指定すれば呼び出しは合計3回で、再試行は2回です。この「初回を含む」という数え方は、後述するSpring Framework 7と食い違う点なので覚えておいてください。
待ち時間は@Backoffで調整します。属性の既定値はdelayが1000ミリ秒、maxDelayとmultiplierが0、randomがfalseです。valueはdelayのエイリアスなので、@Backoff(2000)と書けば2秒の固定間隔になります。multiplierに1より大きい値を入れると指数的に間隔が伸び、maxDelayが上限として効きます。
@Retryable(
retryFor = SocketTimeoutException.class,
maxAttempts = 5,
backoff = @Backoff(delay = 200, multiplier = 3, maxDelay = 2000))
public String fetchInventory(String sku) { ... }
この設定なら呼び出しの間隔は200ミリ秒、600ミリ秒、1800ミリ秒と伸び、4回目は5400ミリ秒になるところがmaxDelayで2000ミリ秒に抑えられます。randomをtrueにするとExponentialRandomBackOffPolicyへ切り替わり、各回の待ち時間が算出値から算出値×multiplierまでの一様乱数になります。同時に失敗した複数のクライアントが同じ瞬間に再試行して相手を押し潰す事態を避けたいときに使ってください。呼び出し先が既に落ちている場合はリトライだけでは復旧を妨げるため、サーキットブレーカーとは?マイクロサービスの障害連鎖を止める仕組みと実装・導入判断を解説で扱う遮断の仕組みと組み合わせて設計します。
@Recoverの実装規約:戻り値と引数のマッチング
回数を使い切ってもなお失敗したときの受け皿が@Recoverです。javadocはマッチング規約を次のように定めています。第1引数はThrowableかそのサブタイプ、戻り値の型はリカバリ対象の@Retryableメソッドと同じ型(実装上はそのスーパータイプでも通ります)。Throwableの第1引数は省略できますが、省略したメソッドは他に一致するものが無いときだけ呼ばれます。2番目以降の引数は、失敗したメソッドの引数リストから順番に埋められます。
@Retryable(retryFor = SocketTimeoutException.class, maxAttempts = 3)
public String fetchInventory(String sku) { ... }
@Recover
public String recoverInventory(SocketTimeoutException e, String sku) {
log.warn("在庫照会に失敗したためキャッシュ値を返します sku={}", sku, e);
return inventoryCache.get(sku);
}
この規約から外れたときの挙動は、意外と知られていません。クラスに@Recoverが1つでも存在すればリカバリのハンドラは登録されるため、どれにも一致しなかった場合は無言で素通りするのではなく、ExhaustedRetryExceptionが「Cannot locate recovery method」というメッセージで投げられます。元の例外はそのcauseに包まれます。元例外がそのまま呼び出し元へ抜けるのは、クラスに@Recoverが1つも無いときだけです。この例外に出くわしたら、戻り値の型が違う、第1引数の例外型が実際に投げられる例外と一致しない、引数の順番がずれている、のいずれかを疑ってください。
同じクラスに複数の@Recoverを置いた場合は、宣言順ではなく、実際に投げられた例外に継承距離が最も近い例外型を宣言したものが選ばれます。距離が同じなら引数の多い方が優先されます。どれが呼ばれるか曖昧になるのを避けたいときは、@Retryableのrecover属性にメソッド名を書いて名指しで指定できます。
リトライが動かないときの原因切り分け
アノテーションを付けたのに再試行されないという症状は、ほぼ次の順で原因が見つかります。
- 同一クラス内からの自己呼び出しになっている。Springのプロキシを通らないためリトライの割り込み自体が起きない。呼び出す側を別のBeanへ切り出す。
- AOPのランタイム依存が入っていない。Spring Bootならspring-boot-starter-aop、それ以外はaspectjweaverを追加する。
@EnableRetryを書いていない、あるいはコンポーネントスキャンの対象外の設定クラスに書いている。- 対象メソッドがprivateまたはfinal、もしくはクラスがfinalでプロキシを作れない。
retryForで指定した例外型と、実際に投げられる例外の型が一致していない。ラップされた例外を投げていないか確認する。
頻度で言えば1つ目の自己呼び出しが突出しています。アノテーションを付けたのに何も起きないという相談は、まずここを確認すれば大半が片付きます。
Spring Framework 7への移行:属性名と回数の数え方の違い
Spring Framework 7の@Retryableはorg.springframework.resilience.annotationパッケージにあり、spring-retryのものとは別物です。有効化は@EnableResilientMethodsで行います。命令的に書きたい場合のRetryTemplateとRetryPolicyはorg.springframework.core.retryに置かれました。
移行で最も危険なのは回数の指定です。Spring Framework 7のリファレンスは「total attempts = 1 initial attempt + maxRetries attempts」と明記しています。maxRetriesは初回を含まない再試行回数であり、既定の3は合計4回の呼び出しを意味します。spring-retryのmaxAttempts = 3をそのままmaxRetries = 3に置き換えると、呼び出し先への負荷が3回分から4回分に増えます。数値は1つ減らして移すのが正しい対応です。
| 項目 | spring-retry 2.0.13 | Spring Framework 7.0 |
|---|---|---|
| アノテーションのパッケージ | org.springframework.retry.annotation | org.springframework.resilience.annotation |
| 有効化 | @EnableRetry | @EnableResilientMethods |
| 回数の属性 | maxAttempts(初回を含む) | maxRetries(初回を含まない) |
| 既定値と合計呼び出し回数 | 3 → 合計3回 | 3 → 合計4回 |
| 対象例外の指定 | retryFor / noRetryFor | includes / excludes |
| 待ち時間の既定 | delay 1000ms / multiplier 0 | delay 1000ms / multiplier 1.0 |
| 待ち時間の上限 | maxDelay 既定0 | maxDelay 既定 Long.MAX_VALUE(無制限) |
| ゆらぎ | random(真偽値) | jitter(ミリ秒) |
| 回復メソッド | @Recover | 相当機能なし |
| 命令的API | org.springframework.retry.support.RetryTemplate | org.springframework.core.retry.RetryTemplate |
| 依存の追加 | 必要(Boot 4ではversion明記) | 不要(spring-core / spring-context同梱) |
属性名の対応も一段階ずれます。例外の絞り込みは複数形のincludesとexcludesになり、ゆらぎは真偽値のrandomではなくミリ秒を渡すjitterになりました。上限を決めるmaxDelayの既定値も、spring-retryの0からLong.MAX_VALUE(無制限)へ変わっています。より細かい条件で判定したい場合はpredicate属性にMethodRetryPredicateの実装クラスを指定できます。移行後のコードは次のような形です。
@Retryable(
includes = SocketTimeoutException.class,
maxRetries = 3,
delay = 200,
jitter = 50,
multiplier = 2,
maxDelay = 3000)
public String fetchInventory(String sku) { ... }
移行にあたって見落としやすいのが回復処理です。Spring Framework 7のレジリエンス関連パッケージには@Recoverに相当するクラスが用意されていません。フォールバック値を返す設計にしている場合は、呼び出し側でtry-catchを書き直す作業が発生します。@Recoverを多用しているコードほど移行コストが上がるため、Spring Boot 4へ上げるタイミングで棚卸ししておくと安全です。なお同じパッケージには同時実行数を制限する@ConcurrencyLimitも入っており、仮想スレッド環境で呼び出し先を保護する用途に使えます。リトライと組み合わせた耐障害設計の全体像はフォールトトレラントとは?耐障害性の仕組みと高可用性・フェイルオーバーとの違いを実装目線で解説が参考になります。
よくある質問
pom.xmlにspring-retryのバージョンを書く必要はありますか?
Spring Boot 3系なら不要です。BOMがバージョンを管理しており、3.5.16では2.0.13が指定されています。Spring Boot 4系ではBOMの管理対象から外れたため、バージョンの明記が必要です。Spring Bootを使わない場合も明記してください。
spring-retryの最新バージョンはいくつですか?
2.0.13です。2026年6月8日(UTC基準)にリリースされました。ただしプロジェクトはメンテナンスのみの状態で、以降は機能追加が行われません。
@EnableRetryはどのクラスに付ければよいですか?
@Configurationが付いた設定クラスに1箇所付ければアプリケーション全体で有効になります。Spring Bootでは@SpringBootApplicationを付けたメインクラスに置く形が一般的です。リトライ対象のクラスごとに付ける必要はありません。
maxAttempts = 3を指定すると何回リトライしますか?
呼び出しは合計3回、再試行は2回です。javadocが「初回の失敗を含む最大試行回数」と定義しているためです。Spring Framework 7のmaxRetriesは初回を含まない数え方なので、移行時は数値を1つ減らします。
@Recoverが呼ばれず Cannot locate recovery method が出るのはなぜですか?
マッチング規約から外れているためです。戻り値の型が@Retryableメソッドと一致しているか、第1引数の例外型が実際に投げられる例外と合っているか、2番目以降の引数が元メソッドの引数と同じ順序かを確認してください。クラスに@Recoverがあるのにどれも一致しない場合、ExhaustedRetryExceptionがこのメッセージで投げられ、元の例外はcauseに包まれます。