Why Skip Your Own Rate Limiter? Using Spring Boot’s Built‑in ConcurrencyThrottleInterceptor
The article explains how Spring Boot 3.5 provides the ConcurrencyThrottleInterceptor for limiting concurrent method calls, demonstrates basic configuration and execution, reveals that all intercepted methods share a single limit, and proposes two fixes—per‑pointcut advisors or a BeanPostProcessor with a custom @ConcurrencyLimit annotation—before recommending dedicated libraries such as Bucket4j or Resilience4j for business‑level throttling.
1. Introduction
Spring Boot 3.5 provides ConcurrencyThrottleInterceptor, a MethodInterceptor that blocks method invocations when a configured concurrency limit is reached.
2. Basic usage and configuration
Define the interceptor as a bean, set setConcurrencyLimit(2), and combine it with an AspectJExpressionPointcut in a DefaultPointcutAdvisor:
@Configuration
public class ConcurrentConfig {
@Bean
ConcurrencyThrottleInterceptor throttleInterceptor() {
ConcurrencyThrottleInterceptor interceptor = new ConcurrencyThrottleInterceptor();
interceptor.setConcurrencyLimit(2);
return interceptor;
}
@Bean
DefaultPointcutAdvisor concurrentAdvisor() {
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor();
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(* com.pack..*.*(..))");
advisor.setPointcut(pointcut);
advisor.setAdvice(throttleInterceptor());
return advisor;
}
}Service method query() sleeps 2 seconds and prints thread name and timestamp. A test creates five threads invoking query(). Output shows at most two threads run concurrently, matching the limit.
3. Problem: shared limit across methods
When multiple methods (e.g., query() and batch()) are intercepted by the same ConcurrencyThrottleInterceptor, they share a single concurrency counter. A test that runs both methods concurrently produces interleaved output, indicating competition for the same two‑slot limit.
4. Solution 1 – Separate advisors per pointcut
Create a distinct DefaultPointcutAdvisor for each method, each with its own ConcurrencyThrottleInterceptor and limit:
@Bean
DefaultPointcutAdvisor concurrentAdvisorQuery() {
AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
pc.setExpression("execution(* com.pack.concurrent.service.UserService.query(..))");
ConcurrencyThrottleInterceptor advice = new ConcurrencyThrottleInterceptor();
advice.setConcurrencyLimit(2);
return new DefaultPointcutAdvisor(pc, advice);
}
@Bean
DefaultPointcutAdvisor concurrentAdvisorBatch() {
AspectJExpressionPointcut pc = new AspectJExpressionPointcut();
pc.setExpression("execution(* com.pack.concurrent.service.UserService.batch(..))");
ConcurrencyThrottleInterceptor advice = new ConcurrencyThrottleInterceptor();
advice.setConcurrencyLimit(3);
return new DefaultPointcutAdvisor(pc, advice);
}Execution shows each method respecting its own limit, but configuring many advisors becomes cumbersome.
5. Solution 2 – BeanPostProcessor with custom annotation
Define annotation @ConcurrencyLimit to declare a limit on a method:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ConcurrencyLimit {
int value(); // concurrency limit
}Implement a BeanPostProcessor that scans beans for methods annotated with @ConcurrencyLimit, creates a dedicated ConcurrencyThrottleInterceptor for each, and attaches it via a dynamically built Advisor:
@Component
public class ConcurrencyBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> targetClass = AopUtils.getTargetClass(bean);
Object result = bean;
List<Method> methods = new ArrayList<>();
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
if (method.getAnnotation(ConcurrencyLimit.class) != null) {
methods.add(method);
}
});
for (Method method : methods) {
boolean isProxy = AopUtils.isAopProxy(result);
if (isProxy && result instanceof Advised adv) {
adv.addAdvisor(createAdvisor(method));
} else {
ProxyFactory factory = new ProxyFactory();
factory.setTarget(bean);
factory.addAdvisor(createAdvisor(method));
result = factory.getProxy();
}
}
return result;
}
private DefaultPointcutAdvisor createAdvisor(Method method) {
ConcurrencyThrottleInterceptor interceptor = new ConcurrencyThrottleInterceptor();
interceptor.setConcurrencyLimit(method.getAnnotation(ConcurrencyLimit.class).value());
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(interceptor);
advisor.setPointcut(new Pointcut() {
final String methodName = method.toString();
public MethodMatcher getMethodMatcher() {
return (m, cls) -> m.toString().equals(methodName);
}
public ClassFilter getClassFilter() { return ClassFilter.TRUE; }
});
return advisor;
}
}This eliminates the need to declare an advisor for every method, but requires writing the annotation and post‑processor, which the author describes as “more trouble”.
6. Recommendation
ConcurrencyThrottleInterceptoris intended for a single, system‑wide resource guard rather than fine‑grained business rate limiting. For per‑API or per‑method limits, use dedicated libraries such as Bucket4j or Resilience4j.
Bucket4j – API rate limiting made simple
Resilience4j – Ensuring system stability and interface safety
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
