mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Introduce MethodRetryEvent for @Retryable execution
Closes gh-35382
This commit is contained in:
+11
-4
@@ -30,6 +30,8 @@ import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvis
|
||||
import org.springframework.aop.support.ComposablePointcut;
|
||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.context.EmbeddedValueResolverAware;
|
||||
import org.springframework.core.MethodClassKey;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
@@ -52,7 +54,9 @@ import org.springframework.util.StringValueResolver;
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
|
||||
implements EmbeddedValueResolverAware {
|
||||
implements ApplicationEventPublisherAware, EmbeddedValueResolverAware {
|
||||
|
||||
private final RetryAnnotationInterceptor interceptor = new RetryAnnotationInterceptor();
|
||||
|
||||
private @Nullable StringValueResolver embeddedValueResolver;
|
||||
|
||||
@@ -62,9 +66,7 @@ public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAd
|
||||
|
||||
Pointcut cpc = new AnnotationMatchingPointcut(Retryable.class, true);
|
||||
Pointcut mpc = new AnnotationMatchingPointcut(null, Retryable.class, true);
|
||||
this.advisor = new DefaultPointcutAdvisor(
|
||||
new ComposablePointcut(cpc).union(mpc),
|
||||
new RetryAnnotationInterceptor());
|
||||
this.advisor = new DefaultPointcutAdvisor(new ComposablePointcut(cpc).union(mpc), this.interceptor);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +75,11 @@ public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAd
|
||||
this.embeddedValueResolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
this.interceptor.setApplicationEventPublisher(applicationEventPublisher);
|
||||
}
|
||||
|
||||
|
||||
private class RetryAnnotationInterceptor extends AbstractRetryInterceptor {
|
||||
|
||||
|
||||
+4
-1
@@ -34,7 +34,10 @@ import org.springframework.resilience.retry.MethodRetryPredicate;
|
||||
*
|
||||
* <p>Aligned with {@link org.springframework.core.retry.RetryTemplate}
|
||||
* as well as Reactor's retry support, either re-invoking an imperative
|
||||
* target method or decorating a reactive result accordingly.
|
||||
* target method or decorating a returned reactive publisher accordingly.
|
||||
*
|
||||
* <p>For tracking the exceptions encountered by method-level retry processing,
|
||||
* consider a {@link org.springframework.resilience.retry.MethodRetryEvent} listener.
|
||||
*
|
||||
* <p>Inspired by the <a href="https://github.com/spring-projects/spring-retry">Spring Retry</a>
|
||||
* project but redesigned as a minimal core retry feature in the Spring Framework.
|
||||
|
||||
+52
-13
@@ -31,10 +31,14 @@ import reactor.core.publisher.Mono;
|
||||
import reactor.util.retry.Retry;
|
||||
|
||||
import org.springframework.aop.ProxyMethodInvocation;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.context.ApplicationEventPublisherAware;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.retry.RetryException;
|
||||
import org.springframework.core.retry.RetryListener;
|
||||
import org.springframework.core.retry.RetryPolicy;
|
||||
import org.springframework.core.retry.RetryState;
|
||||
import org.springframework.core.retry.RetryTemplate;
|
||||
import org.springframework.core.retry.Retryable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -50,7 +54,7 @@ import org.springframework.util.ClassUtils;
|
||||
* @see Mono#retryWhen
|
||||
* @see Flux#retryWhen
|
||||
*/
|
||||
public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
public abstract class AbstractRetryInterceptor implements MethodInterceptor, ApplicationEventPublisherAware {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AbstractRetryInterceptor.class);
|
||||
|
||||
@@ -62,6 +66,8 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
|
||||
private final @Nullable ReactiveAdapterRegistry reactiveAdapterRegistry;
|
||||
|
||||
private @Nullable ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
|
||||
public AbstractRetryInterceptor() {
|
||||
if (REACTIVE_STREAMS_PRESENT) {
|
||||
@@ -72,6 +78,11 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
|
||||
this.applicationEventPublisher = applicationEventPublisher;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
@@ -90,7 +101,7 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
if (result == null) {
|
||||
return null;
|
||||
}
|
||||
return ReactorDelegate.adaptReactiveResult(result, adapter, spec, method);
|
||||
return new ReactorDelegate().adaptReactiveResult(invocation, result, adapter, spec);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +116,17 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
.multiplier(spec.multiplier())
|
||||
.maxDelay(spec.maxDelay())
|
||||
.build();
|
||||
|
||||
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
|
||||
retryTemplate.setRetryListener(new RetryListener() {
|
||||
@Override
|
||||
public void onRetryableExecution(RetryPolicy retryPolicy, Retryable<?> retryable, RetryState retryState) {
|
||||
if (!retryState.isSuccessful()) {
|
||||
onEvent(new MethodRetryEvent(invocation, retryState.getLastException(), false));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
String methodName = ClassUtils.getQualifiedMethodName(method, (target != null ? target.getClass() : null));
|
||||
|
||||
try {
|
||||
@@ -122,13 +143,21 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
});
|
||||
}
|
||||
catch (RetryException ex) {
|
||||
onEvent(new MethodRetryEvent(invocation, ex, true));
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("@Retryable operation '%s' failed".formatted(methodName), ex);
|
||||
logger.debug("Retryable operation '%s' failed".formatted(methodName), ex);
|
||||
}
|
||||
throw ex.getCause();
|
||||
}
|
||||
}
|
||||
|
||||
private void onEvent(MethodRetryEvent event) {
|
||||
logger.trace(event, event.getFailure());
|
||||
if (this.applicationEventPublisher != null) {
|
||||
this.applicationEventPublisher.publishEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the retry specification for the given method on the given target.
|
||||
* @param method the currently executing method
|
||||
@@ -141,29 +170,39 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
/**
|
||||
* Inner class to avoid a hard dependency on Reactive Streams and Reactor at runtime.
|
||||
*/
|
||||
private static class ReactorDelegate {
|
||||
private class ReactorDelegate {
|
||||
|
||||
public static Object adaptReactiveResult(
|
||||
Object result, ReactiveAdapter adapter, MethodRetrySpec spec, Method method) {
|
||||
public Object adaptReactiveResult(
|
||||
MethodInvocation invocation, Object result, ReactiveAdapter adapter, MethodRetrySpec spec) {
|
||||
|
||||
Publisher<?> publisher = adapter.toPublisher(result);
|
||||
Retry retry = Retry.backoff(spec.maxRetries(), spec.delay())
|
||||
.jitter(calculateJitterFactor(spec))
|
||||
.multiplier(spec.multiplier())
|
||||
.maxBackoff(spec.maxDelay())
|
||||
.filter(spec.combinedPredicate().forMethod(method));
|
||||
.filter(spec.combinedPredicate().forMethod(invocation.getMethod()));
|
||||
|
||||
Duration timeout = spec.timeout();
|
||||
boolean timeoutIsPositive = (!timeout.isNegative() && !timeout.isZero());
|
||||
if (adapter.isMultiValue()) {
|
||||
publisher = (timeoutIsPositive ?
|
||||
Flux.from(publisher).retryWhen(retry).timeout(timeout) :
|
||||
Flux.from(publisher).retryWhen(retry));
|
||||
Flux<?> flux = Flux.from(publisher)
|
||||
.doOnError(ex -> onEvent(new MethodRetryEvent(invocation, ex, false)))
|
||||
.retryWhen(retry);
|
||||
if (timeoutIsPositive) {
|
||||
flux = flux.timeout(timeout);
|
||||
}
|
||||
flux = flux.doOnError(ex -> onEvent(new MethodRetryEvent(invocation, ex, true)));
|
||||
publisher = flux;
|
||||
}
|
||||
else {
|
||||
publisher = (timeoutIsPositive ?
|
||||
Mono.from(publisher).retryWhen(retry).timeout(timeout) :
|
||||
Mono.from(publisher).retryWhen(retry));
|
||||
Mono<?> mono = Mono.from(publisher)
|
||||
.doOnError(ex -> onEvent(new MethodRetryEvent(invocation, ex, false)))
|
||||
.retryWhen(retry);
|
||||
if (timeoutIsPositive) {
|
||||
mono = mono.timeout(timeout);
|
||||
}
|
||||
mono = mono.doOnError(ex -> onEvent(new MethodRetryEvent(invocation, ex, true)));
|
||||
publisher = mono;
|
||||
}
|
||||
|
||||
return adapter.fromPublisher(publisher);
|
||||
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.resilience.retry;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Event published for every exception encountered during retryable method invocation.
|
||||
* Can be listened to via an {@code ApplicationListener<MethodRetryEvent>} bean or an
|
||||
* {@code @EventListener(MethodRetryEvent.class)} method.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0.3
|
||||
* @see AbstractRetryInterceptor
|
||||
* @see org.springframework.resilience.annotation.Retryable
|
||||
* @see org.springframework.context.ApplicationListener
|
||||
* @see org.springframework.context.event.EventListener
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MethodRetryEvent extends ApplicationEvent {
|
||||
|
||||
private final Throwable failure;
|
||||
|
||||
private final boolean retryAborted;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new event for the given retryable method invocation.
|
||||
* @param invocation the retryable method invocation
|
||||
* @param failure the exception encountered
|
||||
* @param retryAborted whether the current failure led to the retry execution getting aborted
|
||||
*/
|
||||
public MethodRetryEvent(MethodInvocation invocation, Throwable failure, boolean retryAborted) {
|
||||
super(invocation);
|
||||
this.failure = failure;
|
||||
this.retryAborted = retryAborted;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the method invocation that triggered this event.
|
||||
*/
|
||||
@Override
|
||||
public MethodInvocation getSource() {
|
||||
return (MethodInvocation) super.getSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the method that triggered this event.
|
||||
*/
|
||||
public Method getMethod() {
|
||||
return getSource().getMethod();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the exception encountered.
|
||||
* <p>This may be an exception thrown by the method or emitted by the reactive
|
||||
* publisher returned from the method, or a terminal exception on retry
|
||||
* exhaustion, interruption or timeout.
|
||||
* <p>For {@link org.springframework.core.retry.RetryTemplate} executions,
|
||||
* an {@code instanceof RetryException} check identifies a final exception.
|
||||
* For Reactor pipelines, {@code Exceptions.isRetryExhausted} identifies an
|
||||
* exhaustion exception, whereas {@code instanceof TimeoutException} reveals
|
||||
* a timeout scenario.
|
||||
* @see #isRetryAborted()
|
||||
* @see org.springframework.core.retry.RetryException
|
||||
* @see reactor.core.Exceptions#isRetryExhausted
|
||||
* @see java.util.concurrent.TimeoutException
|
||||
*/
|
||||
public Throwable getFailure() {
|
||||
return this.failure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the current failure led to the retry execution getting aborted,
|
||||
* typically indicating exhaustion, interruption or a timeout scenario.
|
||||
* <p>If this returns {@code true}, {@link #getFailure()} exposes the final exception
|
||||
* thrown by the retry infrastructure (rather than thrown by the method itself).
|
||||
* @see #getFailure()
|
||||
*/
|
||||
public boolean isRetryAborted() {
|
||||
return this.retryAborted;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "MethodRetryEvent: " + ClassUtils.getQualifiedMethodName(getMethod()) + " [" + getFailure() + "]";
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user