Rate Limiting in Java: Redis, Spring AOP, and Custom Annotations

This article explains the concept of rate limiting, introduces common algorithms such as token bucket and leaky bucket, and provides a complete Spring‑Boot implementation using Redis, custom annotations, AOP, and utility classes for distributed request throttling.

Java Captain
Java Captain
Java Captain
Rate Limiting in Java: Redis, Spring AOP, and Custom Annotations

Rate Limiting Introduction

Rate limiting refers to restricting the number of accesses to a resource within a certain time window to prevent abuse, overload, or excessive consumption. Proper rate limiting can protect servers from crashing, improve user experience, and increase system availability.

Common rate‑limiting algorithms include:

Leaky Bucket: A fixed‑size bucket receives requests; tokens are drained at a constant rate. Excess requests are discarded when the bucket overflows.

Token Bucket: Tokens are added to a bucket at a fixed rate. A request consumes a token; if none are available, the request is rejected.

Sliding Window: Counts requests within a moving time window; if the count exceeds a threshold, further requests are denied.

Typical scenarios for applying rate limiting are:

Prevent server crashes caused by sudden traffic spikes.

Maintain a smooth user experience by limiting rapid repeated calls.

Increase overall system reliability by avoiding resource exhaustion.

The implementation relies on Redis, Redisson, AOP, and custom annotations.

Dependencies

<!--redisson-->
<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>lock4j-redisson-spring-boot-starter</artifactId>
</dependency>

<!-- Spring core utilities -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context-support</artifactId>
</dependency>

<!-- Spring Web -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-web</artifactId>
</dependency>

<!-- Custom validation annotation -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

<!-- AOP -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- Common utilities -->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
</dependency>

<!-- Servlet API -->
<dependency>
  <groupId>jakarta.servlet</groupId>
  <artifactId>jakarta.servlet-api</artifactId>
</dependency>

<!-- Hutool utilities -->
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-core</artifactId>
</dependency>
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-http</artifactId>
</dependency>
<dependency>
  <groupId>cn.hutool</groupId>
  <artifactId>hutool-extra</artifactId>
</dependency>

<!-- Lombok -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>

<!-- Configuration processor -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

<!-- Version migration -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-properties-migrator</artifactId>
  <scope>runtime</scope>
</dependency>

<!-- MapStruct Plus -->
<dependency>
  <groupId>io.github.linpeilie</groupId>
  <artifactId>mapstruct-plus-spring-boot-starter</artifactId>
</dependency>

<!-- IP region library -->
<dependency>
  <groupId>org.lionsoul</groupId>
  <artifactId>ip2region</artifactId>
</dependency>

1. Define Rate Limiting Types

public enum LimitType {
    /** Default global limit */
    DEFAULT,
    /** Limit by requester's IP */
    IP,
    /** Cluster instance limit */
    CLUSTER
}

2. Define the @RateLimiter Annotation

import java.lang.annotation.*;
/**
 * Rate limiting annotation
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
    /**
     * Rate‑limiting key, supports Spring EL expressions.
     */
    String key() default "";
    /**
     * Time window in seconds.
     */
    int time() default 60;
    /**
     * Allowed number of requests.
     */
    int count() default 100;
    /**
     * Limiting strategy.
     */
    LimitType limitType() default LimitType.DEFAULT;
    /**
     * Message shown when limit is exceeded (supports i18n).
     */
    String message() default "{rate.limiter.message}";
}

Redis Utility Class

The following class provides common Redis operations used by the rate‑limiting logic. Only the relevant methods are shown; you can copy or adapt them as needed.

@NoArgsConstructor(access = AccessLevel.PRIVATE)
@SuppressWarnings({"unchecked", "rawtypes"})
public class RedisUtils {
    private static final RedissonClient CLIENT = SpringUtils.getBean(RedissonClient.class);
    /**
     * Perform rate limiting.
     */
    public static long rateLimiter(String key, RateType rateType, int rate, int rateInterval) {
        RRateLimiter limiter = CLIENT.getRateLimiter(key);
        limiter.trySetRate(rateType, rate, rateInterval, RateIntervalUnit.SECONDS);
        if (limiter.tryAcquire()) {
            return limiter.availablePermits();
        } else {
            return -1L;
        }
    }
    // ... (other cache helper methods omitted for brevity) ...
}

i18n Resource Helper

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MessageUtils {
    private static final MessageSource MESSAGE_SOURCE = SpringUtils.getBean(MessageSource.class);
    public static String message(String code, Object... args) {
        try {
            return MESSAGE_SOURCE.getMessage(code, args, LocaleContextHolder.getLocale());
        } catch (NoSuchMessageException e) {
            return code;
        }
    }
}

Custom Business Exception

@Data
@EqualsAndHashCode(callSuper = true)
@NoArgsConstructor
@AllArgsConstructor
public final class ServiceException extends RuntimeException {
    @Serial
    private static final long serialVersionUID = 1L;
    private Integer code;
    private String message;
    private String detailMessage;
    public ServiceException(String message) { this.message = message; }
    public ServiceException(String message, Integer code) { this.message = message; this.code = code; }
    @Override
    public String getMessage() { return message; }
    // getters and setters omitted for brevity
}

Servlet Utility Class

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ServletUtils extends JakartaServletUtil {
    /** Get current HttpServletRequest */
    public static HttpServletRequest getRequest() {
        try { return getRequestAttributes().getRequest(); } catch (Exception e) { return null; }
    }
    /** Get client IP address */
    public static String getClientIP() { return getClientIP(getRequest()); }
}

3. Process the @RateLimiter Annotation (Aspect)

@Slf4j
@Aspect
public class RateLimiterAspect {
    private final ExpressionParser parser = new SpelExpressionParser();
    private final ParserContext parserContext = new TemplateParserContext();
    private final EvaluationContext context = new StandardEvaluationContext();
    private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
    private final String GLOBAL_REDIS_KEY = "global:";
    private final String RATE_LIMIT_KEY = GLOBAL_REDIS_KEY + "rate_limit:";

    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        String combineKey = getCombineKey(rateLimiter, point);
        try {
            RateType rateType = RateType.OVERALL;
            if (rateLimiter.limitType() == LimitType.CLUSTER) {
                rateType = RateType.PER_CLIENT;
            }
            long number = RedisUtils.rateLimiter(combineKey, rateType, count, time);
            if (number == -1) {
                String message = rateLimiter.message();
                if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
                    message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
                }
                throw new ServiceException(message);
            }
            log.info("Limit token => {}, remaining => {}, cache key => '{}'", count, number, combineKey);
        } catch (Exception e) {
            if (e instanceof ServiceException) {
                throw e;
            } else {
                throw new RuntimeException("Server rate‑limit exception, please try later");
            }
        }
    }

    /** Build the final Redis key based on the annotation and request context */
    public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        String key = rateLimiter.key();
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (StringUtils.containsAny(key, "#")) {
            Object[] args = point.getArgs();
            String[] paramNames = pnd.getParameterNames(method);
            if (ArrayUtil.isEmpty(paramNames)) {
                throw new ServiceException("Rate‑limit key parsing error! Contact administrator.");
            }
            for (int i = 0; i < paramNames.length; i++) {
                context.setVariable(paramNames[i], args[i]);
            }
            try {
                Expression expression = StringUtils.startsWith(key, parserContext.getExpressionPrefix()) &&
                                      StringUtils.endsWith(key, parserContext.getExpressionSuffix())
                                      ? parser.parseExpression(key, parserContext)
                                      : parser.parseExpression(key);
                key = expression.getValue(context, String.class) + ":";
            } catch (Exception e) {
                throw new ServiceException("Rate‑limit key parsing error! Contact administrator.");
            }
        }
        StringBuilder sb = new StringBuilder(RATE_LIMIT_KEY);
        sb.append(ServletUtils.getRequest().getRequestURI()).append(":");
        if (rateLimiter.limitType() == LimitType.IP) {
            sb.append(ServletUtils.getClientIP()).append(":");
        } else if (rateLimiter.limitType() == LimitType.CLUSTER) {
            sb.append(RedisUtils.getClient().getId()).append(":");
        }
        return sb.append(key).toString();
    }
}

Testing the Rate Limiter

A sample controller demonstrates three scenarios: global limit, IP‑based limit, and cluster‑instance limit. The controller returns a generic response object R, which can be replaced with any project‑specific wrapper.

@Slf4j
@RestController
@RequestMapping("/demo/rateLimiter")
public class RedisRateLimiterController {
    /** Global limit (affects all requests) */
    @RateLimiter(count = 2, time = 10)
    @GetMapping("/test")
    public R<String> test(String value) {
        return R.ok("Operation successful", value);
    }
    /** IP‑based limit */
    @RateLimiter(count = 2, time = 10, limitType = LimitType.IP)
    @GetMapping("/testip")
    public R<String> testIp(String value) {
        return R.ok("Operation successful", value);
    }
    /** Cluster instance limit (different backend nodes are independent) */
    @RateLimiter(count = 2, time = 10, limitType = LimitType.CLUSTER)
    @GetMapping("/testcluster")
    public R<String> testCluster(String value) {
        return R.ok("Operation successful", value);
    }
    /** IP limit with a dynamic key based on method parameter */
    @RateLimiter(count = 2, time = 10, limitType = LimitType.IP, key = "#value")
    @GetMapping("/testObj")
    public R<String> testObj(String value) {
        return R.ok("Operation successful", value);
    }
}

If you encounter any issues or have suggestions, feel free to comment and discuss.

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.

annotationDistributedrate limiting
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

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.