Browse Source
Based on RetryTemplate with ExponentialBackOff. Includes optional jitter support in ExponentialBackOff. Supports reactive methods through Reactor's RetryBackoffSpec. Closes gh-34529pull/35086/head
12 changed files with 1058 additions and 30 deletions
@ -0,0 +1,161 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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.aop.retry; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.time.Duration; |
||||||
|
|
||||||
|
import org.aopalliance.intercept.MethodInterceptor; |
||||||
|
import org.aopalliance.intercept.MethodInvocation; |
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
import org.reactivestreams.Publisher; |
||||||
|
import reactor.core.publisher.Flux; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
import reactor.util.retry.Retry; |
||||||
|
|
||||||
|
import org.springframework.core.ReactiveAdapter; |
||||||
|
import org.springframework.core.ReactiveAdapterRegistry; |
||||||
|
import org.springframework.core.retry.RetryException; |
||||||
|
import org.springframework.core.retry.RetryPolicy; |
||||||
|
import org.springframework.core.retry.RetryTemplate; |
||||||
|
import org.springframework.core.retry.Retryable; |
||||||
|
import org.springframework.util.ClassUtils; |
||||||
|
import org.springframework.util.backoff.ExponentialBackOff; |
||||||
|
|
||||||
|
/** |
||||||
|
* Abstract retry interceptor implementation, adapting a given |
||||||
|
* retry specification to either {@link RetryTemplate} or Reactor. |
||||||
|
* |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @since 7.0 |
||||||
|
* @see #getRetrySpec |
||||||
|
* @see RetryTemplate |
||||||
|
* @see Mono#retryWhen |
||||||
|
* @see Flux#retryWhen |
||||||
|
*/ |
||||||
|
public abstract class AbstractRetryInterceptor implements MethodInterceptor { |
||||||
|
|
||||||
|
/** |
||||||
|
* Reactive Streams API present on the classpath? |
||||||
|
*/ |
||||||
|
private static final boolean reactiveStreamsPresent = ClassUtils.isPresent( |
||||||
|
"org.reactivestreams.Publisher", AbstractRetryInterceptor.class.getClassLoader()); |
||||||
|
|
||||||
|
private final @Nullable ReactiveAdapterRegistry reactiveAdapterRegistry; |
||||||
|
|
||||||
|
|
||||||
|
public AbstractRetryInterceptor() { |
||||||
|
if (reactiveStreamsPresent) { |
||||||
|
this.reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); |
||||||
|
} |
||||||
|
else { |
||||||
|
this.reactiveAdapterRegistry = null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { |
||||||
|
Method method = invocation.getMethod(); |
||||||
|
Object target = invocation.getThis(); |
||||||
|
MethodRetrySpec spec = getRetrySpec(method, (target != null ? target.getClass() : method.getDeclaringClass())); |
||||||
|
|
||||||
|
if (spec == null) { |
||||||
|
return invocation.proceed(); |
||||||
|
} |
||||||
|
|
||||||
|
if (this.reactiveAdapterRegistry != null) { |
||||||
|
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType()); |
||||||
|
if (adapter != null) { |
||||||
|
Object result = invocation.proceed(); |
||||||
|
if (result == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return ReactorDelegate.adaptReactiveResult(result, adapter, spec, method); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
RetryTemplate retryTemplate = new RetryTemplate(); |
||||||
|
|
||||||
|
RetryPolicy.Builder policyBuilder = RetryPolicy.builder(); |
||||||
|
for (Class<? extends Throwable> include : spec.includes()) { |
||||||
|
policyBuilder.includes(include); |
||||||
|
} |
||||||
|
for (Class<? extends Throwable> exclude : spec.excludes()) { |
||||||
|
policyBuilder.excludes(exclude); |
||||||
|
} |
||||||
|
policyBuilder.predicate(spec.predicate().forMethod(method)); |
||||||
|
policyBuilder.maxAttempts(spec.maxAttempts()); |
||||||
|
retryTemplate.setRetryPolicy(policyBuilder.build()); |
||||||
|
|
||||||
|
ExponentialBackOff backOff = new ExponentialBackOff(); |
||||||
|
backOff.setInitialInterval(spec.delay()); |
||||||
|
backOff.setJitter(spec.jitterDelay()); |
||||||
|
backOff.setMultiplier(spec.delayMultiplier()); |
||||||
|
backOff.setMaxInterval(spec.maxDelay()); |
||||||
|
backOff.setMaxAttempts(spec.maxAttempts()); |
||||||
|
retryTemplate.setBackOffPolicy(backOff); |
||||||
|
|
||||||
|
try { |
||||||
|
return retryTemplate.execute(new Retryable<>() { |
||||||
|
@Override |
||||||
|
public @Nullable Object execute() throws Throwable { |
||||||
|
return invocation.proceed(); |
||||||
|
} |
||||||
|
@Override |
||||||
|
public String getName() { |
||||||
|
Object target = invocation.getThis(); |
||||||
|
return ClassUtils.getQualifiedMethodName(method, (target != null ? target.getClass() : null)); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
catch (RetryException ex) { |
||||||
|
Throwable cause = ex.getCause(); |
||||||
|
throw (cause != null ? cause : new IllegalStateException(ex.getMessage(), ex)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Determine the retry specification for the given method on the given target. |
||||||
|
* @param method the currently executing method |
||||||
|
* @param targetClass the class of the current target object |
||||||
|
* @return the retry specification as a {@link MethodRetrySpec} |
||||||
|
*/ |
||||||
|
protected abstract @Nullable MethodRetrySpec getRetrySpec(Method method, Class<?> targetClass); |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Inner class to avoid a hard dependency on Reactive Streams and Reactor at runtime. |
||||||
|
*/ |
||||||
|
private static class ReactorDelegate { |
||||||
|
|
||||||
|
public static Object adaptReactiveResult( |
||||||
|
Object result, ReactiveAdapter adapter, MethodRetrySpec spec, Method method) { |
||||||
|
|
||||||
|
Publisher<?> publisher = adapter.toPublisher(result); |
||||||
|
Retry retry = Retry.backoff(spec.maxAttempts(), Duration.ofMillis(spec.delay())) |
||||||
|
.jitter((double) spec.jitterDelay() / spec.delay()) |
||||||
|
.multiplier(spec.delayMultiplier()) |
||||||
|
.maxBackoff(Duration.ofMillis(spec.maxDelay())) |
||||||
|
.filter(spec.combinedPredicate().forMethod(method)); |
||||||
|
publisher = (adapter.isMultiValue() ? Flux.from(publisher).retryWhen(retry) : |
||||||
|
Mono.from(publisher).retryWhen(retry)); |
||||||
|
return adapter.fromPublisher(publisher); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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.aop.retry; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.function.Predicate; |
||||||
|
|
||||||
|
/** |
||||||
|
* Predicate for retrying a {@link Throwable} from a specific {@link Method}. |
||||||
|
* |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @since 7.0 |
||||||
|
* @see MethodRetrySpec#predicate() |
||||||
|
*/ |
||||||
|
@FunctionalInterface |
||||||
|
public interface MethodRetryPredicate { |
||||||
|
|
||||||
|
/** |
||||||
|
* Determine whether the given {@code Method} should be retried after |
||||||
|
* throwing the given {@code Throwable}. |
||||||
|
* @param method the method to potentially retry |
||||||
|
* @param throwable the exception encountered |
||||||
|
*/ |
||||||
|
boolean shouldRetry(Method method, Throwable throwable); |
||||||
|
|
||||||
|
/** |
||||||
|
* Build a {@code Predicate} for testing exceptions from a given method. |
||||||
|
* @param method the method to build a predicate for |
||||||
|
*/ |
||||||
|
default Predicate<Throwable> forMethod(Method method) { |
||||||
|
return (t -> shouldRetry(method, t)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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.aop.retry; |
||||||
|
|
||||||
|
import java.util.Collection; |
||||||
|
import java.util.Collections; |
||||||
|
|
||||||
|
/** |
||||||
|
* A specification for retry attempts on a given method, combining common |
||||||
|
* retry characteristics. This roughly matches the annotation attributes |
||||||
|
* on {@link org.springframework.aop.retry.annotation.Retryable}. |
||||||
|
* |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @since 7.0 |
||||||
|
* @param includes applicable exceptions types to attempt a retry for |
||||||
|
* @param excludes non-applicable exceptions types to avoid a retry for |
||||||
|
* @param predicate a predicate for filtering exceptions from applicable methods |
||||||
|
* @param maxAttempts the maximum number of retry attempts |
||||||
|
* @param delay the base delay after the initial invocation (in milliseconds) |
||||||
|
* @param jitterDelay a jitter delay for the next retry attempt (in milliseconds) |
||||||
|
* @param delayMultiplier a multiplier for a delay for the next retry attempt |
||||||
|
* @param maxDelay the maximum delay for any retry attempt (in milliseconds) |
||||||
|
* @see AbstractRetryInterceptor#getRetrySpec |
||||||
|
* @see SimpleRetryInterceptor#SimpleRetryInterceptor(MethodRetrySpec) |
||||||
|
* @see org.springframework.aop.retry.annotation.Retryable |
||||||
|
*/ |
||||||
|
public record MethodRetrySpec( |
||||||
|
Collection<Class<? extends Throwable>> includes, |
||||||
|
Collection<Class<? extends Throwable>> excludes, |
||||||
|
MethodRetryPredicate predicate, |
||||||
|
int maxAttempts, |
||||||
|
long delay, |
||||||
|
long jitterDelay, |
||||||
|
double delayMultiplier, |
||||||
|
long maxDelay) { |
||||||
|
|
||||||
|
public MethodRetrySpec(MethodRetryPredicate predicate, int maxAttempts, long delay) { |
||||||
|
this(predicate, maxAttempts, delay, 0,1.0, Integer.MAX_VALUE); |
||||||
|
} |
||||||
|
|
||||||
|
public MethodRetrySpec(MethodRetryPredicate predicate, int maxAttempts, long delay, |
||||||
|
long jitterDelay, double delayMultiplier, long maxDelay) { |
||||||
|
|
||||||
|
this(Collections.emptyList(), Collections.emptyList(), predicate, maxAttempts, delay, |
||||||
|
jitterDelay, delayMultiplier, maxDelay); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
MethodRetryPredicate combinedPredicate() { |
||||||
|
return (method, throwable) -> { |
||||||
|
if (!this.excludes.isEmpty()) { |
||||||
|
for (Class<? extends Throwable> exclude : this.excludes) { |
||||||
|
if (exclude.isInstance(throwable)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
if (!this.includes.isEmpty()) { |
||||||
|
boolean included = false; |
||||||
|
for (Class<? extends Throwable> include : this.includes) { |
||||||
|
if (include.isInstance(throwable)) { |
||||||
|
included = true; |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
if (!included) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
||||||
|
return this.predicate.shouldRetry(method, throwable); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,45 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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.aop.retry; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
|
||||||
|
/** |
||||||
|
* A simple concrete retry interceptor based on a given {@link MethodRetrySpec}. |
||||||
|
* |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
public class SimpleRetryInterceptor extends AbstractRetryInterceptor { |
||||||
|
|
||||||
|
private final MethodRetrySpec retrySpec; |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Create a {@code SimpleRetryInterceptor} for the given {@link MethodRetrySpec}. |
||||||
|
* @param retrySpec the specification to use for all method invocations |
||||||
|
*/ |
||||||
|
public SimpleRetryInterceptor(MethodRetrySpec retrySpec) { |
||||||
|
this.retrySpec = retrySpec; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected MethodRetrySpec getRetrySpec(Method method, Class<?> targetClass) { |
||||||
|
return this.retrySpec; |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,46 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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.aop.retry.annotation; |
||||||
|
|
||||||
|
import org.springframework.aop.Pointcut; |
||||||
|
import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor; |
||||||
|
import org.springframework.aop.support.ComposablePointcut; |
||||||
|
import org.springframework.aop.support.DefaultPointcutAdvisor; |
||||||
|
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; |
||||||
|
import org.springframework.beans.factory.config.BeanPostProcessor; |
||||||
|
|
||||||
|
/** |
||||||
|
* A convenient {@link BeanPostProcessor} that applies {@link RetryAnnotationInterceptor} |
||||||
|
* to all bean methods annotated with {@link Retryable} annotations. |
||||||
|
* |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
@SuppressWarnings("serial") |
||||||
|
public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { |
||||||
|
|
||||||
|
public RetryAnnotationBeanPostProcessor() { |
||||||
|
setBeforeExistingAdvisors(true); |
||||||
|
|
||||||
|
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()); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,82 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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.aop.retry.annotation; |
||||||
|
|
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.util.Arrays; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.concurrent.ConcurrentHashMap; |
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable; |
||||||
|
|
||||||
|
import org.springframework.aop.retry.AbstractRetryInterceptor; |
||||||
|
import org.springframework.aop.retry.MethodRetryPredicate; |
||||||
|
import org.springframework.aop.retry.MethodRetrySpec; |
||||||
|
import org.springframework.core.MethodClassKey; |
||||||
|
import org.springframework.core.annotation.AnnotatedElementUtils; |
||||||
|
import org.springframework.util.ReflectionUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* An annotation-based retry interceptor based on {@link Retryable} annotations. |
||||||
|
* |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
public class RetryAnnotationInterceptor extends AbstractRetryInterceptor { |
||||||
|
|
||||||
|
private final Map<MethodClassKey, MethodRetrySpec> retrySpecCache = new ConcurrentHashMap<>(); |
||||||
|
|
||||||
|
|
||||||
|
@Override |
||||||
|
protected @Nullable MethodRetrySpec getRetrySpec(Method method, Class<?> targetClass) { |
||||||
|
MethodClassKey cacheKey = new MethodClassKey(method, targetClass); |
||||||
|
MethodRetrySpec retrySpec = this.retrySpecCache.get(cacheKey); |
||||||
|
if (retrySpec != null) { |
||||||
|
return retrySpec; |
||||||
|
} |
||||||
|
|
||||||
|
Retryable retryable = AnnotatedElementUtils.getMergedAnnotation(method, Retryable.class); |
||||||
|
if (retryable == null) { |
||||||
|
retryable = AnnotatedElementUtils.getMergedAnnotation(targetClass, Retryable.class); |
||||||
|
if (retryable == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
retrySpec = new MethodRetrySpec( |
||||||
|
Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()), |
||||||
|
instantiatePredicate(retryable.predicate()), retryable.maxAttempts(), |
||||||
|
retryable.delay(), retryable.jitterDelay(), |
||||||
|
retryable.delayMultiplier(), retryable.maxDelay()); |
||||||
|
|
||||||
|
MethodRetrySpec existing = this.retrySpecCache.putIfAbsent(cacheKey, retrySpec); |
||||||
|
return (existing != null ? existing : retrySpec); |
||||||
|
} |
||||||
|
|
||||||
|
private MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPredicate> predicateClass) { |
||||||
|
if (predicateClass == MethodRetryPredicate.class) { |
||||||
|
return (method, throwable) -> true; |
||||||
|
} |
||||||
|
try { |
||||||
|
return ReflectionUtils.accessibleConstructor(predicateClass).newInstance(); |
||||||
|
} |
||||||
|
catch (Throwable ex) { |
||||||
|
throw new IllegalStateException("Failed to instantiate predicate class [" + predicateClass + "]", ex); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,138 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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.aop.retry.annotation; |
||||||
|
|
||||||
|
import java.lang.annotation.Documented; |
||||||
|
import java.lang.annotation.ElementType; |
||||||
|
import java.lang.annotation.Retention; |
||||||
|
import java.lang.annotation.RetentionPolicy; |
||||||
|
import java.lang.annotation.Target; |
||||||
|
|
||||||
|
import org.springframework.aop.retry.MethodRetryPredicate; |
||||||
|
import org.springframework.aot.hint.annotation.Reflective; |
||||||
|
import org.springframework.core.annotation.AliasFor; |
||||||
|
|
||||||
|
/** |
||||||
|
* A common annotation specifying retry characteristics for an individual method, |
||||||
|
* or for all proxy-invoked methods in a given class hierarchy if annotated at |
||||||
|
* the type level. |
||||||
|
* |
||||||
|
* <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. |
||||||
|
* |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @since 7.0 |
||||||
|
* @see RetryAnnotationBeanPostProcessor |
||||||
|
* @see RetryAnnotationInterceptor |
||||||
|
* @see org.springframework.core.retry.RetryTemplate |
||||||
|
* @see reactor.core.publisher.Mono#retryWhen |
||||||
|
* @see reactor.core.publisher.Flux#retryWhen |
||||||
|
*/ |
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD}) |
||||||
|
@Retention(RetentionPolicy.RUNTIME) |
||||||
|
@Documented |
||||||
|
@Reflective |
||||||
|
public @interface Retryable { |
||||||
|
|
||||||
|
/** |
||||||
|
* Convenient default attribute for {@link #includes()}, |
||||||
|
* typically used with a single exception type to retry for. |
||||||
|
*/ |
||||||
|
@AliasFor("includes") |
||||||
|
Class<? extends Throwable>[] value() default {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Applicable exceptions types to attempt a retry for. This attribute |
||||||
|
* allows for the convenient specification of assignable exception types. |
||||||
|
* <p>The default is empty, leading to a retry attempt for any exception. |
||||||
|
* @see #excludes() |
||||||
|
* @see #predicate() |
||||||
|
*/ |
||||||
|
@AliasFor("value") |
||||||
|
Class<? extends Throwable>[] includes() default {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* Non-applicable exceptions types to avoid a retry for. This attribute |
||||||
|
* allows for the convenient specification of assignable exception types. |
||||||
|
* <p>The default is empty, leading to a retry attempt for any exception. |
||||||
|
* @see #includes() |
||||||
|
* @see #predicate() |
||||||
|
*/ |
||||||
|
Class<? extends Throwable>[] excludes() default {}; |
||||||
|
|
||||||
|
/** |
||||||
|
* A predicate for filtering applicable exceptions for which |
||||||
|
* an invocation can be retried. |
||||||
|
* <p>The default is a retry attempt for any exception. |
||||||
|
* @see #includes() |
||||||
|
* @see #excludes() |
||||||
|
*/ |
||||||
|
Class<? extends MethodRetryPredicate> predicate() default MethodRetryPredicate.class; |
||||||
|
|
||||||
|
/** |
||||||
|
* The maximum number of retry attempts, in addition to the initial invocation. |
||||||
|
* <p>The default is 3. |
||||||
|
*/ |
||||||
|
int maxAttempts() default 3; |
||||||
|
|
||||||
|
/** |
||||||
|
* The base delay after the initial invocation in milliseconds. |
||||||
|
* If a multiplier is specified, this serves as the initial delay to multiply from. |
||||||
|
* <p>The default is 1000. |
||||||
|
* @see #jitterDelay() |
||||||
|
* @see #delayMultiplier() |
||||||
|
* @see #maxDelay() |
||||||
|
*/ |
||||||
|
long delay() default 1000; |
||||||
|
|
||||||
|
/** |
||||||
|
* A jitter delay for the base retry attempt (in milliseconds), randomly |
||||||
|
* subtracted or added to the calculated delay, resulting in a value |
||||||
|
* between {@code delay - jitterDelay} and {@code delay + jitterDelay} |
||||||
|
* but never below the base {@link #delay()} or above {@link #maxDelay()}. |
||||||
|
* If a multiplier is specified, it applies to the jitter delay as well. |
||||||
|
* <p>The default is 0 (no jitter). |
||||||
|
* @see #delay() |
||||||
|
* @see #delayMultiplier() |
||||||
|
* @see #maxDelay() |
||||||
|
*/ |
||||||
|
long jitterDelay() default 0; |
||||||
|
|
||||||
|
/** |
||||||
|
* A multiplier for a delay for the next retry attempt, applied |
||||||
|
* to the previous delay (starting with {@link #delay()}) as well |
||||||
|
* as to the applicable {@link #jitterDelay()} for each attempt. |
||||||
|
* <p>The default is 1.0, effectively leading to a fixed delay. |
||||||
|
* @see #delay() |
||||||
|
* @see #jitterDelay() |
||||||
|
* @see #maxDelay() |
||||||
|
*/ |
||||||
|
double delayMultiplier() default 1.0; |
||||||
|
|
||||||
|
/** |
||||||
|
* The maximum delay for any retry attempt (in milliseconds), limiting |
||||||
|
* how far {@link #jitterDelay()} and {@link #delayMultiplier()} can |
||||||
|
* increase {@link #delay()}. |
||||||
|
* <p>The default is unlimited. |
||||||
|
* @see #delay() |
||||||
|
* @see #jitterDelay() |
||||||
|
* @see #delayMultiplier() |
||||||
|
*/ |
||||||
|
long maxDelay() default Integer.MAX_VALUE; |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
/** |
||||||
|
* Annotation-based retry support for common Spring setups. |
||||||
|
*/ |
||||||
|
@NullMarked |
||||||
|
package org.springframework.aop.retry.annotation; |
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked; |
||||||
@ -0,0 +1,7 @@ |
|||||||
|
/** |
||||||
|
* A retry interceptor arrangement based on {@code core.retry} and Reactor. |
||||||
|
*/ |
||||||
|
@NullMarked |
||||||
|
package org.springframework.aop.retry; |
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked; |
||||||
@ -0,0 +1,194 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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.aop.retry; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.nio.file.AccessDeniedException; |
||||||
|
import java.util.concurrent.atomic.AtomicInteger; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
import reactor.core.publisher.Flux; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import org.springframework.aop.framework.AopProxyUtils; |
||||||
|
import org.springframework.aop.framework.ProxyFactory; |
||||||
|
import org.springframework.aop.retry.annotation.RetryAnnotationBeanPostProcessor; |
||||||
|
import org.springframework.aop.retry.annotation.RetryAnnotationInterceptor; |
||||||
|
import org.springframework.aop.retry.annotation.Retryable; |
||||||
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory; |
||||||
|
import org.springframework.beans.factory.support.RootBeanDefinition; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIllegalStateException; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatRuntimeException; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
public class ReactiveRetryInterceptorTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
void withSimpleInterceptor() { |
||||||
|
NonAnnotatedBean target = new NonAnnotatedBean(); |
||||||
|
ProxyFactory pf = new ProxyFactory(); |
||||||
|
pf.setTarget(target); |
||||||
|
pf.addAdvice(new SimpleRetryInterceptor(new MethodRetrySpec((m, t) -> true, 5, 10))); |
||||||
|
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy(); |
||||||
|
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block()) |
||||||
|
.withCauseInstanceOf(IOException.class).havingCause().withMessage("6"); |
||||||
|
assertThat(target.counter.get()).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void withAnnotationInterceptorForMethod() { |
||||||
|
AnnotatedMethodBean target = new AnnotatedMethodBean(); |
||||||
|
ProxyFactory pf = new ProxyFactory(); |
||||||
|
pf.setTarget(target); |
||||||
|
pf.addAdvice(new RetryAnnotationInterceptor()); |
||||||
|
AnnotatedMethodBean proxy = (AnnotatedMethodBean) pf.getProxy(); |
||||||
|
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block()) |
||||||
|
.withCauseInstanceOf(IOException.class).havingCause().withMessage("6"); |
||||||
|
assertThat(target.counter.get()).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void withPostProcessorForMethod() { |
||||||
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); |
||||||
|
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class)); |
||||||
|
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor(); |
||||||
|
bpp.setBeanFactory(bf); |
||||||
|
bf.addBeanPostProcessor(bpp); |
||||||
|
AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class); |
||||||
|
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy); |
||||||
|
|
||||||
|
assertThatIllegalStateException().isThrownBy(() -> proxy.retryOperation().block()) |
||||||
|
.withCauseInstanceOf(IOException.class).havingCause().withMessage("6"); |
||||||
|
assertThat(target.counter.get()).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void withAnnotationInterceptorForClass() { |
||||||
|
AnnotatedClassBean target = new AnnotatedClassBean(); |
||||||
|
ProxyFactory pf = new ProxyFactory(); |
||||||
|
pf.setTarget(target); |
||||||
|
pf.addAdvice(new RetryAnnotationInterceptor()); |
||||||
|
AnnotatedClassBean proxy = (AnnotatedClassBean) pf.getProxy(); |
||||||
|
|
||||||
|
assertThatRuntimeException().isThrownBy(() -> proxy.retryOperation().block()) |
||||||
|
.withCauseInstanceOf(IOException.class).havingCause().withMessage("3"); |
||||||
|
assertThat(target.counter.get()).isEqualTo(3); |
||||||
|
assertThatRuntimeException().isThrownBy(() -> proxy.otherOperation().block()) |
||||||
|
.withCauseInstanceOf(IOException.class); |
||||||
|
assertThat(target.counter.get()).isEqualTo(4); |
||||||
|
assertThatIllegalStateException().isThrownBy(() -> proxy.overrideOperation().blockFirst()) |
||||||
|
.withCauseInstanceOf(IOException.class); |
||||||
|
assertThat(target.counter.get()).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void withPostProcessorForClass() { |
||||||
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); |
||||||
|
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class)); |
||||||
|
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor(); |
||||||
|
bpp.setBeanFactory(bf); |
||||||
|
bf.addBeanPostProcessor(bpp); |
||||||
|
AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class); |
||||||
|
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy); |
||||||
|
|
||||||
|
assertThatRuntimeException().isThrownBy(() -> proxy.retryOperation().block()) |
||||||
|
.withCauseInstanceOf(IOException.class).havingCause().withMessage("3"); |
||||||
|
assertThat(target.counter.get()).isEqualTo(3); |
||||||
|
assertThatRuntimeException().isThrownBy(() -> proxy.otherOperation().block()) |
||||||
|
.withCauseInstanceOf(IOException.class); |
||||||
|
assertThat(target.counter.get()).isEqualTo(4); |
||||||
|
assertThatIllegalStateException().isThrownBy(() -> proxy.overrideOperation().blockFirst()) |
||||||
|
.withCauseInstanceOf(IOException.class); |
||||||
|
assertThat(target.counter.get()).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static class NonAnnotatedBean { |
||||||
|
|
||||||
|
AtomicInteger counter = new AtomicInteger(); |
||||||
|
|
||||||
|
public Mono<Object> retryOperation() { |
||||||
|
return Mono.fromCallable(() -> { |
||||||
|
counter.incrementAndGet(); |
||||||
|
throw new IOException(counter.toString()); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static class AnnotatedMethodBean { |
||||||
|
|
||||||
|
AtomicInteger counter = new AtomicInteger(); |
||||||
|
|
||||||
|
@Retryable(maxAttempts = 5, delay = 10) |
||||||
|
public Mono<Object> retryOperation() { |
||||||
|
return Mono.fromCallable(() -> { |
||||||
|
counter.incrementAndGet(); |
||||||
|
throw new IOException(counter.toString()); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Retryable(delay = 10, jitterDelay = 5, delayMultiplier = 2.0, maxDelay = 40, |
||||||
|
includes = IOException.class, excludes = AccessDeniedException.class, |
||||||
|
predicate = CustomPredicate.class) |
||||||
|
public static class AnnotatedClassBean { |
||||||
|
|
||||||
|
AtomicInteger counter = new AtomicInteger(); |
||||||
|
|
||||||
|
public Mono<Object> retryOperation() { |
||||||
|
return Mono.fromCallable(() -> { |
||||||
|
counter.incrementAndGet(); |
||||||
|
throw new IOException(counter.toString()); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
public Mono<Object> otherOperation() { |
||||||
|
return Mono.fromCallable(() -> { |
||||||
|
counter.incrementAndGet(); |
||||||
|
throw new AccessDeniedException(counter.toString()); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
@Retryable(value = IOException.class, maxAttempts = 1, delay = 10) |
||||||
|
public Flux<Object> overrideOperation() { |
||||||
|
return Flux.from(Mono.fromCallable(() -> { |
||||||
|
counter.incrementAndGet(); |
||||||
|
throw new AccessDeniedException(counter.toString()); |
||||||
|
})); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static class CustomPredicate implements MethodRetryPredicate { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean shouldRetry(Method method, Throwable throwable) { |
||||||
|
return !"3".equals(throwable.getMessage()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,171 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2002-2025 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.aop.retry; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.lang.reflect.Method; |
||||||
|
import java.nio.file.AccessDeniedException; |
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import org.springframework.aop.framework.AopProxyUtils; |
||||||
|
import org.springframework.aop.framework.ProxyFactory; |
||||||
|
import org.springframework.aop.retry.annotation.RetryAnnotationBeanPostProcessor; |
||||||
|
import org.springframework.aop.retry.annotation.RetryAnnotationInterceptor; |
||||||
|
import org.springframework.aop.retry.annotation.Retryable; |
||||||
|
import org.springframework.beans.factory.support.DefaultListableBeanFactory; |
||||||
|
import org.springframework.beans.factory.support.RootBeanDefinition; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat; |
||||||
|
import static org.assertj.core.api.Assertions.assertThatIOException; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author Juergen Hoeller |
||||||
|
* @since 7.0 |
||||||
|
*/ |
||||||
|
public class RetryInterceptorTests { |
||||||
|
|
||||||
|
@Test |
||||||
|
void withSimpleInterceptor() { |
||||||
|
NonAnnotatedBean target = new NonAnnotatedBean(); |
||||||
|
ProxyFactory pf = new ProxyFactory(); |
||||||
|
pf.setTarget(target); |
||||||
|
pf.addAdvice(new SimpleRetryInterceptor(new MethodRetrySpec((m, t) -> true, 5, 10))); |
||||||
|
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy(); |
||||||
|
|
||||||
|
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6"); |
||||||
|
assertThat(target.counter).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void withAnnotationInterceptorForMethod() { |
||||||
|
AnnotatedMethodBean target = new AnnotatedMethodBean(); |
||||||
|
ProxyFactory pf = new ProxyFactory(); |
||||||
|
pf.setTarget(target); |
||||||
|
pf.addAdvice(new RetryAnnotationInterceptor()); |
||||||
|
AnnotatedMethodBean proxy = (AnnotatedMethodBean) pf.getProxy(); |
||||||
|
|
||||||
|
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6"); |
||||||
|
assertThat(target.counter).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void withPostProcessorForMethod() { |
||||||
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); |
||||||
|
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class)); |
||||||
|
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor(); |
||||||
|
bpp.setBeanFactory(bf); |
||||||
|
bf.addBeanPostProcessor(bpp); |
||||||
|
AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class); |
||||||
|
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy); |
||||||
|
|
||||||
|
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6"); |
||||||
|
assertThat(target.counter).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void withAnnotationInterceptorForClass() { |
||||||
|
AnnotatedClassBean target = new AnnotatedClassBean(); |
||||||
|
ProxyFactory pf = new ProxyFactory(); |
||||||
|
pf.setTarget(target); |
||||||
|
pf.addAdvice(new RetryAnnotationInterceptor()); |
||||||
|
AnnotatedClassBean proxy = (AnnotatedClassBean) pf.getProxy(); |
||||||
|
|
||||||
|
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("3"); |
||||||
|
assertThat(target.counter).isEqualTo(3); |
||||||
|
assertThatIOException().isThrownBy(proxy::otherOperation); |
||||||
|
assertThat(target.counter).isEqualTo(4); |
||||||
|
assertThatIOException().isThrownBy(proxy::overrideOperation); |
||||||
|
assertThat(target.counter).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
void withPostProcessorForClass() { |
||||||
|
DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); |
||||||
|
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class)); |
||||||
|
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor(); |
||||||
|
bpp.setBeanFactory(bf); |
||||||
|
bf.addBeanPostProcessor(bpp); |
||||||
|
AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class); |
||||||
|
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy); |
||||||
|
|
||||||
|
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("3"); |
||||||
|
assertThat(target.counter).isEqualTo(3); |
||||||
|
assertThatIOException().isThrownBy(proxy::otherOperation); |
||||||
|
assertThat(target.counter).isEqualTo(4); |
||||||
|
assertThatIOException().isThrownBy(proxy::overrideOperation); |
||||||
|
assertThat(target.counter).isEqualTo(6); |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static class NonAnnotatedBean { |
||||||
|
|
||||||
|
int counter = 0; |
||||||
|
|
||||||
|
public void retryOperation() throws IOException { |
||||||
|
counter++; |
||||||
|
throw new IOException(Integer.toString(counter)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
public static class AnnotatedMethodBean { |
||||||
|
|
||||||
|
int counter = 0; |
||||||
|
|
||||||
|
@Retryable(maxAttempts = 5, delay = 10) |
||||||
|
public void retryOperation() throws IOException { |
||||||
|
counter++; |
||||||
|
throw new IOException(Integer.toString(counter)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
@Retryable(delay = 10, jitterDelay = 5, delayMultiplier = 2.0, maxDelay = 40, |
||||||
|
includes = IOException.class, excludes = AccessDeniedException.class, |
||||||
|
predicate = CustomPredicate.class) |
||||||
|
public static class AnnotatedClassBean { |
||||||
|
|
||||||
|
int counter = 0; |
||||||
|
|
||||||
|
public void retryOperation() throws IOException { |
||||||
|
counter++; |
||||||
|
throw new IOException(Integer.toString(counter)); |
||||||
|
} |
||||||
|
|
||||||
|
public void otherOperation() throws IOException { |
||||||
|
counter++; |
||||||
|
throw new AccessDeniedException(Integer.toString(counter)); |
||||||
|
} |
||||||
|
|
||||||
|
@Retryable(value = IOException.class, maxAttempts = 1, delay = 10) |
||||||
|
public void overrideOperation() throws IOException { |
||||||
|
counter++; |
||||||
|
throw new AccessDeniedException(Integer.toString(counter)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
private static class CustomPredicate implements MethodRetryPredicate { |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean shouldRetry(Method method, Throwable throwable) { |
||||||
|
return !"3".equals(throwable.getMessage()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue