Browse Source
Moves @Retryable infrastructure to resilience package in spring-context module. Includes duration parsing and placeholder resolution for @Retryable attributes. Provides convenient @EnableResilientMethods for @Retryable + @ConcurrencyLimit. Closes gh-35133 See gh-34529pull/35151/head
23 changed files with 1112 additions and 405 deletions
@ -1,46 +0,0 @@
@@ -1,46 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.aop.retry.annotation; |
||||
|
||||
import org.springframework.aop.Pointcut; |
||||
import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor; |
||||
import org.springframework.aop.support.ComposablePointcut; |
||||
import org.springframework.aop.support.DefaultPointcutAdvisor; |
||||
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; |
||||
|
||||
/** |
||||
* A convenient {@link org.springframework.beans.factory.config.BeanPostProcessor |
||||
* BeanPostProcessor} that applies {@link RetryAnnotationInterceptor} |
||||
* to all bean methods annotated with {@link Retryable} annotations. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { |
||||
|
||||
public RetryAnnotationBeanPostProcessor() { |
||||
setBeforeExistingAdvisors(true); |
||||
|
||||
Pointcut cpc = new AnnotationMatchingPointcut(Retryable.class, true); |
||||
Pointcut mpc = new AnnotationMatchingPointcut(null, Retryable.class, true); |
||||
this.advisor = new DefaultPointcutAdvisor( |
||||
new ComposablePointcut(cpc).union(mpc), |
||||
new RetryAnnotationInterceptor()); |
||||
} |
||||
|
||||
} |
||||
@ -1,96 +0,0 @@
@@ -1,96 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.aop.retry.annotation; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.time.Duration; |
||||
import java.util.Arrays; |
||||
import java.util.Map; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.aop.retry.AbstractRetryInterceptor; |
||||
import org.springframework.aop.retry.MethodRetryPredicate; |
||||
import org.springframework.aop.retry.MethodRetrySpec; |
||||
import org.springframework.core.MethodClassKey; |
||||
import org.springframework.core.annotation.AnnotatedElementUtils; |
||||
import org.springframework.util.ReflectionUtils; |
||||
|
||||
/** |
||||
* An annotation-based retry interceptor based on {@link Retryable} annotations. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
*/ |
||||
public class RetryAnnotationInterceptor extends AbstractRetryInterceptor { |
||||
|
||||
private final Map<MethodClassKey, MethodRetrySpec> retrySpecCache = new ConcurrentHashMap<>(); |
||||
|
||||
|
||||
@Override |
||||
protected @Nullable MethodRetrySpec getRetrySpec(Method method, Class<?> targetClass) { |
||||
MethodClassKey cacheKey = new MethodClassKey(method, targetClass); |
||||
MethodRetrySpec retrySpec = this.retrySpecCache.get(cacheKey); |
||||
if (retrySpec != null) { |
||||
return retrySpec; |
||||
} |
||||
|
||||
Retryable retryable = AnnotatedElementUtils.getMergedAnnotation(method, Retryable.class); |
||||
if (retryable == null) { |
||||
retryable = AnnotatedElementUtils.getMergedAnnotation(targetClass, Retryable.class); |
||||
if (retryable == null) { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
TimeUnit timeUnit = retryable.timeUnit(); |
||||
retrySpec = new MethodRetrySpec( |
||||
Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()), |
||||
instantiatePredicate(retryable.predicate()), retryable.maxAttempts(), |
||||
toDuration(retryable.delay(), timeUnit), toDuration(retryable.jitter(), timeUnit), |
||||
retryable.multiplier(), toDuration(retryable.maxDelay(), timeUnit)); |
||||
|
||||
MethodRetrySpec existing = this.retrySpecCache.putIfAbsent(cacheKey, retrySpec); |
||||
return (existing != null ? existing : retrySpec); |
||||
} |
||||
|
||||
|
||||
private static MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPredicate> predicateClass) { |
||||
if (predicateClass == MethodRetryPredicate.class) { |
||||
return (method, throwable) -> true; |
||||
} |
||||
try { |
||||
return ReflectionUtils.accessibleConstructor(predicateClass).newInstance(); |
||||
} |
||||
catch (Throwable ex) { |
||||
throw new IllegalStateException("Failed to instantiate predicate class [" + predicateClass + "]", ex); |
||||
} |
||||
} |
||||
|
||||
private static Duration toDuration(long value, TimeUnit timeUnit) { |
||||
try { |
||||
return Duration.of(value, timeUnit.toChronoUnit()); |
||||
} |
||||
catch (Exception ex) { |
||||
throw new IllegalArgumentException( |
||||
"Unsupported unit " + timeUnit + " for value \"" + value + "\": " + ex.getMessage()); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -1,155 +0,0 @@
@@ -1,155 +0,0 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.aop.retry.annotation; |
||||
|
||||
import java.lang.annotation.Documented; |
||||
import java.lang.annotation.ElementType; |
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import org.springframework.aop.retry.MethodRetryPredicate; |
||||
import org.springframework.aot.hint.annotation.Reflective; |
||||
import org.springframework.core.annotation.AliasFor; |
||||
|
||||
/** |
||||
* A common annotation specifying retry characteristics for an individual method, |
||||
* or for all proxy-invoked methods in a given class hierarchy if annotated at |
||||
* the type level. |
||||
* |
||||
* <p>Aligned with {@link org.springframework.core.retry.RetryTemplate} |
||||
* as well as Reactor's retry support, either re-invoking an imperative |
||||
* target method or decorating a reactive result accordingly. |
||||
* |
||||
* <p>Inspired by the <a href="https://github.com/spring-projects/spring-retry">Spring Retry</a> |
||||
* project but redesigned as a minimal core retry feature in the Spring Framework. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
* @see RetryAnnotationBeanPostProcessor |
||||
* @see RetryAnnotationInterceptor |
||||
* @see org.springframework.core.retry.RetryPolicy |
||||
* @see org.springframework.core.retry.RetryTemplate |
||||
* @see reactor.core.publisher.Mono#retryWhen |
||||
* @see reactor.core.publisher.Flux#retryWhen |
||||
*/ |
||||
@Target({ElementType.TYPE, ElementType.METHOD}) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@Reflective |
||||
public @interface Retryable { |
||||
|
||||
/** |
||||
* Convenient default attribute for {@link #includes()}, |
||||
* typically used with a single exception type to retry for. |
||||
*/ |
||||
@AliasFor("includes") |
||||
Class<? extends Throwable>[] value() default {}; |
||||
|
||||
/** |
||||
* Applicable exception types to attempt a retry for. This attribute |
||||
* allows for the convenient specification of assignable exception types. |
||||
* <p>The default is empty, leading to a retry attempt for any exception. |
||||
* @see #excludes() |
||||
* @see #predicate() |
||||
*/ |
||||
@AliasFor("value") |
||||
Class<? extends Throwable>[] includes() default {}; |
||||
|
||||
/** |
||||
* Non-applicable exception types to avoid a retry for. This attribute |
||||
* allows for the convenient specification of assignable exception types. |
||||
* <p>The default is empty, leading to a retry attempt for any exception. |
||||
* @see #includes() |
||||
* @see #predicate() |
||||
*/ |
||||
Class<? extends Throwable>[] excludes() default {}; |
||||
|
||||
/** |
||||
* A predicate for filtering applicable exceptions for which |
||||
* an invocation can be retried. |
||||
* <p>The default is a retry attempt for any exception. |
||||
* @see #includes() |
||||
* @see #excludes() |
||||
*/ |
||||
Class<? extends MethodRetryPredicate> predicate() default MethodRetryPredicate.class; |
||||
|
||||
/** |
||||
* The maximum number of retry attempts, in addition to the initial invocation. |
||||
* <p>The default is 3. |
||||
*/ |
||||
long maxAttempts() default 3; |
||||
|
||||
/** |
||||
* The base delay after the initial invocation. If a multiplier is specified, |
||||
* this serves as the initial delay to multiply from. |
||||
* <p>The time unit is milliseconds by default but can be overridden via |
||||
* {@link #timeUnit}. |
||||
* <p>The default is 1000. |
||||
* @see #jitter() |
||||
* @see #multiplier() |
||||
* @see #maxDelay() |
||||
*/ |
||||
long delay() default 1000; |
||||
|
||||
/** |
||||
* A jitter value for the base retry attempt, randomly subtracted or added to |
||||
* the calculated delay, resulting in a value between {@code delay - jitter} |
||||
* and {@code delay + jitter} but never below the base {@link #delay()} or |
||||
* above {@link #maxDelay()}. If a multiplier is specified, it is applied |
||||
* to the jitter value as well. |
||||
* <p>The time unit is milliseconds by default but can be overridden via |
||||
* {@link #timeUnit}. |
||||
* <p>The default is 0 (no jitter). |
||||
* @see #delay() |
||||
* @see #multiplier() |
||||
* @see #maxDelay() |
||||
*/ |
||||
long jitter() default 0; |
||||
|
||||
/** |
||||
* A multiplier for a delay for the next retry attempt, applied |
||||
* to the previous delay (starting with {@link #delay()}) as well |
||||
* as to the applicable {@link #jitter()} for each attempt. |
||||
* <p>The default is 1.0, effectively resulting in a fixed delay. |
||||
* @see #delay() |
||||
* @see #jitter() |
||||
* @see #maxDelay() |
||||
*/ |
||||
double multiplier() default 1.0; |
||||
|
||||
/** |
||||
* The maximum delay for any retry attempt, limiting how far {@link #jitter()} |
||||
* and {@link #multiplier()} can increase the {@linkplain #delay() delay}. |
||||
* <p>The time unit is milliseconds by default but can be overridden via |
||||
* {@link #timeUnit}. |
||||
* <p>The default is unlimited. |
||||
* @see #delay() |
||||
* @see #jitter() |
||||
* @see #multiplier() |
||||
*/ |
||||
long maxDelay() default Long.MAX_VALUE; |
||||
|
||||
/** |
||||
* The {@link TimeUnit} to use for {@link #delay}, {@link #jitter}, |
||||
* and {@link #maxDelay}. |
||||
* <p>The default is {@link TimeUnit#MILLISECONDS}. |
||||
*/ |
||||
TimeUnit timeUnit() default TimeUnit.MILLISECONDS; |
||||
|
||||
} |
||||
@ -1,7 +0,0 @@
@@ -1,7 +0,0 @@
|
||||
/** |
||||
* Annotation-based retry support for common Spring setups. |
||||
*/ |
||||
@NullMarked |
||||
package org.springframework.aop.retry.annotation; |
||||
|
||||
import org.jspecify.annotations.NullMarked; |
||||
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.resilience.annotation; |
||||
|
||||
import java.lang.annotation.Documented; |
||||
import java.lang.annotation.ElementType; |
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
|
||||
import org.springframework.aot.hint.annotation.Reflective; |
||||
|
||||
/** |
||||
* A common annotation specifying a concurrency limit for an individual method, |
||||
* or for all proxy-invoked methods in a given class hierarchy if annotated at |
||||
* the type level. |
||||
* |
||||
* <p>In the type-level case, all methods inheriting the concurrency limit |
||||
* from the type level share a common concurrency throttle, with any mix |
||||
* of such method invocations contributing to the shared concurrency limit. |
||||
* Whereas for a locally annotated method, a local throttle with the specified |
||||
* limit is going to be applied to invocations of that particular method only. |
||||
* |
||||
* <p>This is particularly useful with Virtual Threads where there is generally |
||||
* no thread pool limit in place. For asynchronous tasks, this can be constrained |
||||
* on {@link org.springframework.core.task.SimpleAsyncTaskExecutor}; for |
||||
* synchronous invocations, this annotation provides equivalent behavior through |
||||
* {@link org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor}. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
* @see EnableResilientMethods |
||||
* @see ConcurrencyLimitBeanPostProcessor |
||||
* @see org.springframework.aop.interceptor.ConcurrencyThrottleInterceptor |
||||
* @see org.springframework.core.task.SimpleAsyncTaskExecutor#setConcurrencyLimit |
||||
*/ |
||||
@Target({ElementType.TYPE, ElementType.METHOD}) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@Reflective |
||||
public @interface ConcurrencyLimit { |
||||
|
||||
/** |
||||
* 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 number of concurrent invocations similar to the upper bound of a pool. |
||||
*/ |
||||
int value() default 1; |
||||
|
||||
} |
||||
@ -0,0 +1,118 @@
@@ -0,0 +1,118 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.resilience.annotation; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.util.Map; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
|
||||
import org.aopalliance.intercept.Joinpoint; |
||||
import org.aopalliance.intercept.MethodInterceptor; |
||||
import org.aopalliance.intercept.MethodInvocation; |
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.aop.Pointcut; |
||||
import org.springframework.aop.ProxyMethodInvocation; |
||||
import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor; |
||||
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.core.annotation.AnnotatedElementUtils; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.ConcurrentReferenceHashMap; |
||||
|
||||
/** |
||||
* A convenient {@link org.springframework.beans.factory.config.BeanPostProcessor |
||||
* BeanPostProcessor} that applies a concurrency interceptor to all bean methods |
||||
* annotated with {@link ConcurrencyLimit} annotations. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class ConcurrencyLimitBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor { |
||||
|
||||
public ConcurrencyLimitBeanPostProcessor() { |
||||
setBeforeExistingAdvisors(true); |
||||
|
||||
Pointcut cpc = new AnnotationMatchingPointcut(ConcurrencyLimit.class, true); |
||||
Pointcut mpc = new AnnotationMatchingPointcut(null, ConcurrencyLimit.class, true); |
||||
this.advisor = new DefaultPointcutAdvisor( |
||||
new ComposablePointcut(cpc).union(mpc), |
||||
new ConcurrencyLimitInterceptor()); |
||||
} |
||||
|
||||
|
||||
private static class ConcurrencyLimitInterceptor implements MethodInterceptor { |
||||
|
||||
private final Map<Object, ConcurrencyThrottleCache> cachePerInstance = |
||||
new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); |
||||
|
||||
@Override |
||||
public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { |
||||
Method method = invocation.getMethod(); |
||||
Object target = invocation.getThis(); |
||||
Class<?> targetClass = (target != null ? target.getClass() : method.getDeclaringClass()); |
||||
if (target == null && invocation instanceof ProxyMethodInvocation methodInvocation) { |
||||
// Allow validation for AOP proxy without a target
|
||||
target = methodInvocation.getProxy(); |
||||
} |
||||
Assert.state(target != null, "Target must not be null"); |
||||
|
||||
ConcurrencyThrottleCache cache = this.cachePerInstance.computeIfAbsent(target, |
||||
k -> new ConcurrencyThrottleCache()); |
||||
MethodInterceptor interceptor = cache.methodInterceptors.get(method); |
||||
if (interceptor == null) { |
||||
synchronized (cache) { |
||||
interceptor = cache.methodInterceptors.get(method); |
||||
if (interceptor == null) { |
||||
boolean perMethod = false; |
||||
ConcurrencyLimit limit = AnnotatedElementUtils.getMergedAnnotation(method, ConcurrencyLimit.class); |
||||
if (limit != null) { |
||||
perMethod = true; |
||||
} |
||||
else { |
||||
interceptor = cache.classInterceptor; |
||||
if (interceptor == null) { |
||||
limit = AnnotatedElementUtils.getMergedAnnotation(targetClass, ConcurrencyLimit.class); |
||||
} |
||||
} |
||||
if (interceptor == null) { |
||||
interceptor = (limit != null ? new ConcurrencyThrottleInterceptor(limit.value()) : |
||||
Joinpoint::proceed); |
||||
if (!perMethod) { |
||||
cache.classInterceptor = interceptor; |
||||
} |
||||
} |
||||
cache.methodInterceptors.put(method, interceptor); |
||||
} |
||||
} |
||||
} |
||||
return interceptor.invoke(invocation); |
||||
} |
||||
} |
||||
|
||||
|
||||
private static class ConcurrencyThrottleCache { |
||||
|
||||
final Map<Method, MethodInterceptor> methodInterceptors = new ConcurrentHashMap<>(); |
||||
|
||||
@Nullable MethodInterceptor classInterceptor; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,69 @@
@@ -0,0 +1,69 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.resilience.annotation; |
||||
|
||||
import java.lang.annotation.Documented; |
||||
import java.lang.annotation.ElementType; |
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
|
||||
import org.springframework.context.annotation.Import; |
||||
import org.springframework.core.Ordered; |
||||
|
||||
/** |
||||
* Enables Spring's core resilience features for method invocations: |
||||
* {@link Retryable} as well as {@link ConcurrencyLimit}. |
||||
* |
||||
* <p>These annotations can also be individually enabled through |
||||
* defining a {@link RetryAnnotationBeanPostProcessor} and/or a |
||||
* {@link ConcurrencyLimitBeanPostProcessor}. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
* @see RetryAnnotationBeanPostProcessor |
||||
* @see ConcurrencyLimitBeanPostProcessor |
||||
*/ |
||||
@Target(ElementType.TYPE) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@Import(ResilientMethodsConfiguration.class) |
||||
public @interface EnableResilientMethods { |
||||
|
||||
/** |
||||
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed |
||||
* to standard Java interface-based proxies. |
||||
* <p>The default is {@code false}. |
||||
* <p>Note that setting this attribute to {@code true} will affect <em>all</em> |
||||
* Spring-managed beans requiring proxying, not just those marked with {@code @Retryable} |
||||
* or {@code @ConcurrencyLimit}. For example, other beans marked with Spring's |
||||
* {@code @Transactional} annotation will be upgraded to subclass proxying at |
||||
* the same time. This approach has no negative impact in practice unless one is |
||||
* explicitly expecting one type of proxy vs. another — for example, in tests. |
||||
*/ |
||||
boolean proxyTargetClass() default false; |
||||
|
||||
/** |
||||
* Indicate the order in which the {@link RetryAnnotationBeanPostProcessor} |
||||
* and {@link ConcurrencyLimitBeanPostProcessor} should be applied. |
||||
* <p>The default is {@link Ordered#LOWEST_PRECEDENCE} in order to run |
||||
* after all other post-processors, so that it can add an advisor to |
||||
* existing proxies rather than double-proxy. |
||||
*/ |
||||
int order() default Ordered.LOWEST_PRECEDENCE; |
||||
|
||||
} |
||||
@ -0,0 +1,77 @@
@@ -0,0 +1,77 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.resilience.annotation; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.aop.framework.ProxyProcessorSupport; |
||||
import org.springframework.beans.factory.config.BeanDefinition; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.annotation.ImportAware; |
||||
import org.springframework.context.annotation.Role; |
||||
import org.springframework.core.annotation.AnnotationAttributes; |
||||
import org.springframework.core.type.AnnotationMetadata; |
||||
|
||||
/** |
||||
* {@code @Configuration} class that registers the Spring infrastructure beans necessary |
||||
* to enable proxy-based method invocations with retry and concurrency limit behavior. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
* @see EnableResilientMethods |
||||
* @see RetryAnnotationBeanPostProcessor |
||||
* @see ConcurrencyLimitBeanPostProcessor |
||||
*/ |
||||
@Configuration(proxyBeanMethods = false) |
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
||||
public class ResilientMethodsConfiguration implements ImportAware { |
||||
|
||||
private @Nullable AnnotationAttributes enableResilientMethods; |
||||
|
||||
|
||||
@Override |
||||
public void setImportMetadata(AnnotationMetadata importMetadata) { |
||||
this.enableResilientMethods = AnnotationAttributes.fromMap( |
||||
importMetadata.getAnnotationAttributes(EnableResilientMethods.class.getName())); |
||||
} |
||||
|
||||
private void configureProxySupport(ProxyProcessorSupport proxySupport) { |
||||
if (this.enableResilientMethods != null) { |
||||
proxySupport.setProxyTargetClass(this.enableResilientMethods.getBoolean("proxyTargetClass")); |
||||
proxySupport.setOrder(this.enableResilientMethods.<Integer>getNumber("order")); |
||||
} |
||||
} |
||||
|
||||
|
||||
@Bean(name = "org.springframework.resilience.annotation.internalRetryAnnotationProcessor") |
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
||||
public RetryAnnotationBeanPostProcessor retryAdvisor() { |
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor(); |
||||
configureProxySupport(bpp); |
||||
return bpp; |
||||
} |
||||
|
||||
@Bean(name = "org.springframework.resilience.annotation.internalConcurrencyLimitProcessor") |
||||
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) |
||||
public ConcurrencyLimitBeanPostProcessor concurrencyLimitAdvisor() { |
||||
ConcurrencyLimitBeanPostProcessor bpp = new ConcurrencyLimitBeanPostProcessor(); |
||||
configureProxySupport(bpp); |
||||
return bpp; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,170 @@
@@ -0,0 +1,170 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.resilience.annotation; |
||||
|
||||
import java.lang.reflect.Method; |
||||
import java.time.Duration; |
||||
import java.util.Arrays; |
||||
import java.util.Map; |
||||
import java.util.concurrent.ConcurrentHashMap; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
|
||||
import org.springframework.aop.Pointcut; |
||||
import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor; |
||||
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.MethodClassKey; |
||||
import org.springframework.core.annotation.AnnotatedElementUtils; |
||||
import org.springframework.format.annotation.DurationFormat; |
||||
import org.springframework.format.datetime.standard.DurationFormatterUtils; |
||||
import org.springframework.resilience.retry.AbstractRetryInterceptor; |
||||
import org.springframework.resilience.retry.MethodRetryPredicate; |
||||
import org.springframework.resilience.retry.MethodRetrySpec; |
||||
import org.springframework.util.ReflectionUtils; |
||||
import org.springframework.util.StringUtils; |
||||
import org.springframework.util.StringValueResolver; |
||||
|
||||
/** |
||||
* A convenient {@link org.springframework.beans.factory.config.BeanPostProcessor |
||||
* BeanPostProcessor} that applies a retry interceptor to all bean methods |
||||
* annotated with {@link Retryable} annotations. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
*/ |
||||
@SuppressWarnings("serial") |
||||
public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor |
||||
implements EmbeddedValueResolverAware { |
||||
|
||||
private @Nullable StringValueResolver embeddedValueResolver; |
||||
|
||||
|
||||
public RetryAnnotationBeanPostProcessor() { |
||||
setBeforeExistingAdvisors(true); |
||||
|
||||
Pointcut cpc = new AnnotationMatchingPointcut(Retryable.class, true); |
||||
Pointcut mpc = new AnnotationMatchingPointcut(null, Retryable.class, true); |
||||
this.advisor = new DefaultPointcutAdvisor( |
||||
new ComposablePointcut(cpc).union(mpc), |
||||
new RetryAnnotationInterceptor()); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public void setEmbeddedValueResolver(StringValueResolver resolver) { |
||||
this.embeddedValueResolver = resolver; |
||||
} |
||||
|
||||
|
||||
private class RetryAnnotationInterceptor extends AbstractRetryInterceptor { |
||||
|
||||
private final Map<MethodClassKey, MethodRetrySpec> retrySpecCache = new ConcurrentHashMap<>(); |
||||
|
||||
@Override |
||||
protected @Nullable MethodRetrySpec getRetrySpec(Method method, Class<?> targetClass) { |
||||
MethodClassKey cacheKey = new MethodClassKey(method, targetClass); |
||||
MethodRetrySpec retrySpec = this.retrySpecCache.get(cacheKey); |
||||
if (retrySpec != null) { |
||||
return retrySpec; |
||||
} |
||||
|
||||
Retryable retryable = AnnotatedElementUtils.getMergedAnnotation(method, Retryable.class); |
||||
if (retryable == null) { |
||||
retryable = AnnotatedElementUtils.getMergedAnnotation(targetClass, Retryable.class); |
||||
if (retryable == null) { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
TimeUnit timeUnit = retryable.timeUnit(); |
||||
retrySpec = new MethodRetrySpec( |
||||
Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()), |
||||
instantiatePredicate(retryable.predicate()), |
||||
parseLong(retryable.maxAttempts(), retryable.maxAttemptsString()), |
||||
parseDuration(retryable.delay(), retryable.delayString(), timeUnit), |
||||
parseDuration(retryable.jitter(), retryable.jitterString(), timeUnit), |
||||
parseDouble(retryable.multiplier(), retryable.multiplierString()), |
||||
parseDuration(retryable.maxDelay(), retryable.maxDelayString(), timeUnit)); |
||||
|
||||
MethodRetrySpec existing = this.retrySpecCache.putIfAbsent(cacheKey, retrySpec); |
||||
return (existing != null ? existing : retrySpec); |
||||
} |
||||
|
||||
private MethodRetryPredicate instantiatePredicate(Class<? extends MethodRetryPredicate> predicateClass) { |
||||
if (predicateClass == MethodRetryPredicate.class) { |
||||
return (method, throwable) -> true; |
||||
} |
||||
try { |
||||
return (beanFactory != null ? beanFactory.createBean(predicateClass) : |
||||
ReflectionUtils.accessibleConstructor(predicateClass).newInstance()); |
||||
} |
||||
catch (Throwable ex) { |
||||
throw new IllegalStateException("Failed to instantiate predicate class [" + predicateClass + "]", ex); |
||||
} |
||||
} |
||||
|
||||
private long parseLong(long value, String stringValue) { |
||||
if (StringUtils.hasText(stringValue)) { |
||||
if (embeddedValueResolver != null) { |
||||
stringValue = embeddedValueResolver.resolveStringValue(stringValue); |
||||
} |
||||
if (StringUtils.hasText(stringValue)) { |
||||
return Long.parseLong(stringValue); |
||||
} |
||||
} |
||||
return value; |
||||
} |
||||
|
||||
private double parseDouble(double value, String stringValue) { |
||||
if (StringUtils.hasText(stringValue)) { |
||||
if (embeddedValueResolver != null) { |
||||
stringValue = embeddedValueResolver.resolveStringValue(stringValue); |
||||
} |
||||
if (StringUtils.hasText(stringValue)) { |
||||
return Double.parseDouble(stringValue); |
||||
} |
||||
} |
||||
return value; |
||||
} |
||||
|
||||
private Duration parseDuration(long value, String stringValue, TimeUnit timeUnit) { |
||||
if (StringUtils.hasText(stringValue)) { |
||||
if (embeddedValueResolver != null) { |
||||
stringValue = embeddedValueResolver.resolveStringValue(stringValue); |
||||
} |
||||
if (StringUtils.hasText(stringValue)) { |
||||
return toDuration(stringValue, timeUnit); |
||||
} |
||||
} |
||||
return toDuration(value, timeUnit); |
||||
} |
||||
|
||||
private static Duration toDuration(long value, TimeUnit timeUnit) { |
||||
return Duration.of(value, timeUnit.toChronoUnit()); |
||||
} |
||||
|
||||
private static Duration toDuration(String value, TimeUnit timeUnit) { |
||||
DurationFormat.Unit unit = DurationFormat.Unit.fromChronoUnit(timeUnit.toChronoUnit()); |
||||
return DurationFormatterUtils.detectAndParse(value, unit); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,241 @@
@@ -0,0 +1,241 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.resilience.annotation; |
||||
|
||||
import java.lang.annotation.Documented; |
||||
import java.lang.annotation.ElementType; |
||||
import java.lang.annotation.Retention; |
||||
import java.lang.annotation.RetentionPolicy; |
||||
import java.lang.annotation.Target; |
||||
import java.util.concurrent.TimeUnit; |
||||
|
||||
import org.springframework.aot.hint.annotation.Reflective; |
||||
import org.springframework.core.annotation.AliasFor; |
||||
import org.springframework.resilience.retry.MethodRetryPredicate; |
||||
|
||||
/** |
||||
* A common annotation specifying retry characteristics for an individual method, |
||||
* or for all proxy-invoked methods in a given class hierarchy if annotated at |
||||
* the type level. |
||||
* |
||||
* <p>Aligned with {@link org.springframework.core.retry.RetryTemplate} |
||||
* as well as Reactor's retry support, either re-invoking an imperative |
||||
* target method or decorating a reactive result accordingly. |
||||
* |
||||
* <p>Inspired by the <a href="https://github.com/spring-projects/spring-retry">Spring Retry</a> |
||||
* project but redesigned as a minimal core retry feature in the Spring Framework. |
||||
* |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
* @see EnableResilientMethods |
||||
* @see RetryAnnotationBeanPostProcessor |
||||
* @see org.springframework.core.retry.RetryPolicy |
||||
* @see org.springframework.core.retry.RetryTemplate |
||||
* @see reactor.core.publisher.Mono#retryWhen |
||||
* @see reactor.core.publisher.Flux#retryWhen |
||||
*/ |
||||
@Target({ElementType.TYPE, ElementType.METHOD}) |
||||
@Retention(RetentionPolicy.RUNTIME) |
||||
@Documented |
||||
@Reflective |
||||
public @interface Retryable { |
||||
|
||||
/** |
||||
* Convenient default attribute for {@link #includes()}, |
||||
* typically used with a single exception type to retry for. |
||||
*/ |
||||
@AliasFor("includes") |
||||
Class<? extends Throwable>[] value() default {}; |
||||
|
||||
/** |
||||
* Applicable exception types to attempt a retry for. This attribute |
||||
* allows for the convenient specification of assignable exception types. |
||||
* <p>The default is empty, leading to a retry attempt for any exception. |
||||
* @see #excludes() |
||||
* @see #predicate() |
||||
*/ |
||||
@AliasFor("value") |
||||
Class<? extends Throwable>[] includes() default {}; |
||||
|
||||
/** |
||||
* Non-applicable exception types to avoid a retry for. This attribute |
||||
* allows for the convenient specification of assignable exception types. |
||||
* <p>The default is empty, leading to a retry attempt for any exception. |
||||
* @see #includes() |
||||
* @see #predicate() |
||||
*/ |
||||
Class<? extends Throwable>[] excludes() default {}; |
||||
|
||||
/** |
||||
* A predicate for filtering applicable exceptions for which |
||||
* an invocation can be retried. |
||||
* <p>The default is a retry attempt for any exception. |
||||
* <p>A specified {@link MethodRetryPredicate} implementation will be instantiated |
||||
* per method. It can use dependency injection at the constructor level or through |
||||
* autowiring annotations, in case it needs access to other beans or facilities. |
||||
* @see #includes() |
||||
* @see #excludes() |
||||
*/ |
||||
Class<? extends MethodRetryPredicate> predicate() default MethodRetryPredicate.class; |
||||
|
||||
/** |
||||
* The maximum number of retry attempts, in addition to the initial invocation. |
||||
* <p>The default is 3. |
||||
*/ |
||||
long maxAttempts() default 3; |
||||
|
||||
/** |
||||
* The maximum number of retry attempts, as a configurable String. |
||||
* A non-empty value specified here overrides the {@link #maxAttempts()} attribute. |
||||
* <p>This supports Spring-style "${...}" placeholders as well as SpEL expressions. |
||||
* @see #maxAttempts() |
||||
*/ |
||||
String maxAttemptsString() default ""; |
||||
|
||||
/** |
||||
* The base delay after the initial invocation. If a multiplier is specified, |
||||
* this serves as the initial delay to multiply from. |
||||
* <p>The time unit is milliseconds by default but can be overridden via |
||||
* {@link #timeUnit}. |
||||
* <p>The default is 1000. |
||||
* @see #jitter() |
||||
* @see #multiplier() |
||||
* @see #maxDelay() |
||||
*/ |
||||
long delay() default 1000; |
||||
|
||||
/** |
||||
* The base delay after the initial invocation, as a duration String. |
||||
* A non-empty value specified here overrides the {@link #delay()} attribute. |
||||
* <p>The duration String can be in several formats: |
||||
* <ul> |
||||
* <li>a plain integer — which is interpreted to represent a duration in |
||||
* milliseconds by default unless overridden via {@link #timeUnit()} (prefer |
||||
* using {@link #delay()} in that case)</li> |
||||
* <li>any of the known {@link org.springframework.format.annotation.DurationFormat.Style |
||||
* DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601} |
||||
* style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style |
||||
* — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit</li> |
||||
* <li>one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions</li> |
||||
* </ul> |
||||
* @return the initial delay as a String value — for example a placeholder, |
||||
* or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value |
||||
* or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value |
||||
* @see #delay() |
||||
*/ |
||||
String delayString() default ""; |
||||
|
||||
/** |
||||
* A jitter value for the base retry attempt, randomly subtracted or added to |
||||
* the calculated delay, resulting in a value between {@code delay - jitter} |
||||
* and {@code delay + jitter} but never below the base {@link #delay()} or |
||||
* above {@link #maxDelay()}. If a multiplier is specified, it is applied |
||||
* to the jitter value as well. |
||||
* <p>The time unit is milliseconds by default but can be overridden via |
||||
* {@link #timeUnit}. |
||||
* <p>The default is 0 (no jitter). |
||||
* @see #delay() |
||||
* @see #multiplier() |
||||
* @see #maxDelay() |
||||
*/ |
||||
long jitter() default 0; |
||||
|
||||
/** |
||||
* A jitter value for the base retry attempt, as a duration String. |
||||
* A non-empty value specified here overrides the {@link #jitter()} attribute. |
||||
* <p>The duration String can be in several formats: |
||||
* <ul> |
||||
* <li>a plain integer — which is interpreted to represent a duration in |
||||
* milliseconds by default unless overridden via {@link #timeUnit()} (prefer |
||||
* using {@link #jitter()} in that case)</li> |
||||
* <li>any of the known {@link org.springframework.format.annotation.DurationFormat.Style |
||||
* DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601} |
||||
* style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style |
||||
* — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit</li> |
||||
* <li>one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions</li> |
||||
* </ul> |
||||
* @return the initial delay as a String value — for example a placeholder, |
||||
* or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value |
||||
* or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value |
||||
* @see #jitter() |
||||
*/ |
||||
String jitterString() default ""; |
||||
|
||||
/** |
||||
* A multiplier for a delay for the next retry attempt, applied |
||||
* to the previous delay (starting with {@link #delay()}) as well |
||||
* as to the applicable {@link #jitter()} for each attempt. |
||||
* <p>The default is 1.0, effectively resulting in a fixed delay. |
||||
* @see #delay() |
||||
* @see #jitter() |
||||
* @see #maxDelay() |
||||
*/ |
||||
double multiplier() default 1.0; |
||||
|
||||
/** |
||||
* A multiplier for a delay for the next retry attempt, as a configurable String. |
||||
* A non-empty value specified here overrides the {@link #multiplier()} attribute. |
||||
* <p>This supports Spring-style "${...}" placeholders as well as SpEL expressions. |
||||
* @see #multiplier() |
||||
*/ |
||||
String multiplierString() default ""; |
||||
|
||||
/** |
||||
* The maximum delay for any retry attempt, limiting how far {@link #jitter()} |
||||
* and {@link #multiplier()} can increase the {@linkplain #delay() delay}. |
||||
* <p>The time unit is milliseconds by default but can be overridden via |
||||
* {@link #timeUnit}. |
||||
* <p>The default is unlimited. |
||||
* @see #delay() |
||||
* @see #jitter() |
||||
* @see #multiplier() |
||||
*/ |
||||
long maxDelay() default Long.MAX_VALUE; |
||||
|
||||
/** |
||||
* The maximum delay for any retry attempt, as a duration String. |
||||
* A non-empty value specified here overrides the {@link #maxDelay()} attribute. |
||||
* <p>The duration String can be in several formats: |
||||
* <ul> |
||||
* <li>a plain integer — which is interpreted to represent a duration in |
||||
* milliseconds by default unless overridden via {@link #timeUnit()} (prefer |
||||
* using {@link #maxDelay()} in that case)</li> |
||||
* <li>any of the known {@link org.springframework.format.annotation.DurationFormat.Style |
||||
* DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601} |
||||
* style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style |
||||
* — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit</li> |
||||
* <li>one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions</li> |
||||
* </ul> |
||||
* @return the initial delay as a String value — for example a placeholder, |
||||
* or a {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value |
||||
* or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value |
||||
* @see #maxDelay() |
||||
*/ |
||||
String maxDelayString() default ""; |
||||
|
||||
/** |
||||
* The {@link TimeUnit} to use for {@link #delay}, {@link #delayString}, |
||||
* {@link #jitter}, {@link #jitterString}, {@link #maxDelay}, and |
||||
* {@link #maxDelayString}. |
||||
* <p>The default is {@link TimeUnit#MILLISECONDS}. |
||||
* <p>This attribute is ignored for {@link java.time.Duration} values supplied |
||||
* via {@link #delayString}, {@link #jitterString}, or {@link #maxDelayString}. |
||||
* @return the {@code TimeUnit} to use |
||||
*/ |
||||
TimeUnit timeUnit() default TimeUnit.MILLISECONDS; |
||||
|
||||
} |
||||
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
/** |
||||
* Annotation-based retry and concurrency limit support. |
||||
*/ |
||||
@NullMarked |
||||
package org.springframework.resilience.annotation; |
||||
|
||||
import org.jspecify.annotations.NullMarked; |
||||
@ -0,0 +1,188 @@
@@ -0,0 +1,188 @@
|
||||
/* |
||||
* Copyright 2002-present the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.resilience; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.List; |
||||
import java.util.concurrent.CompletableFuture; |
||||
import java.util.concurrent.atomic.AtomicInteger; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.aop.framework.AopProxyUtils; |
||||
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.resilience.annotation.ConcurrencyLimit; |
||||
import org.springframework.resilience.annotation.ConcurrencyLimitBeanPostProcessor; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* @author Juergen Hoeller |
||||
* @since 7.0 |
||||
*/ |
||||
public class ConcurrencyLimitTests { |
||||
|
||||
@Test |
||||
void withSimpleInterceptor() { |
||||
NonAnnotatedBean target = new NonAnnotatedBean(); |
||||
ProxyFactory pf = new ProxyFactory(); |
||||
pf.setTarget(target); |
||||
pf.addAdvice(new ConcurrencyThrottleInterceptor(2)); |
||||
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy(); |
||||
|
||||
List<CompletableFuture<?>> 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.counter).hasValue(0); |
||||
} |
||||
|
||||
@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 target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy); |
||||
|
||||
List<CompletableFuture<?>> 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); |
||||
} |
||||
|
||||
@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 target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy); |
||||
|
||||
List<CompletableFuture<?>> futures = new ArrayList<>(30); |
||||
for (int i = 0; i < 10; i++) { |
||||
futures.add(CompletableFuture.runAsync(proxy::concurrentOperation)); |
||||
} |
||||
for (int i = 0; i < 10; i++) { |
||||
futures.add(CompletableFuture.runAsync(proxy::otherOperation)); |
||||
} |
||||
for (int i = 0; i < 10; i++) { |
||||
futures.add(CompletableFuture.runAsync(proxy::overrideOperation)); |
||||
} |
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); |
||||
assertThat(target.current).hasValue(0); |
||||
} |
||||
|
||||
|
||||
public static class NonAnnotatedBean { |
||||
|
||||
AtomicInteger counter = new AtomicInteger(); |
||||
|
||||
public void concurrentOperation() { |
||||
if (counter.incrementAndGet() > 2) { |
||||
throw new IllegalStateException(); |
||||
} |
||||
try { |
||||
Thread.sleep(100); |
||||
} |
||||
catch (InterruptedException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
counter.decrementAndGet(); |
||||
} |
||||
} |
||||
|
||||
|
||||
public static class AnnotatedMethodBean { |
||||
|
||||
AtomicInteger current = new AtomicInteger(); |
||||
|
||||
@ConcurrencyLimit(2) |
||||
public void concurrentOperation() { |
||||
if (current.incrementAndGet() > 2) { |
||||
throw new IllegalStateException(); |
||||
} |
||||
try { |
||||
Thread.sleep(100); |
||||
} |
||||
catch (InterruptedException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
current.decrementAndGet(); |
||||
} |
||||
} |
||||
|
||||
|
||||
@ConcurrencyLimit(2) |
||||
public static class AnnotatedClassBean { |
||||
|
||||
AtomicInteger current = new AtomicInteger(); |
||||
|
||||
AtomicInteger currentOverride = new AtomicInteger(); |
||||
|
||||
public void concurrentOperation() { |
||||
if (current.incrementAndGet() > 2) { |
||||
throw new IllegalStateException(); |
||||
} |
||||
try { |
||||
Thread.sleep(100); |
||||
} |
||||
catch (InterruptedException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
current.decrementAndGet(); |
||||
} |
||||
|
||||
public void otherOperation() { |
||||
if (current.incrementAndGet() > 2) { |
||||
throw new IllegalStateException(); |
||||
} |
||||
try { |
||||
Thread.sleep(100); |
||||
} |
||||
catch (InterruptedException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
current.decrementAndGet(); |
||||
} |
||||
|
||||
@ConcurrencyLimit(1) |
||||
public void overrideOperation() { |
||||
if (currentOverride.incrementAndGet() > 1) { |
||||
throw new IllegalStateException(); |
||||
} |
||||
try { |
||||
Thread.sleep(100); |
||||
} |
||||
catch (InterruptedException ex) { |
||||
throw new IllegalStateException(ex); |
||||
} |
||||
currentOverride.decrementAndGet(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue