diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/EnableResilientMethods.java b/spring-context/src/main/java/org/springframework/resilience/annotation/EnableResilientMethods.java index 4facad6d4e3..e8c00bfc887 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/EnableResilientMethods.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/EnableResilientMethods.java @@ -60,10 +60,10 @@ public @interface EnableResilientMethods { /** * Indicate the order in which the {@link RetryAnnotationBeanPostProcessor} * and {@link ConcurrencyLimitBeanPostProcessor} should be applied. - *

The default is {@link Ordered#LOWEST_PRECEDENCE} in order to run - * after all other post-processors, so that they can add advisors to - * existing proxies rather than double-proxy. + *

The default is {@link Ordered#LOWEST_PRECEDENCE - 1} in order to run + * after all common post-processors, except for {@code @EnableAsync}. + * @see org.springframework.scheduling.annotation.EnableAsync#order() */ - int order() default Ordered.LOWEST_PRECEDENCE; + int order() default Ordered.LOWEST_PRECEDENCE - 1; } diff --git a/spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java b/spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java index b85a181fc99..d4a14fa3427 100644 --- a/spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java +++ b/spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java @@ -17,6 +17,7 @@ package org.springframework.resilience.retry; import java.lang.reflect.Method; +import java.util.concurrent.Future; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; @@ -77,7 +78,7 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor { return invocation.proceed(); } - if (this.reactiveAdapterRegistry != null) { + if (this.reactiveAdapterRegistry != null && !Future.class.isAssignableFrom(method.getReturnType())) { ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(method.getReturnType()); if (adapter != null) { Object result = invocation.proceed(); diff --git a/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java b/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java index 456687a7579..7e32ebdaa59 100644 --- a/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java @@ -22,6 +22,8 @@ import java.nio.charset.MalformedInputException; import java.nio.file.AccessDeniedException; import java.time.Duration; import java.util.Properties; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.atomic.AtomicInteger; import org.aopalliance.intercept.MethodInterceptor; @@ -44,8 +46,11 @@ import org.springframework.resilience.annotation.RetryAnnotationBeanPostProcesso import org.springframework.resilience.annotation.Retryable; import org.springframework.resilience.retry.MethodRetrySpec; import org.springframework.resilience.retry.SimpleRetryInterceptor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.scheduling.annotation.EnableAsync; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIOException; import static org.assertj.core.api.Assertions.assertThatRuntimeException; @@ -265,11 +270,11 @@ class RetryInterceptorTests { @Test void withEnableAnnotation() throws Exception { AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.registerBeanDefinition("bean", new RootBeanDefinition(DoubleAnnotatedBean.class)); + ctx.registerBeanDefinition("bean", new RootBeanDefinition(ConcurrencyLimitAnnotatedBean.class)); ctx.registerBeanDefinition("config", new RootBeanDefinition(EnablingConfig.class)); ctx.refresh(); - DoubleAnnotatedBean proxy = ctx.getBean(DoubleAnnotatedBean.class); - DoubleAnnotatedBean target = (DoubleAnnotatedBean) AopProxyUtils.getSingletonTarget(proxy); + ConcurrencyLimitAnnotatedBean proxy = ctx.getBean(ConcurrencyLimitAnnotatedBean.class); + ConcurrencyLimitAnnotatedBean target = (ConcurrencyLimitAnnotatedBean) AopProxyUtils.getSingletonTarget(proxy); Thread thread = new Thread(() -> assertThatIOException().isThrownBy(proxy::retryOperation)); thread.start(); @@ -279,6 +284,20 @@ class RetryInterceptorTests { assertThat(target.threadChange).hasValue(2); } + @Test + void withAsyncAnnotation() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.registerBeanDefinition("bean", new RootBeanDefinition(AsyncAnnotatedBean.class)); + ctx.registerBeanDefinition("config", new RootBeanDefinition(EnablingConfigWithAsync.class)); + ctx.refresh(); + AsyncAnnotatedBean proxy = ctx.getBean(AsyncAnnotatedBean.class); + AsyncAnnotatedBean target = (AsyncAnnotatedBean) AopProxyUtils.getSingletonTarget(proxy); + + assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> proxy.retryOperation().join()) + .withCauseInstanceOf(IllegalStateException.class); + assertThat(target.counter).hasValue(3); + } + static class NonAnnotatedBean implements PlainInterface { @@ -392,7 +411,7 @@ class RetryInterceptorTests { } - static class DoubleAnnotatedBean { + static class ConcurrencyLimitAnnotatedBean { AtomicInteger current = new AtomicInteger(); @@ -419,8 +438,26 @@ class RetryInterceptorTests { } + static class AsyncAnnotatedBean { + + AtomicInteger counter = new AtomicInteger(); + + @Async + @Retryable(maxAttempts = 2, delay = 10) + public CompletableFuture retryOperation() { + throw new IllegalStateException(Integer.toString(counter.incrementAndGet())); + } + } + + @EnableResilientMethods static class EnablingConfig { } + + @EnableAsync + @EnableResilientMethods + static class EnablingConfigWithAsync { + } + }