Backend Development 18 min read

Implementing Interface Rate Limiting in Spring Boot Using Interceptor, Redis, and Custom Annotations

This article demonstrates how to build a flexible API rate‑limiting solution in Spring Boot by combining a HandlerInterceptor, Redis counters, and custom annotations with reflection, covering configuration, code examples, mapping rules, time‑window nuances, path‑parameter handling, and real‑IP considerations.

Java Architect Essentials
Java Architect Essentials
Java Architect Essentials
Implementing Interface Rate Limiting in Spring Boot Using Interceptor, Redis, and Custom Annotations

The author introduces a demo that prevents excessive API calls by using a Spring HandlerInterceptor together with Redis to count requests and enforce limits.

Principle

Requests are identified by the concatenation of the client IP address and the request URI; the interceptor checks Redis for a lock key and a count key to decide whether to allow the request or block it.

Project

The source code is hosted on GitHub ( https://github.com/Tonciy/interface-brush-protection ) and an Apifox mock is provided. The most important logic resides in the AccessLimintInterceptor class.

/**
 * @author Zero
 * @time 2023/2/14
 * @description 接口防刷拦截处理
 */
@Slf4j
public class AccessLimintInterceptor implements HandlerInterceptor {
    @Resource
    private RedisTemplate
redisTemplate;
    @Value("${interfaceAccess.second}")
    private Long second = 10L;
    @Value("${interfaceAccess.time}")
    private Long time = 3L;
    @Value("${interfaceAccess.lockTime}")
    private Long lockTime = 60L;
    public static final String LOCK_PREFIX = "LOCK";
    public static final String COUNT_PREFIX = "COUNT";
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String uri = request.getRequestURI();
        String ip = request.getRemoteAddr();
        String lockKey = LOCK_PREFIX + ip + uri;
        Object isLock = redisTemplate.opsForValue().get(lockKey);
        if (Objects.isNull(isLock)) {
            String countKey = COUNT_PREFIX + ip + uri;
            Object count = redisTemplate.opsForValue().get(countKey);
            if (Objects.isNull(count)) {
                log.info("首次访问");
                redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
            } else {
                if ((Integer) count < time) {
                    redisTemplate.opsForValue().increment(countKey);
                } else {
                    log.info("{}禁用访问{}", ip, uri);
                    redisTemplate.opsForValue().set(lockKey, 1, lockTime, TimeUnit.SECONDS);
                    redisTemplate.delete(countKey);
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                }
            }
        } else {
            throw new CommonException(ResultCode.ACCESS_FREQUENT);
        }
        return true;
    }
}

Self‑Questioning

The initial implementation works but lacks flexibility: all protected endpoints share the same x seconds, y max calls, and z lock duration, which is not always desirable.

Interceptor Mapping Rules

By configuring the interceptor's URL patterns, only selected APIs can be rate‑limited. This solves the “all‑or‑nothing” problem but still forces uniform parameters for the chosen endpoints.

Custom Annotation + Reflection (Version 2.0)

A custom @AccessLimit annotation is defined with attributes second , maxTime , and forbiddenTime . The annotation can be placed on methods or classes; class‑level annotation applies to all methods unless a method overrides it.

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface AccessLimit {
    long second();
    long maxTime();
    long forbiddenTime();
}

The interceptor now uses reflection to read the annotation from the handler method or its declaring class, allowing per‑endpoint configuration.

Time‑Window Logic Issue

The simple Redis‑key expiration approach does not enforce a true sliding window (any x ‑second interval). The author explains the limitation with an example where requests at seconds 2, 7, 12, and 17 would never trigger a block despite exceeding the intended rate.

Path‑Parameter Problem

When URI contains path variables, different parameter values produce different URIs, causing the same logical endpoint to be treated as distinct. Solutions include avoiding path variables or replacing the URI with a method‑level identifier (e.g., ClassName#methodName ).

Real IP Retrieval

Using request.getRemoteAddr() may return the proxy’s address; the article notes that extracting the real client IP from headers (e.g., X‑Forwarded‑For ) is required in production.

Conclusion

The tutorial walks through building a basic rate‑limiting interceptor, evolving it with custom annotations and reflection for fine‑grained control, and discusses practical pitfalls such as sliding‑window accuracy, path‑parameter handling, and real‑IP detection.

JavaRedisSpring BootCustom AnnotationInterceptorrate limiting
Java Architect Essentials
Written by

Java Architect Essentials

Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.

0 followers
Reader feedback

How this landed with the community

login 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.