---
title: "Spring Retryの使い方：Maven依存・@Retryable・@RecoverとSpring Boot 4での扱い【2.0.13実測】"
url: "https://www.issoh.co.jp/tech/details/4056/"
published: 2024-11-04
updated: 2026-11-03
categories: ["Spring"]
publisher: "株式会社一創"
---

# 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](https://github.com/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の変更点](https://docs.spring.io/spring-batch/reference/whatsnew.html)は、新しいチャンク処理のリトライが「Spring Framework 7で導入されたリトライ機能に基づく」と書き、[Spring Integration 7.0の変更点](https://docs.spring.io/spring-integration/reference/7.0/whats-new.html)は「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](https://docs.spring.io/spring-framework/reference/core/resilience.html)（`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のサポート期間](https://spring.io/projects/spring-boot#support)）。

### 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の移行ガイド](https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-4.0-Migration-Guide)に「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年最新】](/tech/details/10124/)にまとめています。

### 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](https://github.com/spring-attic/spring-retry/blob/v2.0.13/src/main/java/org/springframework/retry/annotation/Retryable.java)も `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](https://github.com/spring-attic/spring-retry/blob/v2.0.13/src/main/java/org/springframework/retry/annotation/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での使い方・実行順序と自己呼び出し](/tech/details/3422/)で解説しています。

| 症状                       | 原因              | 直し方              |
| ------------------------ | --------------- | ---------------- |
| 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](https://docs.spring.io/spring-framework/docs/7.0.10/javadoc-api/org/springframework/resilience/annotation/Retryable.html)で、`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の選び方

- **Boot 3.5のまま運用を続ける**：spring-retryで問題ありません。ただしBoot 3.5自体のOSSサポートが終わっているので、Boot 4への移行計画とセットで考えます。
- **Boot 4へ上げる・新規開発**：Spring Framework 7の `@Retryable` を使います。依存が増えず、Spring BatchとSpring Integrationも同じ仕組みへ移りました。フレームワーク本体については[Spring Frameworkとは？Spring Bootとの違い・Tomcat対応と最新版7.0](/tech/details/3867/)を参照してください。
- **サーキットブレーカーやフォールバックメソッドが要る**：Resilience4j（2.4.0）を選びます。spring-retryにはサーキットブレーカー機能がありますが、OSS保守は終了しています。Spring Framework 7本体のリトライ機能にはサーキットブレーカーは含まれません。考え方は[サーキットブレーカーとは？マイクロサービスの障害連鎖を止める仕組みと実装・導入判断を解説](/tech/details/13216/)、設定値の決め方は[circuit breakerの実装：Java・C#・Go・Node.jsの設定コードと閾値の決め方](/tech/details/17948/)にあります。

どれを選んでも、再試行してよいのは同じ要求を繰り返しても結果が変わらない処理だけです。決済や登録のPOSTのように二重実行が問題になる処理は、冪等キーを用意してから再試行を付けます。外部APIとの連携でタイムアウトや再試行の設計から相談したい場合は、[API開発・システム連携](https://www.issoh.co.jp/service/system/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` に置き換えます。

## 関連記事

- [Spring Frameworkとは？Spring Bootとの違い・Tomcat対応と最新版7.0](/tech/details/3867/)
- [Spring Boot 4とは？最新バージョン4.1の変更点・新機能とSpring Boot 3との違いを解説【2026年最新】](/tech/details/10124/)
- [AOP（アスペクト指向プログラミング）とは？Spring Bootでの使い方・実行順序と自己呼び出し](/tech/details/3422/)
- [サーキットブレーカーとは？マイクロサービスの障害連鎖を止める仕組みと実装・導入判断を解説](/tech/details/13216/)
- [circuit breakerの実装：Java・C#・Go・Node.jsの設定コードと閾値の決め方](/tech/details/17948/)

---

出典: [Spring Retryの使い方：Maven依存・@Retryable・@RecoverとSpring Boot 4での扱い【2.0.13実測】](<https://www.issoh.co.jp/tech/details/4056/>)（株式会社一創）
