Skip to content

Commit 91f112a

Browse files
HyunSangHansbrannen
authored andcommitted
Add placeholder resolution support for @⁠ConcurrencyLimit
See gh-35461 Closes gh-35470 Signed-off-by: Hyunsang Han <gustkd3@gmail.com>
1 parent 23b8c61 commit 91f112a

File tree

3 files changed

+90
-3
lines changed

3 files changed

+90
-3
lines changed

spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimit.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
* {@link org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor}.
4343
*
4444
* @author Juergen Hoeller
45+
* @author Hyunsang Han
4546
* @since 7.0
4647
* @see EnableResilientMethods
4748
* @see ConcurrencyLimitBeanPostProcessor
@@ -62,4 +63,12 @@
6263
*/
6364
int value() default 1;
6465

66+
/**
67+
* The concurrency limit as a configurable String.
68+
* A non-empty value specified here overrides the {@link #value()} attribute.
69+
* <p>This supports Spring-style "${...}" placeholders as well as SpEL expressions.
70+
* @see #value()
71+
*/
72+
String valueString() default "";
73+
6574
}

spring-context/src/main/java/org/springframework/resilience/annotation/ConcurrencyLimitBeanPostProcessor.java

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,27 @@
3131
import org.springframework.aop.support.ComposablePointcut;
3232
import org.springframework.aop.support.DefaultPointcutAdvisor;
3333
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
34+
import org.springframework.context.EmbeddedValueResolverAware;
3435
import org.springframework.core.annotation.AnnotatedElementUtils;
3536
import org.springframework.util.Assert;
3637
import org.springframework.util.ConcurrentReferenceHashMap;
38+
import org.springframework.util.StringUtils;
39+
import org.springframework.util.StringValueResolver;
3740

3841
/**
3942
* A convenient {@link org.springframework.beans.factory.config.BeanPostProcessor
4043
* BeanPostProcessor} that applies a concurrency interceptor to all bean methods
4144
* annotated with {@link ConcurrencyLimit @ConcurrencyLimit}.
4245
*
4346
* @author Juergen Hoeller
47+
* @author Hyunsang Han
4448
* @since 7.0
4549
*/
4650
@SuppressWarnings("serial")
47-
public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor {
51+
public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
52+
implements EmbeddedValueResolverAware {
53+
54+
private @Nullable StringValueResolver embeddedValueResolver;
4855

4956
public ConcurrencyLimitBeanPostProcessor() {
5057
setBeforeExistingAdvisors(true);
@@ -57,7 +64,13 @@ public ConcurrencyLimitBeanPostProcessor() {
5764
}
5865

5966

60-
private static class ConcurrencyLimitInterceptor implements MethodInterceptor {
67+
@Override
68+
public void setEmbeddedValueResolver(StringValueResolver resolver) {
69+
this.embeddedValueResolver = resolver;
70+
}
71+
72+
73+
private class ConcurrencyLimitInterceptor implements MethodInterceptor {
6174

6275
private final Map<Object, ConcurrencyThrottleCache> cachePerInstance =
6376
new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
@@ -93,7 +106,8 @@ private static class ConcurrencyLimitInterceptor implements MethodInterceptor {
93106
}
94107
if (interceptor == null) {
95108
Assert.state(limit != null, "No @ConcurrencyLimit annotation found");
96-
interceptor = new ConcurrencyThrottleInterceptor(limit.value());
109+
int concurrencyLimit = parseInt(limit.value(), limit.valueString());
110+
interceptor = new ConcurrencyThrottleInterceptor(concurrencyLimit);
97111
if (!perMethod) {
98112
cache.classInterceptor = interceptor;
99113
}
@@ -104,6 +118,18 @@ private static class ConcurrencyLimitInterceptor implements MethodInterceptor {
104118
}
105119
return interceptor.invoke(invocation);
106120
}
121+
122+
private int parseInt(int value, String stringValue) {
123+
if (StringUtils.hasText(stringValue)) {
124+
if (embeddedValueResolver != null) {
125+
stringValue = embeddedValueResolver.resolveStringValue(stringValue);
126+
}
127+
if (StringUtils.hasText(stringValue)) {
128+
return Integer.parseInt(stringValue);
129+
}
130+
}
131+
return value;
132+
}
107133
}
108134

109135

spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.ArrayList;
2020
import java.util.List;
21+
import java.util.Properties;
2122
import java.util.concurrent.CompletableFuture;
2223
import java.util.concurrent.atomic.AtomicInteger;
2324

@@ -28,13 +29,17 @@
2829
import org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor;
2930
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
3031
import org.springframework.beans.factory.support.RootBeanDefinition;
32+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
33+
import org.springframework.core.env.PropertiesPropertySource;
3134
import org.springframework.resilience.annotation.ConcurrencyLimit;
3235
import org.springframework.resilience.annotation.ConcurrencyLimitBeanPostProcessor;
36+
import org.springframework.resilience.annotation.EnableResilientMethods;
3337

3438
import static org.assertj.core.api.Assertions.assertThat;
3539

3640
/**
3741
* @author Juergen Hoeller
42+
* @author Hyunsang Han
3843
* @since 7.0
3944
*/
4045
class ConcurrencyLimitTests {
@@ -97,6 +102,28 @@ void withPostProcessorForClass() {
97102
assertThat(target.current).hasValue(0);
98103
}
99104

105+
@Test
106+
void withPlaceholderResolution() {
107+
Properties props = new Properties();
108+
props.setProperty("test.concurrency.limit", "3");
109+
110+
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
111+
ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("test", props));
112+
ctx.register(PlaceholderTestConfig.class, PlaceholderBean.class);
113+
ctx.refresh();
114+
115+
PlaceholderBean proxy = ctx.getBean(PlaceholderBean.class);
116+
PlaceholderBean target = (PlaceholderBean) AopProxyUtils.getSingletonTarget(proxy);
117+
118+
// Test with limit=3 from properties
119+
List<CompletableFuture<?>> futures = new ArrayList<>(10);
120+
for (int i = 0; i < 10; i++) {
121+
futures.add(CompletableFuture.runAsync(proxy::concurrentOperation));
122+
}
123+
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
124+
assertThat(target.current).hasValue(0);
125+
ctx.close();
126+
}
100127

101128
static class NonAnnotatedBean {
102129

@@ -185,4 +212,29 @@ public void overrideOperation() {
185212
}
186213
}
187214

215+
216+
@EnableResilientMethods
217+
static class PlaceholderTestConfig {
218+
}
219+
220+
221+
static class PlaceholderBean {
222+
223+
AtomicInteger current = new AtomicInteger();
224+
225+
@ConcurrencyLimit(valueString = "${test.concurrency.limit}")
226+
public void concurrentOperation() {
227+
if (current.incrementAndGet() > 3) { // Assumes test.concurrency.limit=3
228+
throw new IllegalStateException();
229+
}
230+
try {
231+
Thread.sleep(100);
232+
}
233+
catch (InterruptedException ex) {
234+
throw new IllegalStateException(ex);
235+
}
236+
current.decrementAndGet();
237+
}
238+
}
239+
188240
}

0 commit comments

Comments
 (0)