From 516bf1c60627e053323ed6e369700cc0b3844bc7 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Fri, 30 Jan 2026 17:12:22 +0100 Subject: [PATCH] Consistently detect resilience annotations on interfaces Closes gh-36233 --- .../ConcurrencyLimitBeanPostProcessor.java | 4 +- .../RetryAnnotationBeanPostProcessor.java | 4 +- .../resilience/ConcurrencyLimitTests.java | 66 ++++++++++++++++++- .../resilience/RetryInterceptorTests.java | 1 - 4 files changed, 69 insertions(+), 6 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimitBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimitBeanPostProcessor.java index 28d0bdde8e4..b3e919d96ce 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimitBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimitBeanPostProcessor.java @@ -101,14 +101,14 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA interceptor = holder.methodInterceptors.get(method); if (interceptor == null) { boolean perMethod = false; - ConcurrencyLimit annotation = AnnotatedElementUtils.getMergedAnnotation(method, ConcurrencyLimit.class); + ConcurrencyLimit annotation = AnnotatedElementUtils.findMergedAnnotation(method, ConcurrencyLimit.class); if (annotation != null) { perMethod = true; } else { interceptor = holder.classInterceptor; if (interceptor == null) { - annotation = AnnotatedElementUtils.getMergedAnnotation(targetClass, ConcurrencyLimit.class); + annotation = AnnotatedElementUtils.findMergedAnnotation(targetClass, ConcurrencyLimit.class); } } if (interceptor == null) { diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java index 4535d0ac9b1..fdf3b8cdf47 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/RetryAnnotationBeanPostProcessor.java @@ -93,9 +93,9 @@ public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAd return retrySpec; } - Retryable retryable = AnnotatedElementUtils.getMergedAnnotation(method, Retryable.class); + Retryable retryable = AnnotatedElementUtils.findMergedAnnotation(method, Retryable.class); if (retryable == null) { - retryable = AnnotatedElementUtils.getMergedAnnotation(targetClass, Retryable.class); + retryable = AnnotatedElementUtils.findMergedAnnotation(targetClass, Retryable.class); if (retryable == null) { return null; } diff --git a/spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java b/spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java index 814514fd0f4..72bf334df4c 100644 --- a/spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java +++ b/spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java @@ -23,9 +23,11 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; +import org.springframework.aop.config.AopConfigUtils; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -77,6 +79,32 @@ class ConcurrencyLimitTests { assertThat(target.current).hasValue(0); } + @Test + void withPostProcessorForMethodWithInterface() { + AnnotatedInterface proxy = createProxy(AnnotatedMethodBeanWithInterface.class, AnnotatedInterface.class, false); + AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy); + + List> futures = new ArrayList<>(10); + for (int i = 0; i < 10; i++) { + futures.add(CompletableFuture.runAsync(proxy::concurrentOperation)); + } + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + assertThat(target.current).hasValue(0); + } + + @Test + void withPostProcessorForMethodWithInterfaceAndDefaultTargetClass() { + AnnotatedInterface proxy = createProxy(AnnotatedMethodBeanWithInterface.class, AnnotatedInterface.class, true); + AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy); + + List> futures = new ArrayList<>(10); + for (int i = 0; i < 10; i++) { + futures.add(CompletableFuture.runAsync(proxy::concurrentOperation)); + } + CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); + assertThat(target.current).hasValue(0); + } + @Test void withPostProcessorForMethodWithUnboundedConcurrency() { AnnotatedMethodBean proxy = createProxy(AnnotatedMethodBean.class); @@ -201,12 +229,21 @@ class ConcurrencyLimitTests { private static T createProxy(Class beanClass) { + return createProxy(beanClass, beanClass, true); + } + + private static T createProxy(Class beanClass, Class exposedClass, boolean proxyTargetClass) { DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); + if (exposedClass.isInterface() && proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(bf); + } bf.registerBeanDefinition("bean", new RootBeanDefinition(beanClass)); ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor(); bpp.setBeanFactory(bf); bf.addBeanPostProcessor(bpp); - return bf.getBean(beanClass); + T proxy = bf.getBean(exposedClass); + assertThat(proxyTargetClass ? AopUtils.isCglibProxy(proxy) : AopUtils.isJdkDynamicProxy(proxy)).isTrue(); + return proxy; } @@ -274,6 +311,33 @@ class ConcurrencyLimitTests { } + static class AnnotatedMethodBeanWithInterface implements AnnotatedInterface { + + final AtomicInteger current = new AtomicInteger(); + + @Override + public void concurrentOperation() { + if (current.incrementAndGet() > 2) { + throw new IllegalStateException(); + } + try { + Thread.sleep(100); + } + catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + current.decrementAndGet(); + } + } + + + interface AnnotatedInterface { + + @ConcurrencyLimit(2) + void concurrentOperation(); + } + + @ConcurrencyLimit(2) static class AnnotatedClassBean { 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 79ae11979c3..78d968f7333 100644 --- a/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java +++ b/spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java @@ -523,7 +523,6 @@ class RetryInterceptorTests { int counter = 0; - @Retryable(maxRetries = 5, delay = 10) @Override public void retryOperation() throws IOException { counter++;