Implementing Request Rate Limiting in Spring Boot with Custom Annotation and Redis

This article explains how to limit the number of requests an interface can receive within a specific time window in a Spring Boot application by using a custom @RequestLimit annotation, Redis for counting, and a Spring interceptor to enforce the limits, complete with full code examples and configuration steps.

Top Architect
Top Architect
Top Architect
Implementing Request Rate Limiting in Spring Boot with Custom Annotation and Redis

The article introduces a request rate limiting solution for Spring Boot applications, where an interface can be limited to N requests within a specified time window.

Principle

When a request arrives, the server records the request count in Redis; if the count exceeds the limit, the request is denied. The Redis key has an expiration time.

Code Implementation

It uses a custom annotation @RequestLimit to specify second and maxCount, and an interceptor RequestLimitIntercept that checks the Redis count before allowing the request.

@RequestLimit Annotation

import java.lang.annotation.*;
/**
 * 请求限制的自定义注解
 *
 * @Target 注解可修饰的对象范围,ElementType.METHOD 作用于方法,ElementType.TYPE 作用于类
 * (ElementType)取值有:
 *      1.CONSTRUCTOR:用于描述构造器
 *      2.FIELD:用于描述域
 *      3.LOCAL_VARIABLE:用于描述局部变量
 *      4.METHOD:用于描述方法
 *      5.PACKAGE:用于描述包
 *      6.PARAMETER:用于描述参数
 *      7.TYPE:用于描述类、接口(包括注解类型) 或enum声明
 * @Retention 定义了该Annotation被保留的时间长短:
 *      1.SOURCE:在源文件中有效(即源文件保留)
 *      2.CLASS:在class文件中有效(即class保留)
 *      3.RUNTIME:在运行时有效(即运行时保留)
 * @Inherited 元注解是一个标记注解,@Inherited阐述了某个被标注的类型是被继承的。
 */
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
    // 在 second 秒内,最大只能请求 maxCount 次
    int second() default 1;
    int maxCount() default 1;
}

RequestLimitIntercept Interceptor

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;
/**
 * 请求拦截
 */
@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)){
                    resonseOut(response,Result.error(ApiResultEnum.REQUST_LIMIT));
                    return false;
                }
            }
        }
        return super.preHandle(request, response, handler);
    }
    // 判断请求是否受限
    public boolean isLimit(HttpServletRequest request, RequestLimit requestLimit){
        // 受限的redis 缓存key ,因为这里用浏览器做测试,我就用sessionid 来做唯一key,如果是app ,可以使用 用户ID 之类的唯一标识。
        String limitKey = request.getServletPath()+request.getSession().getId();
        Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey);
        if(redisCount == null){
            // 初始次数
            redisTemplate.opsForValue().set(limitKey,1,requestLimit.second(), TimeUnit.SECONDS);
        } else {
            if(redisCount.intValue() >= requestLimit.maxCount()){
                return true;
            }
            // 次数自增
            redisTemplate.opsForValue().increment(limitKey);
        }
        return false;
    }
    /**
     * 回写给客户端
     */
    private void resonseOut(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 Configuration Class

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Autowired
    private RequestLimitIntercept requestLimitIntercept;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        log.info("添加拦截");
        registry.addInterceptor(requestLimitIntercept);
    }
}

Controller Usage

@RestController
@RequestMapping("/index")
@RequestLimit(maxCount = 5, second = 1)
public class IndexController {
    /**
     * @RequestLimit 修饰在方法上,优先使用其参数
     */
    @GetMapping("/test1")
    @RequestLimit
    public Result test(){
        // TODO ...
        return Result.ok();
    }
    /**
     * @RequestLimit 修饰在类上,用的是类的参数
     */
    @GetMapping("/test2")
    public Result test2(){
        // TODO ...
        return Result.ok();
    }
}

If both class and method have @RequestLimit, the method-level parameters take precedence.

The complete source code is hosted on Gitee and GitHub (links provided in the original article).

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaSpring Bootannotation
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.