Build a Switchable SpringBoot Rate Limiter with Guava and Redis
This tutorial walks through creating a reusable SpringBoot starter that combines Guava's local token‑bucket limiter and Redis's distributed limiter, allowing developers to toggle between them via a simple configuration property and integrate the solution seamlessly with AOP and conditional beans.
Earlier articles introduced using Guava and Redis for rate limiting in SpringBoot. This guide shows how to merge both mechanisms into a single, configurable component that can be switched at runtime.
Step 1 – Create a common module cloud-limiter-starter
Add the required dependencies to the module’s pom.xml:
<dependencies>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
<!--SpringFramework-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>Tip: Name the starter module with the starter suffix and set the dependencies’ scope to provided .
Step 2 – Implement the rate‑limiting logic
Create a common interface:
public interface LimiterManager {
boolean tryAccess(Limiter limiter);
}Guava implementation ( GuavaLimiter)
@Slf4j
public class GuavaLimiter implements LimiterManager {
private final Map<String, RateLimiter> limiterMap = Maps.newConcurrentMap();
@Override
public boolean tryAccess(Limiter limiter) {
RateLimiter rateLimiter = getRateLimiter(limiter);
if (rateLimiter == null) {
return false;
}
boolean access = rateLimiter.tryAcquire(1, 100, TimeUnit.MILLISECONDS);
log.info("{} access :{}", limiter.getKey(), access);
return access;
}
}Redis implementation ( RedisLimiter)
@Slf4j
public class RedisLimiter implements LimiterManager {
private final StringRedisTemplate stringRedisTemplate;
public RedisLimiter(StringRedisTemplate stringRedisTemplate) {
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryAccess(Limiter limiter) {
String key = limiter.getKey();
if (StringUtils.isEmpty(key)) {
throw new LimiterException("redis limiter key cannot be null");
}
List<String> keys = new ArrayList<>();
keys.add(key);
int seconds = limiter.getSeconds();
int limitCount = limiter.getLimitNum();
String luaScript = buildLuaScript();
RedisScript<Long> redisScript = new DefaultRedisScript<>(luaScript, Long.class);
Long count = stringRedisTemplate.execute(redisScript, keys, "" + limitCount, "" + seconds);
log.info("Access try count is {} for key={}", count, key);
return count != null && count != 0;
}
}Step 3 – Create a configuration class
@Configuration
public class LimiterConfigure {
@Bean
@ConditionalOnProperty(name = "limit.type", havingValue = "local")
public LimiterManager guavaLimiter() {
return new GuavaLimiter();
}
@Bean
@ConditionalOnProperty(name = "limit.type", havingValue = "redis")
public LimiterManager redisLimiter(StringRedisTemplate stringRedisTemplate) {
return new RedisLimiter(stringRedisTemplate);
}
}Step 4 – Implement the AOP aspect
@Aspect
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Conditional(LimitAspectCondition.class)
public class LimitAspect {
@Setter(onMethod_ = @Autowired)
private LimiterManager limiterManager;
@Pointcut("@annotation(com.jianzh5.limit.aop.Limit)")
private void check() {}
@Before("check()")
public void before(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
Limit limit = method.getAnnotation(Limit.class);
if (limit != null) {
Limiter limiter = Limiter.builder()
.limitNum(limit.limitNum())
.seconds(limit.seconds())
.key(limit.key())
.build();
if (!limiterManager.tryAccess(limiter)) {
throw new LimiterException("There are currently many people, please try again later!");
}
}
}
}The aspect is only loaded when the configuration property limit.type exists, thanks to the custom condition:
public class LimitAspectCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// Check if the environment contains the property
return context.getEnvironment().containsProperty(ConfigConstant.LIMIT_TYPE);
}
}Step 5 – Register the auto‑configuration
## AutoConfiguration
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.jianzh5.limit.config.LimiterConfigure,\
com.jianzh5.limit.aop.LimitAspectDirectory structure (illustrative):
Step 6 – Use the limiter in a project
Add the starter dependency:
<dependency>
<groupId>com.jianzh5</groupId>
<artifactId>cloud-limit-starter</artifactId>
</dependency>Configure the desired limiter in application.properties: limit.type=redis Annotate the target API method:
@Limit(key = "Limiter:test", limitNum = 3, seconds = 1)When the property is omitted, no limiter is loaded.
Conclusion
By following these steps you obtain a flexible, plug‑and‑play rate‑limiting starter for SpringBoot. Switching between local Guava and distributed Redis implementations is as easy as changing a configuration value, keeping business code clean and maintainable.
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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
