How to Implement a Request Rate Limiter in Spring Boot Using Custom Annotations
This article demonstrates a complete Spring Boot solution for limiting how many times a specific API can be called by a user within a given time window, using a custom @RequestLimit annotation, Redis for counting, and an interceptor to enforce the rule.
In many projects you may need to restrict a certain interface so that a specific user can only request it N times within a defined period.
The common problem of repeated button clicks can be solved on the backend by recording request counts in Redis; if the count exceeds the limit, the request is denied. Redis keys have an expiration time, so they are automatically removed after the window.
Principle
When a request arrives, the server increments a Redis key that records the number of accesses. If the count exceeds the configured maximum, the request is blocked. The key expires after the configured number of seconds.
Code Implementation
To make the solution look more sophisticated, it is implemented with a custom annotation.
@RequestLimit Annotation
import java.lang.annotation.*;
/**
* Custom annotation for request limiting
*/
@Documented
@Inherited
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
// Maximum number of requests allowed within 'second' seconds
int second() default 1;
int maxCount() default 1;
}RequestLimitIntercept Interceptor
The interceptor checks the request count before the controller method is executed.
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import top.lrshuai.limit.annotation.RequestLimit;
import top.lrshuai.limit.common.ApiResultEnum;
import top.lrshuai.limit.common.Result;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/** Request interceptor */
@Slf4j
@Component
public class RequestLimitIntercept extends HandlerInterceptorAdapter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler.getClass().isAssignableFrom(HandlerMethod.class)) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RequestLimit methodAnnotation = method.getAnnotation(RequestLimit.class);
RequestLimit classAnnotation = method.getDeclaringClass().getAnnotation(RequestLimit.class);
RequestLimit requestLimit = methodAnnotation != null ? methodAnnotation : classAnnotation;
if (requestLimit != null) {
if (isLimit(request, requestLimit)) {
responseOut(response, Result.error(ApiResultEnum.REQUST_LIMIT));
return false;
}
}
}
return super.preHandle(request, response, handler);
}
// Determine whether the request exceeds the limit
public boolean isLimit(HttpServletRequest request, RequestLimit requestLimit) {
// Use session ID as a unique key for demonstration; in production you may use user ID
String limitKey = request.getServletPath() + request.getSession().getId();
Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey);
if (redisCount == null) {
// First request, set initial count with expiration
redisTemplate.opsForValue().set(limitKey, 1, requestLimit.second(), TimeUnit.SECONDS);
} else {
if (redisCount.intValue() >= requestLimit.maxCount()) {
return true;
}
// Increment count
redisTemplate.opsForValue().increment(limitKey);
}
return false;
}
// Write JSON response back to client
private void responseOut(HttpServletResponse response, Result result) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
String json = JSONObject.toJSON(result).toString();
PrintWriter out = response.getWriter();
out.append(json);
}
}WebMvcConfig Registration
For Spring Boot 2.x, implement WebMvcConfigurer and add the interceptor.
@Slf4j
@Component
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private RequestLimitIntercept requestLimitIntercept;
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.info("Adding request limit interceptor");
registry.addInterceptor(requestLimitIntercept);
}
}Controller Usage
Apply the annotation on a class or method. Method-level parameters take precedence over class-level ones.
@RestController
@RequestMapping("/index")
@RequestLimit(maxCount = 5, second = 1)
public class IndexController {
/** Limit on method, overrides class settings */
@GetMapping("/test1")
@RequestLimit
public Result test() {
return Result.ok();
}
/** Uses class-level limit */
@GetMapping("/test2")
public Result test2() {
return Result.ok();
}
}By default, each interface can be called only once per second (maxCount=1, second=1). Adjust maxCount and second as needed.
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.
Java High-Performance Architecture
Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.
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.
