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() | 	 * @see #limitString() | ||||||
| 	 */ | 	 */ | ||||||
| 	@AliasFor("limit") | 	@AliasFor("limit") | ||||||
| 	int value() default 1; | 	int value() default Integer.MIN_VALUE; | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * The applicable concurrency limit: 1 by default, | 	 * The concurrency limit. | ||||||
| 	 * effectively locking the target instance for each method invocation. | 	 * <p>Specify {@code 1} to effectively lock the target instance for each method | ||||||
| 	 * <p>Specify a limit higher than 1 for pool-like throttling, constraining | 	 * 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. | 	 * the number of concurrent invocations similar to the upper bound of a pool. | ||||||
|  | 	 * <p>Specify {@code -1} for unbounded concurrency. | ||||||
| 	 * @see #value() | 	 * @see #value() | ||||||
| 	 * @see #limitString() | 	 * @see #limitString() | ||||||
|  | 	 * @see org.springframework.util.ConcurrencyThrottleSupport#UNBOUNDED_CONCURRENCY | ||||||
| 	 */ | 	 */ | ||||||
| 	@AliasFor("value") | 	@AliasFor("value") | ||||||
| 	int limit() default 1; | 	int limit() default Integer.MIN_VALUE; | ||||||
| 
 | 
 | ||||||
| 	/** | 	/** | ||||||
| 	 * The concurrency limit, as a configurable String. | 	 * The concurrency limit, as a configurable String. | ||||||
| 	 * <p>A non-empty value specified here overrides the {@link #limit()} (or | 	 * <p>A non-empty value specified here overrides the {@link #limit()} and | ||||||
| 	 * {@link #value()}) attribute. | 	 * {@link #value()} attributes. | ||||||
| 	 * <p>This supports Spring-style "${...}" placeholders as well as SpEL expressions. | 	 * <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 #limit() | ||||||
|  | 	 * @see org.springframework.util.ConcurrencyThrottleSupport#UNBOUNDED_CONCURRENCY | ||||||
| 	 */ | 	 */ | ||||||
| 	String limitString() default ""; | 	String limitString() default ""; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -108,6 +108,9 @@ public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareA | ||||||
| 						if (interceptor == null) { | 						if (interceptor == null) { | ||||||
| 							Assert.state(annotation != null, "No @ConcurrencyLimit annotation found"); | 							Assert.state(annotation != null, "No @ConcurrencyLimit annotation found"); | ||||||
| 							int concurrencyLimit = parseInt(annotation.limit(), annotation.limitString()); | 							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); | 							interceptor = new ConcurrencyThrottleInterceptor(concurrencyLimit); | ||||||
| 							if (!perMethod) { | 							if (!perMethod) { | ||||||
| 								cache.classInterceptor = interceptor; | 								cache.classInterceptor = interceptor; | ||||||
|  |  | ||||||
|  | @ -35,10 +35,13 @@ import org.springframework.resilience.annotation.ConcurrencyLimitBeanPostProcess | ||||||
| import org.springframework.resilience.annotation.EnableResilientMethods; | import org.springframework.resilience.annotation.EnableResilientMethods; | ||||||
| 
 | 
 | ||||||
| import static org.assertj.core.api.Assertions.assertThat; | 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 Juergen Hoeller | ||||||
|  * @author Hyunsang Han |  * @author Hyunsang Han | ||||||
|  |  * @author Sam Brannen | ||||||
|  * @since 7.0 |  * @since 7.0 | ||||||
|  */ |  */ | ||||||
| class ConcurrencyLimitTests { | class ConcurrencyLimitTests { | ||||||
|  | @ -61,12 +64,7 @@ class ConcurrencyLimitTests { | ||||||
| 
 | 
 | ||||||
| 	@Test | 	@Test | ||||||
| 	void withPostProcessorForMethod() { | 	void withPostProcessorForMethod() { | ||||||
| 		DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); | 		AnnotatedMethodBean proxy = createProxy(AnnotatedMethodBean.class); | ||||||
| 		bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class)); |  | ||||||
| 		ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor(); |  | ||||||
| 		bpp.setBeanFactory(bf); |  | ||||||
| 		bf.addBeanPostProcessor(bpp); |  | ||||||
| 		AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class); |  | ||||||
| 		AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy); | 		AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy); | ||||||
| 
 | 
 | ||||||
| 		List<CompletableFuture<?>> futures = new ArrayList<>(10); | 		List<CompletableFuture<?>> futures = new ArrayList<>(10); | ||||||
|  | @ -77,14 +75,22 @@ class ConcurrencyLimitTests { | ||||||
| 		assertThat(target.current).hasValue(0); | 		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 | 	@Test | ||||||
| 	void withPostProcessorForClass() { | 	void withPostProcessorForClass() { | ||||||
| 		DefaultListableBeanFactory bf = new DefaultListableBeanFactory(); | 		AnnotatedClassBean proxy = createProxy(AnnotatedClassBean.class); | ||||||
| 		bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class)); |  | ||||||
| 		ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor(); |  | ||||||
| 		bpp.setBeanFactory(bf); |  | ||||||
| 		bf.addBeanPostProcessor(bpp); |  | ||||||
| 		AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class); |  | ||||||
| 		AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy); | 		AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy); | ||||||
| 
 | 
 | ||||||
| 		List<CompletableFuture<?>> futures = new ArrayList<>(30); | 		List<CompletableFuture<?>> futures = new ArrayList<>(30); | ||||||
|  | @ -122,17 +128,52 @@ class ConcurrencyLimitTests { | ||||||
| 		ctx.close(); | 		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 { | 	static class NonAnnotatedBean { | ||||||
| 
 | 
 | ||||||
| 		AtomicInteger counter = new AtomicInteger(); | 		final AtomicInteger counter = new AtomicInteger(); | ||||||
| 
 | 
 | ||||||
| 		public void concurrentOperation() { | 		public void concurrentOperation() { | ||||||
| 			if (counter.incrementAndGet() > 2) { | 			if (counter.incrementAndGet() > 2) { | ||||||
| 				throw new IllegalStateException(); | 				throw new IllegalStateException(); | ||||||
| 			} | 			} | ||||||
| 			try { | 			try { | ||||||
| 				Thread.sleep(100); | 				Thread.sleep(10); | ||||||
| 			} | 			} | ||||||
| 			catch (InterruptedException ex) { | 			catch (InterruptedException ex) { | ||||||
| 				throw new IllegalStateException(ex); | 				throw new IllegalStateException(ex); | ||||||
|  | @ -144,7 +185,7 @@ class ConcurrencyLimitTests { | ||||||
| 
 | 
 | ||||||
| 	static class AnnotatedMethodBean { | 	static class AnnotatedMethodBean { | ||||||
| 
 | 
 | ||||||
| 		AtomicInteger current = new AtomicInteger(); | 		final AtomicInteger current = new AtomicInteger(); | ||||||
| 
 | 
 | ||||||
| 		@ConcurrencyLimit(2) | 		@ConcurrencyLimit(2) | ||||||
| 		public void concurrentOperation() { | 		public void concurrentOperation() { | ||||||
|  | @ -152,29 +193,40 @@ class ConcurrencyLimitTests { | ||||||
| 				throw new IllegalStateException(); | 				throw new IllegalStateException(); | ||||||
| 			} | 			} | ||||||
| 			try { | 			try { | ||||||
| 				Thread.sleep(100); | 				Thread.sleep(10); | ||||||
| 			} | 			} | ||||||
| 			catch (InterruptedException ex) { | 			catch (InterruptedException ex) { | ||||||
| 				throw new IllegalStateException(ex); | 				throw new IllegalStateException(ex); | ||||||
| 			} | 			} | ||||||
| 			current.decrementAndGet(); | 			current.decrementAndGet(); | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		@ConcurrencyLimit(limit = -1) | ||||||
|  | 		public void unboundedConcurrency() { | ||||||
|  | 			current.incrementAndGet(); | ||||||
|  | 			try { | ||||||
|  | 				Thread.sleep(10); | ||||||
|  | 			} | ||||||
|  | 			catch (InterruptedException ex) { | ||||||
|  | 				throw new IllegalStateException(ex); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 	@ConcurrencyLimit(2) | 	@ConcurrencyLimit(2) | ||||||
| 	static class AnnotatedClassBean { | 	static class AnnotatedClassBean { | ||||||
| 
 | 
 | ||||||
| 		AtomicInteger current = new AtomicInteger(); | 		final AtomicInteger current = new AtomicInteger(); | ||||||
| 
 | 
 | ||||||
| 		AtomicInteger currentOverride = new AtomicInteger(); | 		final AtomicInteger currentOverride = new AtomicInteger(); | ||||||
| 
 | 
 | ||||||
| 		public void concurrentOperation() { | 		public void concurrentOperation() { | ||||||
| 			if (current.incrementAndGet() > 2) { | 			if (current.incrementAndGet() > 2) { | ||||||
| 				throw new IllegalStateException(); | 				throw new IllegalStateException(); | ||||||
| 			} | 			} | ||||||
| 			try { | 			try { | ||||||
| 				Thread.sleep(100); | 				Thread.sleep(10); | ||||||
| 			} | 			} | ||||||
| 			catch (InterruptedException ex) { | 			catch (InterruptedException ex) { | ||||||
| 				throw new IllegalStateException(ex); | 				throw new IllegalStateException(ex); | ||||||
|  | @ -187,7 +239,7 @@ class ConcurrencyLimitTests { | ||||||
| 				throw new IllegalStateException(); | 				throw new IllegalStateException(); | ||||||
| 			} | 			} | ||||||
| 			try { | 			try { | ||||||
| 				Thread.sleep(100); | 				Thread.sleep(10); | ||||||
| 			} | 			} | ||||||
| 			catch (InterruptedException ex) { | 			catch (InterruptedException ex) { | ||||||
| 				throw new IllegalStateException(ex); | 				throw new IllegalStateException(ex); | ||||||
|  | @ -201,7 +253,7 @@ class ConcurrencyLimitTests { | ||||||
| 				throw new IllegalStateException(); | 				throw new IllegalStateException(); | ||||||
| 			} | 			} | ||||||
| 			try { | 			try { | ||||||
| 				Thread.sleep(100); | 				Thread.sleep(10); | ||||||
| 			} | 			} | ||||||
| 			catch (InterruptedException ex) { | 			catch (InterruptedException ex) { | ||||||
| 				throw new IllegalStateException(ex); | 				throw new IllegalStateException(ex); | ||||||
|  | @ -218,7 +270,7 @@ class ConcurrencyLimitTests { | ||||||
| 
 | 
 | ||||||
| 	static class PlaceholderBean { | 	static class PlaceholderBean { | ||||||
| 
 | 
 | ||||||
| 		AtomicInteger current = new AtomicInteger(); | 		final AtomicInteger current = new AtomicInteger(); | ||||||
| 
 | 
 | ||||||
| 		@ConcurrencyLimit(limitString = "${test.concurrency.limit}") | 		@ConcurrencyLimit(limitString = "${test.concurrency.limit}") | ||||||
| 		public void concurrentOperation() { | 		public void concurrentOperation() { | ||||||
|  | @ -226,7 +278,7 @@ class ConcurrencyLimitTests { | ||||||
| 				throw new IllegalStateException(); | 				throw new IllegalStateException(); | ||||||
| 			} | 			} | ||||||
| 			try { | 			try { | ||||||
| 				Thread.sleep(100); | 				Thread.sleep(10); | ||||||
| 			} | 			} | ||||||
| 			catch (InterruptedException ex) { | 			catch (InterruptedException ex) { | ||||||
| 				throw new IllegalStateException(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