このバージョンはまだ開発中であり、まだ安定しているとは見なされていません。最新の安定バージョンについては、Spring Framework 7.0.10 を使用してください!

レジリエンス機能

7.0 の時点で、コア Spring Framework には共通の回復力機能、特にメソッド呼び出し用の @Retryable および @ConcurrencyLimit アノテーションとプログラムによる再試行のサポートが含まれています。

@Retryable

@Retryable (Javadoc) は、個々のメソッド (メソッドレベルで宣言されたアノテーションを使用)、または特定のクラス階層内のすべてのプロキシ呼び出しメソッド (型レベルで宣言されたアノテーションを使用) の再試行特性を指定するアノテーションです。

@Retryable
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

デフォルトでは、例外が発生した場合、メソッド呼び出しは再試行されます。最初の失敗後、最大 3 回(maxRetries = 3)の再試行が行われ、再試行の間隔は 1 秒です。すべての再試行が失敗し、再試行ポリシーが尽きた場合、対象メソッドからの最後の元の例外が呼び出し元に伝播されます。

@Retryable メソッドは少なくとも 1 回呼び出され、最大 maxRetries 回再試行されます。ここで、maxRetries は再試行の最大回数です。具体的には、total attempts = 1 initial attempt + maxRetries attempts です。

例: maxRetries が 4 に設定されている場合、@Retryable メソッドは少なくとも 1 回、最大 5 回呼び出されます。

必要に応じて、各メソッドごとにカスタマイズできます。たとえば、includes 属性と excludes 属性を使用して、再試行する例外を絞り込むことができます。指定された例外型は、失敗した呼び出しによってスローされた例外だけでなく、ネストされた原因とも照合されます。

@Retryable(MessageDeliveryException.class)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}
@Retryable(MessageDeliveryException.class) は @Retryable(includes = MessageDeliveryException.class) のショートカットです。

高度な使用例では、@Retryable の predicate 属性を介してカスタム MethodRetryPredicate を指定できます。述語は、Method と指定された Throwable に基づいて、たとえば Throwable のメッセージをチェックすることによって、失敗したメソッド呼び出しを再試行するかどうかを決定するために使用されます。

カスタム述語は includes および excludes と組み合わせることができますが、カスタム述語は常に includes および excludes が適用された後に適用されます。

または、4 回の再試行と、若干のジッターを伴う指数バックオフ戦略の場合:

@Retryable(
        includes = MessageDeliveryException.class,
        maxRetries = 4,
        delay = 100,
        jitter = 10,
        multiplier = 2,
        maxDelay = 1000)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

delay が 0 と正の jitter を組み合わせたものである場合、設定された multiplier に関係なく遅延は増加しないため、設定された jitter 全体が 0 から min(jitter, maxDelay) の範囲のランダム遅延として直接適用されます。

最後になりましたが、@Retryable はリアクティブ戻り型を持つリアクティブメソッドでも機能し、パイプラインを Reactor の再試行機能で装飾します。

@Retryable(maxRetries = 4, delay = 100)
public Mono<Void> sendNotification() {
    return Mono.from(...); (1)
}
1 この生の Mono は再試行仕様で装飾されます。

さまざまな特性の詳細については、@Retryable (Javadoc) で使用可能なアノテーション属性を参照してください。

@Retryable のいくつかの属性には、上記の例で使用されている特別に型指定されたアノテーション属性の代替として、プロパティプレースホルダーと SpEL サポートを提供する String バリアントがあります。

@Retryable 処理中、Spring は対象メソッドから発生した例外ごとに MethodRetryEvent を発行します。これにより、すべての元の例外を追跡 / ログに記録できますが、@Retryable メソッドの呼び出し元は最後の例外しか見ることができません。

@Retryable と他のプロキシベースの機能を組み合わせる

Spring AOP は、@Retryable、@Transactional、@Cacheable、@Async などの複数のアノテーションが同じメソッドに存在する場合に、インターセプターを特定の順序で適用します。結果として得られるアドバイスチェーンは、各機能とリトライがどのように相互作用するかを決定し、チェーンを理解することは、@Retryable を他のアノテーションと組み合わせて正しく使用するために重要です。

@Transactional と共に

@Transactional と @Retryable を併用する場合、アドバイスチェーンは次のようになります。

Retry (OUTER) → Transaction (INNER) → target method

再試行するたびに、新しいトランザクションが開始されます。ターゲットメソッドが例外をスローした場合、トランザクションはロールバックされ、@Retryable が再試行するかどうかを決定します。成功した場合、トランザクションはコミットされます。これは通常、データベースのデッドロックなどの一時的な障害に対して望ましい動作です。

@Transactional
@Retryable(TransientDataAccessException.class)
public void updateRecord() {
    // Each retry runs in its own transaction
}

リトライインターセプターはトランザクションインターセプターの外側にあるため、リトライインターセプターが例外を受け取る時点では、現在のトランザクションはすでにロールバックされています。リトライインターセプターは、ターゲットメソッドがスローした例外をそのまま、ラップされていない状態で受け取ります。

宣言型トランザクション管理に関する一般的な詳細については、@Transactional を使用するを参照してください。

@Cacheable と共に

@Cacheable と @Retryable を併用する場合、アドバイスチェーンは次のようになります。

Retry (OUTER) → Cache (INNER) → target method

キャッシュインターセプターは、試行ごとに実行されます。試行の合間にキャッシュが読み込まれた場合(たとえば、同時リクエストによって)、後続の再試行では、対象メソッドを呼び出すことなくキャッシュされた値が返されます。成功した場合は、通常どおりキャッシュが読み込まれます。

@CacheEvict と @CachePut は同じ基盤となるキャッシュアドバイザーを共有しているため、同じ固定順序が適用されます。

@Cacheable("items")
@Retryable
public Item loadItem(String id) {
    // Retry wraps the cache lookup; each attempt checks the cache first
}
宣言型キャッシングに関する一般的な詳細については、@Cacheable アノテーションを参照してください。

@Async と共に

@Async と @Retryable を併用する場合、アドバイスチェーンは次のようになります。

Async (OUTER) → Retry (INNER) → target method

このメソッドは非同期エグゼキュータに一度だけ送信され、すべての再試行は同じ非同期スレッドで実行されます。呼び出し元は、最後の再試行が完了したとき(結果または最終例外のいずれかで)、完了した CompletableFuture または Future を受け取ります。

@Async
@Retryable
public CompletableFuture<String> fetchData() {
    // Retries happen on the async thread, not the calling thread
}

@Async は最上位スレッドであるため、呼び出し元のスレッドは再試行遅延によってブロックされることはありません。再試行の試行はすべて、設定された遅延時間も含め、非同期実行スレッド上で実行されます。

非同期メソッド実行に関する一般的な詳細については、@Async アノテーションを参照してください。

アドバイスオーダーの調整

上記で説明した @Async の順序は、RetryAnnotationBeanPostProcessor (@EnableResilientMethods によって登録) と AsyncAnnotationBeanPostProcessor (@EnableAsync によって登録) の相対的な order を反映しています。どちらも通常の Ordered Bean ポストプロセッサーであるため、@EnableResilientMethods および / または @EnableAsync の order 属性を設定することで、それらの相対的な順序を変更できます。

@Configuration
@EnableResilientMethods(order = Ordered.LOWEST_PRECEDENCE) (1)
@EnableAsync(order = Ordered.LOWEST_PRECEDENCE - 1) (2)
class AppConfig {
}
1 再試行後処理の実行順序を上げて、非同期後処理の後に実行されるようにします。
2 非同期ポストプロセッサーの実行順序を下げ、再試行ポストプロセッサーよりも先に実行されるようにします。結果として、再試行が最も外側のアドバイスとなり、非同期が最も内側のアドバイスとなり、デフォルトの順序が逆転します。

With the reversed order shown above, exceptions thrown during asynchronous execution are not retried: the retry interceptor only sees the Future handle, which is returned immediately, rather than the outcome of the asynchronous invocation. Such an arrangement only retries synchronous submission failures (for example, a rejected task submission) and is rarely desirable in practice.

This technique does not apply to @Transactional or @Cacheable. Their advisors are registered through Spring’s shared InfrastructureAdvisorAutoProxyCreator, whose own post-processor order is fixed at Ordered.HIGHEST_PRECEDENCE and is unaffected by the order attribute on @EnableTransactionManagement or @EnableCaching (that attribute only affects ordering relative to other advisors on the same proxy). As a result, retry advice is always applied outside @Transactional and @Cacheable, regardless of the order configured on @EnableResilientMethods.

@ConcurrencyLimit

@ConcurrencyLimit (Javadoc) は、個々のメソッド (メソッドレベルで宣言されたアノテーションを使用)、または特定のクラス階層内のすべてのプロキシ呼び出しメソッド (型レベルで宣言されたアノテーションを使用) の同時実行制限を指定するアノテーションです。

@ConcurrencyLimit(10)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

これは、制限に達した場合にアクセスをブロックするスレッドプールまたは接続プールのプールサイズ制限の効果と同様に、同時に多数のスレッドからターゲットリソースへのアクセスが防止されることを目的としています。

オプションで制限を 1 に設定して、ターゲットの Bean インスタンスへのアクセスを効果的にロックすることもできます。

@ConcurrencyLimit(1)
public void sendNotification() {
    this.jmsClient.destination("notifications").send(...);
}

このような制限は、一般的にスレッドプールの制限がない仮想スレッドで特に有用です。非同期タスクの場合、これは SimpleAsyncTaskExecutor (Javadoc) で制約できます。同期呼び出しの場合、このアノテーションは ConcurrencyThrottleInterceptor (Javadoc) を通じて同等の動作を提供します。ConcurrencyThrottleInterceptor (Javadoc) は、Spring Framework 1.0 以降、AOP フレームワークを用いたプログラム的な使用のために提供されています。

@ConcurrencyLimit には、上記の int ベースの例の代替として、プロパティプレースホルダーと SpEL サポートを提供する limitString 属性もあります。

回復力のある方法の実現

Spring のコアとなるアノテーションベースの機能の多くと同様に、@Retryable と @ConcurrencyLimit はメタデータとして設計されており、適用するか無視するかを選択できます。レジリエンスアノテーションの処理を有効にする最も便利な方法は、対応する @Configuration クラスで @EnableResilientMethods (Javadoc) を宣言することです。

あるいは、コンテキストで RetryAnnotationBeanPostProcessor または ConcurrencyLimitBeanPostProcessor Bean を定義することで、これらのアノテーションを個別に有効にすることもできます。

プログラムによる再試行のサポート

ApplicationContext に登録された Bean 内のメソッドの再試行セマンティクスを指定するための宣言的なアプローチを提供する @Retryable とは対照的に、RetryTemplate (Javadoc) は任意のコードブロックを再試行するためのプログラム API を提供します。

具体的には、RetryTemplate は構成された RetryPolicy (Javadoc) に基づいて Retryable (Javadoc) 操作を実行し、場合によっては再試行します。

var retryTemplate = new RetryTemplate(); (1)

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));
1 暗黙的に RetryPolicy.withDefaults() を使用します。

デフォルトでは、再試行可能な操作は、スローされたすべての例外に対して再試行されます。最初の失敗後、最大 3 回の再試行 (maxRetries = 3) が行われ、試行間の遅延は 1 秒になります。

再試行可能な操作は、少なくとも 1 回実行され、最大 maxRetries 回再試行されます。ここで、maxRetries は再試行の最大回数です。具体的には、total attempts = 1 initial attempt + maxRetries attempts です。

例: maxRetries が 4 に設定されている場合、再試行可能な操作は少なくとも 1 回、最大 5 回呼び出されます。

再試行回数のみをカスタマイズする必要がある場合は、以下に示すように RetryPolicy.withMaxRetries() ファクトリメソッドを使用できます。

var retryTemplate = new RetryTemplate(RetryPolicy.withMaxRetries(4)); (1)

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));
1RetryPolicy.withMaxRetries(4) を明示的に使用します。

再試行する例外の種類を絞り込む必要がある場合は、includes() および excludes() ビルダーメソッドを使用できます。指定された例外の種類は、失敗した操作によってスローされた例外や、ネストされた原因と照合されます。

var retryPolicy = RetryPolicy.builder()
        .includes(MessageDeliveryException.class) (1)
        .excludes(...) (2)
        .build();

var retryTemplate = new RetryTemplate(retryPolicy);

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));
1 含める例外の種類を 1 つ以上指定します。
2 除外する例外型を 1 つ以上指定します。

高度な使用例では、RetryPolicy.Builder の predicate() メソッドを介してカスタム Predicate<Throwable> を指定できます。また、述語は、たとえば Throwable のメッセージをチェックすることによって、特定の Throwable に基づいて失敗した操作を再試行するかどうかを決定するために使用されます。

カスタム述語は includes および excludes と組み合わせることができますが、カスタム述語は常に includes および excludes が適用された後に適用されます。

次の例は、4 回の再試行と、少しのジッターを伴う指数バックオフ戦略を使用して RetryPolicy を構成する方法を示しています。

var retryPolicy = RetryPolicy.builder()
        .includes(MessageDeliveryException.class)
        .maxRetries(4)
        .delay(Duration.ofMillis(100))
        .jitter(Duration.ofMillis(10))
        .multiplier(2)
        .maxDelay(Duration.ofSeconds(1))
        .build();

var retryTemplate = new RetryTemplate(retryPolicy);

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));

delay がゼロで、jitter が正の場合、設定された multiplier に関係なく遅延は増加しないため、設定された jitter 全体がゼロから min(jitter, maxDelay) の範囲のランダム遅延として直接適用されます。

RetryPolicy のファクトリメソッドとビルダー API は、一般的な構成シナリオのほとんどをカバーしていますが、カスタム RetryPolicy を実装することで、再試行をトリガーする例外の種類と使用する BackOff (Javadoc) 戦略を完全に制御できます。また、RetryPolicy.Builder の backOff() メソッドを介して、カスタマイズされた BackOff 戦略を構成することもできます。

上記の例では、@Retryable メソッド呼び出しと同様のパターンが適用され、最後の元の例外が呼び出し元に伝播されます。これは、戻り値の有無にかかわらず利用可能な RetryTemplate の invoke バリアントを使用して行われます。コールバックはチェックされない例外をスローする可能性があり、その最後の例外は呼び出し元側で直接処理できるように公開されます。

try {
    retryTemplate.invoke(
            () -> jmsClient.destination("notifications").send(...));
}
catch (MessageDeliveryException ex) {
    // coming out of the original JmsClient send method
}
try {
    var result = retryTemplate.invoke(() -> {
        jmsClient.destination("notifications").send(...);
        return "result";
    });
}
catch (MessageDeliveryException ex) {
    // coming out of the original JmsClient send method
}

RetryTemplate インスタンスは非常に軽量で、必要に応じてオンザフライで作成でき、特定の呼び出しに対して使用する再試行ポリシーを指定することも可能です。

try {
    new RetryTemplate(RetryPolicy.withMaxRetries(4)).invoke(
            () -> jmsClient.destination("notifications").send(...));
}
catch (MessageDeliveryException ex) {
    // coming out of the original JmsClient send method
}

より詳細なやり取りを行うには、RetryTemplate の execute メソッドを使用できます。呼び出し元は、RetryTemplate によってスローされるチェック例外 RetryException を処理し、すべての試行の結果を公開する必要があります。

try {
    var result = retryTemplate.execute(() -> {
        jmsClient.destination("notifications").send(...);
        return "result";
    });
}
catch (RetryException ex) {
    // ex.getExceptions() / ex.getLastException() ...
}

RetryListener (Javadoc) は RetryTemplate に登録することで、重要な再試行ステップ (再試行の前後など) や、単にすべての呼び出し試行に対応し、コールバックから発生するすべての例外とすべての再試行結果 (枯渇、中断、タイムアウト) を追跡できます。これは、invoke を使用する場合に特に便利です。invoke では、最後の元の例外以外の再試行状態は公開されないためです。

var retryTemplate = new RetryTemplate();
retryTemplate.setRetryListener(new RetryListener() {
    @Override
    public void onRetryableExecution(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState) {
        ...
    }
});

retryTemplate.invoke(
        () -> jmsClient.destination("notifications").send(...));

CompositeRetryListener (Javadoc) を介して複数のリスナーを構成することもできます。