Prevent empty declaration of @ConcurrencyLimit
As a follow-up to gh-35461 and a comment left on the Spring Blog, we have decided to prevent empty declarations of @ConcurrencyLimit, thereby requiring users to explicitly declare the value for the limit. Closes gh-35523
This commit is contained in:
		
							parent
							
								
									8b254ad25e
								
							
						
					
					
						commit
						5ac3c40689
					
				|  | @ -64,25 +64,30 @@ public @interface ConcurrencyLimit { | |||
| 	 * @see #limitString() | ||||
| 	 */ | ||||
| 	@AliasFor("limit") | ||||
| 	int value() default 1; | ||||
| 	int value() default Integer.MIN_VALUE; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * The applicable concurrency limit: 1 by default, | ||||
| 	 * effectively locking the target instance for each method invocation. | ||||
| 	 * <p>Specify a limit higher than 1 for pool-like throttling, constraining | ||||
| 	 * The concurrency limit. | ||||
| 	 * <p>Specify {@code 1} to effectively lock the target instance for each method | ||||
| 	 * invocation. | ||||
| 	 * <p>Specify a limit greater than {@code 1} for pool-like throttling, constraining | ||||
| 	 * the number of concurrent invocations similar to the upper bound of a pool. | ||||
| 	 * <p>Specify {@code -1} for unbounded concurrency. | ||||
| 	 * @see #value() | ||||
| 	 * @see #limitString() | ||||
| 	 * @see org.springframework.util.ConcurrencyThrottleSupport#UNBOUNDED_CONCURRENCY | ||||
| 	 */ | ||||
| 	@AliasFor("value") | ||||
| 	int limit() default 1; | ||||
| 	int limit() default Integer.MIN_VALUE; | ||||
| 
 | ||||
| 	/** | ||||
| 	 * The concurrency limit, as a configurable String. | ||||
| 	 * <p>A non-empty value specified here overrides the {@link #limit()} (or | ||||
| 	 * {@link #value()}) attribute. | ||||
| 	 * <p>A non-empty value specified here overrides the {@link #limit()} and | ||||
| 	 * {@link #value()} attributes. | ||||
| 	 * <p>This supports Spring-style "${...}" placeholders as well as SpEL expressions. | ||||
| 	 * <p>See the Javadoc for {@link #limit()} for details on supported values. | ||||
| 	 * @see #limit() | ||||
| 	 * @see org.springframework.util.ConcurrencyThrottleSupport#UNBOUNDED_CONCURRENCY | ||||
| 	 */ | ||||
| 	String limitString() default ""; | ||||
| 
 | ||||
|  |  | |||
|  | @ -108,6 +108,9 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA | |||
| 						if (interceptor == null) { | ||||
| 							Assert.state(annotation != null, "No @ConcurrencyLimit annotation found"); | ||||
| 							int concurrencyLimit = parseInt(annotation.limit(), annotation.limitString()); | ||||
| 							if (concurrencyLimit < -1) { | ||||
| 								throw new IllegalStateException(annotation + " must be configured with a valid limit"); | ||||
| 							} | ||||
| 							interceptor = new ConcurrencyThrottleInterceptor(concurrencyLimit); | ||||
| 							if (!perMethod) { | ||||
| 								cache.classInterceptor = interceptor; | ||||
|  |  | |||
|  | @ -35,10 +35,13 @@ import org.springframework.resilience.annotation.ConcurrencyLimitBeanPostProcess | |||
| import org.springframework.resilience.annotation.EnableResilientMethods; | ||||
| 
 | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.assertj.core.api.Assertions.assertThatExceptionOfType; | ||||
| import static org.assertj.core.api.Assertions.assertThatIllegalStateException; | ||||
| 
 | ||||
| /** | ||||
|  * @author Juergen Hoeller | ||||
|  * @author Hyunsang Han | ||||
|  * @author Sam Brannen | ||||
|  * @since 7.0 | ||||
|  */ | ||||
| class ConcurrencyLimitTests { | ||||
|  | @ -61,12 +64,7 @@ class ConcurrencyLimitTests { | |||
| 
 | ||||
| 	@Test | ||||
| 	void withPostProcessorForMethod() { | ||||
| 		DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); | ||||
| 		bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class)); | ||||
| 		ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor(); | ||||
| 		bpp.setBeanFactory(bf); | ||||
| 		bf.addBeanPostProcessor(bpp); | ||||
| 		AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class); | ||||
| 		AnnotatedMethodBean proxy = createProxy(AnnotatedMethodBean.class); | ||||
| 		AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy); | ||||
| 
 | ||||
| 		List<CompletableFuture<?>> futures = new ArrayList<>(10); | ||||
|  | @ -77,14 +75,22 @@ class ConcurrencyLimitTests { | |||
| 		assertThat(target.current).hasValue(0); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void withPostProcessorForMethodWithUnboundedConcurrency() { | ||||
| 		AnnotatedMethodBean proxy = createProxy(AnnotatedMethodBean.class); | ||||
| 		AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy); | ||||
| 
 | ||||
| 		List<CompletableFuture<?>> futures = new ArrayList<>(10); | ||||
| 		for (int i = 0; i < 10; i++) { | ||||
| 			futures.add(CompletableFuture.runAsync(proxy::unboundedConcurrency)); | ||||
| 		} | ||||
| 		CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); | ||||
| 		assertThat(target.current).hasValue(10); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void withPostProcessorForClass() { | ||||
| 		DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); | ||||
| 		bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class)); | ||||
| 		ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor(); | ||||
| 		bpp.setBeanFactory(bf); | ||||
| 		bf.addBeanPostProcessor(bpp); | ||||
| 		AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class); | ||||
| 		AnnotatedClassBean proxy = createProxy(AnnotatedClassBean.class); | ||||
| 		AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy); | ||||
| 
 | ||||
| 		List<CompletableFuture<?>> futures = new ArrayList<>(30); | ||||
|  | @ -122,17 +128,52 @@ class ConcurrencyLimitTests { | |||
| 		ctx.close(); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void configurationErrors() { | ||||
| 		ConfigurationErrorsBean proxy = createProxy(ConfigurationErrorsBean.class); | ||||
| 
 | ||||
| 		assertThatIllegalStateException() | ||||
| 				.isThrownBy(proxy::emptyDeclaration) | ||||
| 				.withMessageMatching("@.+?ConcurrencyLimit(.+?) must be configured with a valid limit") | ||||
| 				.withMessageContaining("\"\"") | ||||
| 				.withMessageContaining(String.valueOf(Integer.MIN_VALUE)); | ||||
| 
 | ||||
| 		assertThatIllegalStateException() | ||||
| 				.isThrownBy(proxy::negative42Int) | ||||
| 				.withMessageMatching("@.+?ConcurrencyLimit(.+?) must be configured with a valid limit") | ||||
| 				.withMessageContaining("-42"); | ||||
| 
 | ||||
| 		assertThatIllegalStateException() | ||||
| 				.isThrownBy(proxy::negative42String) | ||||
| 				.withMessageMatching("@.+?ConcurrencyLimit(.+?) must be configured with a valid limit") | ||||
| 				.withMessageContaining("-42"); | ||||
| 
 | ||||
| 		assertThatExceptionOfType(NumberFormatException.class) | ||||
| 				.isThrownBy(proxy::alphanumericString) | ||||
| 				.withMessageContaining("B2"); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	private static <T> T createProxy(Class<T> beanClass) { | ||||
| 		DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); | ||||
| 		bf.registerBeanDefinition("bean", new RootBeanDefinition(beanClass)); | ||||
| 		ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor(); | ||||
| 		bpp.setBeanFactory(bf); | ||||
| 		bf.addBeanPostProcessor(bpp); | ||||
| 		return bf.getBean(beanClass); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	static class NonAnnotatedBean { | ||||
| 
 | ||||
| 		AtomicInteger counter = new AtomicInteger(); | ||||
| 		final AtomicInteger counter = new AtomicInteger(); | ||||
| 
 | ||||
| 		public void concurrentOperation() { | ||||
| 			if (counter.incrementAndGet() > 2) { | ||||
| 				throw new IllegalStateException(); | ||||
| 			} | ||||
| 			try { | ||||
| 				Thread.sleep(100); | ||||
| 				Thread.sleep(10); | ||||
| 			} | ||||
| 			catch (InterruptedException ex) { | ||||
| 				throw new IllegalStateException(ex); | ||||
|  | @ -144,7 +185,7 @@ class ConcurrencyLimitTests { | |||
| 
 | ||||
| 	static class AnnotatedMethodBean { | ||||
| 
 | ||||
| 		AtomicInteger current = new AtomicInteger(); | ||||
| 		final AtomicInteger current = new AtomicInteger(); | ||||
| 
 | ||||
| 		@ConcurrencyLimit(2) | ||||
| 		public void concurrentOperation() { | ||||
|  | @ -152,29 +193,40 @@ class ConcurrencyLimitTests { | |||
| 				throw new IllegalStateException(); | ||||
| 			} | ||||
| 			try { | ||||
| 				Thread.sleep(100); | ||||
| 				Thread.sleep(10); | ||||
| 			} | ||||
| 			catch (InterruptedException ex) { | ||||
| 				throw new IllegalStateException(ex); | ||||
| 			} | ||||
| 			current.decrementAndGet(); | ||||
| 		} | ||||
| 
 | ||||
| 		@ConcurrencyLimit(limit = -1) | ||||
| 		public void unboundedConcurrency() { | ||||
| 			current.incrementAndGet(); | ||||
| 			try { | ||||
| 				Thread.sleep(10); | ||||
| 			} | ||||
| 			catch (InterruptedException ex) { | ||||
| 				throw new IllegalStateException(ex); | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	@ConcurrencyLimit(2) | ||||
| 	static class AnnotatedClassBean { | ||||
| 
 | ||||
| 		AtomicInteger current = new AtomicInteger(); | ||||
| 		final AtomicInteger current = new AtomicInteger(); | ||||
| 
 | ||||
| 		AtomicInteger currentOverride = new AtomicInteger(); | ||||
| 		final AtomicInteger currentOverride = new AtomicInteger(); | ||||
| 
 | ||||
| 		public void concurrentOperation() { | ||||
| 			if (current.incrementAndGet() > 2) { | ||||
| 				throw new IllegalStateException(); | ||||
| 			} | ||||
| 			try { | ||||
| 				Thread.sleep(100); | ||||
| 				Thread.sleep(10); | ||||
| 			} | ||||
| 			catch (InterruptedException ex) { | ||||
| 				throw new IllegalStateException(ex); | ||||
|  | @ -187,7 +239,7 @@ class ConcurrencyLimitTests { | |||
| 				throw new IllegalStateException(); | ||||
| 			} | ||||
| 			try { | ||||
| 				Thread.sleep(100); | ||||
| 				Thread.sleep(10); | ||||
| 			} | ||||
| 			catch (InterruptedException ex) { | ||||
| 				throw new IllegalStateException(ex); | ||||
|  | @ -201,7 +253,7 @@ class ConcurrencyLimitTests { | |||
| 				throw new IllegalStateException(); | ||||
| 			} | ||||
| 			try { | ||||
| 				Thread.sleep(100); | ||||
| 				Thread.sleep(10); | ||||
| 			} | ||||
| 			catch (InterruptedException ex) { | ||||
| 				throw new IllegalStateException(ex); | ||||
|  | @ -218,7 +270,7 @@ class ConcurrencyLimitTests { | |||
| 
 | ||||
| 	static class PlaceholderBean { | ||||
| 
 | ||||
| 		AtomicInteger current = new AtomicInteger(); | ||||
| 		final AtomicInteger current = new AtomicInteger(); | ||||
| 
 | ||||
| 		@ConcurrencyLimit(limitString = "${test.concurrency.limit}") | ||||
| 		public void concurrentOperation() { | ||||
|  | @ -226,7 +278,7 @@ class ConcurrencyLimitTests { | |||
| 				throw new IllegalStateException(); | ||||
| 			} | ||||
| 			try { | ||||
| 				Thread.sleep(100); | ||||
| 				Thread.sleep(10); | ||||
| 			} | ||||
| 			catch (InterruptedException ex) { | ||||
| 				throw new IllegalStateException(ex); | ||||
|  | @ -235,4 +287,24 @@ class ConcurrencyLimitTests { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	static class ConfigurationErrorsBean { | ||||
| 
 | ||||
| 		@ConcurrencyLimit | ||||
| 		public void emptyDeclaration() { | ||||
| 		} | ||||
| 
 | ||||
| 		@ConcurrencyLimit(-42) | ||||
| 		public void negative42Int() { | ||||
| 		} | ||||
| 
 | ||||
| 		@ConcurrencyLimit(limitString = "-42") | ||||
| 		public void negative42String() { | ||||
| 		} | ||||
| 
 | ||||
| 		@ConcurrencyLimit(limitString = "B2") | ||||
| 		public void alphanumericString() { | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| } | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue