From 91f112a918812c8cbc15c85b2999f25c5dadc015 Mon Sep 17 00:00:00 2001 From: Hyunsang Han Date: Sat, 13 Sep 2025 00:15:57 +0900 Subject: [PATCH] =?UTF-8?q?Add=20placeholder=20resolution=20support=20for?= =?UTF-8?q?=20@=E2=81=A0ConcurrencyLimit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See gh-35461 Closes gh-35470 Signed-off-by: Hyunsang Han --- .../annotation/ConcurrencyLimit.java | 9 ++++ .../ConcurrencyLimitBeanPostProcessor.java | 32 ++++++++++-- .../resilience/ConcurrencyLimitTests.java | 52 +++++++++++++++++++ 3 files changed, 90 insertions(+), 3 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimit.java b/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimit.java index 465ba44bb5b..d042b5c1d6a 100644 --- a/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimit.java +++ b/spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimit.java @@ -42,6 +42,7 @@ import org.springframework.aot.hint.annotation.Reflective; * {@link org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor}. * * @author Juergen Hoeller + * @author Hyunsang Han * @since 7.0 * @see EnableResilientMethods * @see ConcurrencyLimitBeanPostProcessor @@ -62,4 +63,12 @@ public @interface ConcurrencyLimit { */ int value() default 1; + /** + * The concurrency limit as a configurable String. + * A non-empty value specified here overrides the {@link #value()} attribute. + *

This supports Spring-style "${...}" placeholders as well as SpEL expressions. + * @see #value() + */ + String valueString() default ""; + } 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 c556c7d52ba..2f133cb65f2 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 @@ -31,9 +31,12 @@ import org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor; import org.springframework.aop.support.ComposablePointcut; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.StringUtils; +import org.springframework.util.StringValueResolver; /** * A convenient {@link org.springframework.beans.factory.config.BeanPostProcessor @@ -41,10 +44,14 @@ import org.springframework.util.ConcurrentReferenceHashMap; * annotated with {@link ConcurrencyLimit @ConcurrencyLimit}. * * @author Juergen Hoeller + * @author Hyunsang Han * @since 7.0 */ @SuppressWarnings("serial") -public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { +public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor + implements EmbeddedValueResolverAware { + + private @Nullable StringValueResolver embeddedValueResolver; public ConcurrencyLimitBeanPostProcessor() { setBeforeExistingAdvisors(true); @@ -57,7 +64,13 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA } - private static class ConcurrencyLimitInterceptor implements MethodInterceptor { + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.embeddedValueResolver = resolver; + } + + + private class ConcurrencyLimitInterceptor implements MethodInterceptor { private final Map cachePerInstance = new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); @@ -93,7 +106,8 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA } if (interceptor == null) { Assert.state(limit != null, "No @ConcurrencyLimit annotation found"); - interceptor = new ConcurrencyThrottleInterceptor(limit.value()); + int concurrencyLimit = parseInt(limit.value(), limit.valueString()); + interceptor = new ConcurrencyThrottleInterceptor(concurrencyLimit); if (!perMethod) { cache.classInterceptor = interceptor; } @@ -104,6 +118,18 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA } return interceptor.invoke(invocation); } + + private int parseInt(int value, String stringValue) { + if (StringUtils.hasText(stringValue)) { + if (embeddedValueResolver != null) { + stringValue = embeddedValueResolver.resolveStringValue(stringValue); + } + if (StringUtils.hasText(stringValue)) { + return Integer.parseInt(stringValue); + } + } + return value; + } } 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 597389309d9..26ca0645e20 100644 --- a/spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java +++ b/spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java @@ -18,6 +18,7 @@ package org.springframework.resilience; import java.util.ArrayList; import java.util.List; +import java.util.Properties; import java.util.concurrent.CompletableFuture; import java.util.concurrent.atomic.AtomicInteger; @@ -28,13 +29,17 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.core.env.PropertiesPropertySource; import org.springframework.resilience.annotation.ConcurrencyLimit; import org.springframework.resilience.annotation.ConcurrencyLimitBeanPostProcessor; +import org.springframework.resilience.annotation.EnableResilientMethods; import static org.assertj.core.api.Assertions.assertThat; /** * @author Juergen Hoeller + * @author Hyunsang Han * @since 7.0 */ class ConcurrencyLimitTests { @@ -97,6 +102,28 @@ class ConcurrencyLimitTests { assertThat(target.current).hasValue(0); } + @Test + void withPlaceholderResolution() { + Properties props = new Properties(); + props.setProperty("test.concurrency.limit", "3"); + + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("test", props)); + ctx.register(PlaceholderTestConfig.class, PlaceholderBean.class); + ctx.refresh(); + + PlaceholderBean proxy = ctx.getBean(PlaceholderBean.class); + PlaceholderBean target = (PlaceholderBean) AopProxyUtils.getSingletonTarget(proxy); + + // Test with limit=3 from properties + 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); + ctx.close(); + } static class NonAnnotatedBean { @@ -185,4 +212,29 @@ class ConcurrencyLimitTests { } } + + @EnableResilientMethods + static class PlaceholderTestConfig { + } + + + static class PlaceholderBean { + + AtomicInteger current = new AtomicInteger(); + + @ConcurrencyLimit(valueString = "${test.concurrency.limit}") + public void concurrentOperation() { + if (current.incrementAndGet() > 3) { // Assumes test.concurrency.limit=3 + throw new IllegalStateException(); + } + try { + Thread.sleep(100); + } + catch (InterruptedException ex) { + throw new IllegalStateException(ex); + } + current.decrementAndGet(); + } + } + }