From 73ad1ebffb2c10e57c812919b0afb0aba13cc940 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Mon, 22 Sep 2025 16:02:31 +0200 Subject: [PATCH] =?UTF-8?q?Introduce=20@=E2=81=A0ConcurrencyLimit(limit)?= =?UTF-8?q?=20and=20revise=20contribution?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a new `limit` attribute in @⁠ConcurrencyLimit as an alias for the existing `value` attribute. This commit also renames the `valueString` attribute to `limitString`. See gh-35461 See gh-35470 --- .../annotation/ConcurrencyLimit.java | 25 +++++++++++++++---- .../ConcurrencyLimitBeanPostProcessor.java | 11 ++++---- .../resilience/ConcurrencyLimitTests.java | 16 ++++++------ 3 files changed, 33 insertions(+), 19 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 d042b5c1d6a..bacc60a5756 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 @@ -23,6 +23,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import org.springframework.aot.hint.annotation.Reflective; +import org.springframework.core.annotation.AliasFor; /** * A common annotation specifying a concurrency limit for an individual method, @@ -43,6 +44,7 @@ import org.springframework.aot.hint.annotation.Reflective; * * @author Juergen Hoeller * @author Hyunsang Han + * @author Sam Brannen * @since 7.0 * @see EnableResilientMethods * @see ConcurrencyLimitBeanPostProcessor @@ -55,20 +57,33 @@ import org.springframework.aot.hint.annotation.Reflective; @Reflective public @interface ConcurrencyLimit { + /** + * Alias for {@link #limit()}. + *

Intended to be used when no other attributes are needed — for + * example, {@code @ConcurrencyLimit(5)}. + * @see #limitString() + */ + @AliasFor("limit") + int value() default 1; + /** * The applicable concurrency limit: 1 by default, * effectively locking the target instance for each method invocation. *

Specify a limit higher than 1 for pool-like throttling, constraining * the number of concurrent invocations similar to the upper bound of a pool. + * @see #value() + * @see #limitString() */ - int value() default 1; + @AliasFor("value") + int limit() default 1; /** - * The concurrency limit as a configurable String. - * A non-empty value specified here overrides the {@link #value()} attribute. + * The concurrency limit, as a configurable String. + *

A non-empty value specified here overrides the {@link #limit()} (or + * {@link #value()}) attribute. *

This supports Spring-style "${...}" placeholders as well as SpEL expressions. - * @see #value() + * @see #limit() */ - String valueString() default ""; + String limitString() 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 2f133cb65f2..e195bd8d6f3 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 @@ -53,6 +53,7 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA private @Nullable StringValueResolver embeddedValueResolver; + public ConcurrencyLimitBeanPostProcessor() { setBeforeExistingAdvisors(true); @@ -94,19 +95,19 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA interceptor = cache.methodInterceptors.get(method); if (interceptor == null) { boolean perMethod = false; - ConcurrencyLimit limit = AnnotatedElementUtils.getMergedAnnotation(method, ConcurrencyLimit.class); - if (limit != null) { + ConcurrencyLimit annotation = AnnotatedElementUtils.getMergedAnnotation(method, ConcurrencyLimit.class); + if (annotation != null) { perMethod = true; } else { interceptor = cache.classInterceptor; if (interceptor == null) { - limit = AnnotatedElementUtils.getMergedAnnotation(targetClass, ConcurrencyLimit.class); + annotation = AnnotatedElementUtils.getMergedAnnotation(targetClass, ConcurrencyLimit.class); } } if (interceptor == null) { - Assert.state(limit != null, "No @ConcurrencyLimit annotation found"); - int concurrencyLimit = parseInt(limit.value(), limit.valueString()); + Assert.state(annotation != null, "No @ConcurrencyLimit annotation found"); + int concurrencyLimit = parseInt(annotation.limit(), annotation.limitString()); interceptor = new ConcurrencyThrottleInterceptor(concurrencyLimit); if (!perMethod) { cache.classInterceptor = interceptor; 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 26ca0645e20..d8c4c7c3488 100644 --- a/spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java +++ b/spring-context/src/test/java/org/springframework/resilience/ConcurrencyLimitTests.java @@ -18,7 +18,6 @@ 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; @@ -30,7 +29,7 @@ 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.core.testfixture.env.MockPropertySource; import org.springframework.resilience.annotation.ConcurrencyLimit; import org.springframework.resilience.annotation.ConcurrencyLimitBeanPostProcessor; import org.springframework.resilience.annotation.EnableResilientMethods; @@ -104,18 +103,16 @@ class ConcurrencyLimitTests { @Test void withPlaceholderResolution() { - Properties props = new Properties(); - props.setProperty("test.concurrency.limit", "3"); - + MockPropertySource mockPropertySource = new MockPropertySource("test").withProperty("test.concurrency.limit", "3"); AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.getEnvironment().getPropertySources().addFirst(new PropertiesPropertySource("test", props)); + ctx.getEnvironment().getPropertySources().addFirst(mockPropertySource); 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 + // Test with limit=3 from MockPropertySource List> futures = new ArrayList<>(10); for (int i = 0; i < 10; i++) { futures.add(CompletableFuture.runAsync(proxy::concurrentOperation)); @@ -125,6 +122,7 @@ class ConcurrencyLimitTests { ctx.close(); } + static class NonAnnotatedBean { AtomicInteger counter = new AtomicInteger(); @@ -197,7 +195,7 @@ class ConcurrencyLimitTests { current.decrementAndGet(); } - @ConcurrencyLimit(1) + @ConcurrencyLimit(limit = 1) public void overrideOperation() { if (currentOverride.incrementAndGet() > 1) { throw new IllegalStateException(); @@ -222,7 +220,7 @@ class ConcurrencyLimitTests { AtomicInteger current = new AtomicInteger(); - @ConcurrencyLimit(valueString = "${test.concurrency.limit}") + @ConcurrencyLimit(limitString = "${test.concurrency.limit}") public void concurrentOperation() { if (current.incrementAndGet() > 3) { // Assumes test.concurrency.limit=3 throw new IllegalStateException();