java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot AOP接口限流

基于SpringBoot+AOP实现接口限流

作者:希望永不加班

本文介绍了使用SpringBoot和AOP实现接口限流的方法,通过自定义注解、限流工具类、AOP切面等实现限流功能,支持多种限流策略、粒度和存储方式,并提供了详细的代码示例和测试验证方法,需要的朋友可以参考下

前面我们用 AOP 实现了操作日志和权限校验,彻底摆脱了代码冗余的困扰;今天继续 AOP 实例——SpringBoot + AOP 实现接口限流

做后端开发的同学都知道,接口限流是系统稳定性的“第一道防线”:

比如登录接口、短信验证码接口、支付接口,很容易被恶意请求刷爆(比如频繁调用发送短信、恶意登录试错),导致系统响应变慢、服务崩溃,甚至产生额外的费用(短信费、接口调用费)。

如果在每个接口中手动写限流逻辑,不仅代码冗余,还难以统一管理和扩展。而用 AOP 实现接口限流,只需一行注解,就能灵活控制接口的请求频率,不侵入业务代码,兼顾优雅和实用。

一、接口限流的核心场景

接口限流的核心是“限制单位时间内的请求次数”,结合企业实战场景,本次需求覆盖以下核心点,可直接适配大部分项目:

  1. 多限流策略:支持固定窗口限流(简单易实现)和滑动窗口限流(精准度高,避免临界问题),可灵活选择;
  2. 自定义限流key:支持按 IP 限流(限制单个IP的请求频率)、按用户ID限流(限制单个用户的请求频率),适配不同场景;
  3. 自定义限流参数:可灵活配置“单位时间”和“最大请求次数”(如 1分钟内最多请求10次、10秒内最多请求3次);
  4. 统一限流响应:触发限流时,返回统一的 JSON 格式,包含错误码、错误信息,便于前端提示用户“请求过于频繁”;
  5. 不侵入业务代码:通过 AOP 增强实现,业务接口无需修改,降低耦合度;
  6. 分布式适配:支持单机限流(本地缓存)和分布式限流(Redis),适配集群部署场景;
  7. 异常处理:限流逻辑异常时,不影响接口正常访问,保证系统稳定性。

二、设计思路

在写代码前,先搞懂两个核心限流策略(新手也能轻松理解),以及整体设计思路,避免写代码时逻辑混乱。

1. 两种核心限流策略

(1)固定窗口限流

原理:将时间划分为固定的窗口(如 1分钟一个窗口),统计每个窗口内的请求次数,超过最大次数则触发限流。

举例:配置“1分钟内最多请求10次”,第一个窗口(0-60秒)请求10次后,后续请求被限流;60秒后进入新窗口,请求次数重置,可再次请求。

优点:实现简单、性能高;缺点:存在临界问题(比如59秒请求10次,61秒再请求10次,2秒内请求20次,突破限流阈值)。

(2)滑动窗口限流

原理:将固定窗口拆分为多个小窗口(如 1分钟拆分为6个10秒小窗口),每次请求时,只统计“当前时间往前推1分钟”内的请求次数,超过阈值则限流。

举例:同样配置“1分钟内最多请求10次”,59秒请求10次后,61秒请求时,统计的是1-61秒内的请求次数(仍为10次),会被限流,避免临界问题。

优点:限流精准,无临界问题;缺点:实现稍复杂,性能略低于固定窗口。

2. 整体设计思路

  1. 自定义注解:创建 @RateLimit 注解,用于标记需要限流的接口,配置限流策略、限流key、时间窗口、最大请求次数;
  2. 限流工具类:分别实现固定窗口和滑动窗口的限流逻辑,支持本地缓存(单机)和 Redis(分布式)存储请求次数;
  3. AOP 切面:定义切点(拦截所有添加了 @RateLimit 注解的方法),用环绕通知实现限流校验逻辑;
  4. 限流key生成:根据注解配置,生成不同的限流key(IP/用户ID),实现精准限流;
  5. 统一异常与响应:触发限流时,抛出自定义限流异常,通过全局异常处理器返回统一 JSON 响应;
  6. 多场景测试:覆盖单机/分布式、不同限流策略、不同限流key,验证限流效果。

三、完整代码

本次实战基于 SpringBoot 2.7.x 版本,整合 Redis(支持分布式限流),所有代码添加详细注释,新手也能轻松理解每一步的作用,无需修改核心逻辑,直接适配项目。

步骤1:导入核心依赖

需要导入 AOP 依赖、Redis 依赖、工具包,pom.xml 如下:

<!-- Spring AOP 核心依赖(限流核心) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Redis 依赖(分布式限流必备,单机可省略) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 工具包(JSON 响应、缓存操作,简化代码) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.32</version>
</dependency>
<!--  lombok 依赖(简化实体类、工具类代码) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

步骤2:配置文件(application.yml)

配置 Redis、服务器端口,单机限流可省略 Redis 配置:

server:
  port: 8080 # 服务器端口
# Redis 配置(分布式限流必备)
spring:
  redis:
    host: localhost # Redis 地址(本地测试用)
    port: 6379 # Redis 端口
    password: # Redis 密码(无密码则留空)
    database: 0 # 数据库索引
    lettuce:
      pool:
        max-active: 100 # 最大连接数
        max-idle: 10 # 最大空闲连接
        min-idle: 5 # 最小空闲连接
# 限流全局配置(可选,可在注解中覆盖)
rate-limit:
  default-time: 60 # 默认时间窗口(秒)
  default-count: 10 # 默认最大请求次数
  default-type: FIXED_WINDOW # 默认限流策略(FIXED_WINDOW:固定窗口,SLIDING_WINDOW:滑动窗口)
  default-key-type: IP # 默认限流key类型(IP:按IP限流,USER_ID:按用户ID限流)

步骤3:自定义限流注解

创建 @RateLimit 注解,用于标记需要限流的接口,可灵活配置限流参数,贴合企业实战需求:

import java.lang.annotation.*;
/**
 * 自定义接口限流注解
 * @Target(ElementType.METHOD):仅作用于方法(接口方法)
 * @Retention(RetentionPolicy.RUNTIME):运行时保留,AOP 切面可获取注解属性
 * @Documented:生成 API 文档时,显示该注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    /**
     * 限流策略(固定窗口/滑动窗口)
     * 默认为全局配置的策略,可在接口上单独配置覆盖
     */
    LimitType type() default LimitType.FIXED_WINDOW;
    /**
     * 限流key类型(按IP/按用户ID)
     * 默认为全局配置的key类型,可单独覆盖
     */
    KeyType keyType() default KeyType.IP;
    /**
     * 时间窗口(单位:秒)
     * 默认为全局配置的时间,可单独覆盖(如 60=1分钟,10=10秒)
     */
    int time() default 0;
    /**
     * 单位时间内的最大请求次数(限流阈值)
     * 默认为全局配置的次数,可单独覆盖
     */
    int count() default 0;
    /**
     * 限流提示信息(触发限流时返回)
     */
    String message() default "请求过于频繁,请稍后再试!";
    /**
     * 限流存储方式(本地缓存/Redis)
     * 默认为 Redis,单机部署可改为 LOCAL
     */
    StoreType storeType() default StoreType.REDIS;
    /**
     * 限流策略枚举
     */
    enum LimitType {
        FIXED_WINDOW,  // 固定窗口限流
        SLIDING_WINDOW // 滑动窗口限流
    }
    /**
     * 限流key类型枚举
     */
    enum KeyType {
        IP,        // 按请求IP限流(最常用)
        USER_ID    // 按当前登录用户ID限流(需结合用户上下文)
    }
    /**
     * 存储方式枚举
     */
    enum StoreType {
        LOCAL,  // 本地缓存(单机部署用)
        REDIS   // Redis(分布式部署用)
    }
}

注解属性说明:

步骤4:核心工具类

这部分是限流的核心,分别实现固定窗口、滑动窗口的限流逻辑,支持本地缓存和 Redis 存储,代码可直接复用:

4.1 限流常量类(统一管理key前缀)

/**
 * 限流常量类(统一管理 Redis/本地缓存的key前缀,避免混乱)
 */
public class RateLimitConstant {
    // 限流key前缀(Redis中使用,如 rate_limit:ip:127.0.0.1:接口路径)
    public static final String RATE_LIMIT_KEY_PREFIX = "rate_limit:";
    // 滑动窗口小窗口大小(默认10秒,可根据需求调整)
    public static final int SLIDING_WINDOW_INTERVAL = 10;
}

4.2 限流工具类

import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
 * 限流工具类(实现固定窗口、滑动窗口限流,支持本地/Redis存储)
 */
@Slf4j
@Component
public class RateLimitUtil {
    // 本地缓存(单机限流用,ConcurrentHashMap 线程安全)
    private final ConcurrentHashMap&lt;String, Integer&gt; localCache = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Long> localWindowCache = new ConcurrentHashMap<>();
    // Redis 模板(分布式限流用)
    @Autowired(required = false) // 单机部署时,Redis 可省略,避免报错
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 固定窗口限流(核心方法)
     * @param key 限流key(如 ip:127.0.0.1:接口路径)
     * @param time 时间窗口(秒)
     * @param count 最大请求次数
     * @param storeType 存储方式(本地/Redis)
     * @return true=触发限流,false=未触发限流
     */
    public boolean fixedWindowLimit(String key, int time, int count, RateLimit.StoreType storeType) {
        if (storeType == RateLimit.StoreType.LOCAL) {
            // 本地缓存实现固定窗口
            return localFixedWindowLimit(key, time, count);
        } else {
            // Redis 实现固定窗口(分布式)
            return redisFixedWindowLimit(key, time, count);
        }
    }
    /**
     * 滑动窗口限流(核心方法)
     * @param key 限流key(如 ip:127.0.0.1:接口路径)
     * @param time 时间窗口(秒)
     * @param count 最大请求次数
     * @param storeType 存储方式(本地/Redis)
     * @return true=触发限流,false=未触发限流
     */
    public boolean slidingWindowLimit(String key, int time, int count, RateLimit.StoreType storeType) {
        if (storeType == RateLimit.StoreType.LOCAL) {
            // 本地缓存实现滑动窗口
            return localSlidingWindowLimit(key, time, count);
        } else {
            // Redis 实现滑动窗口(分布式)
            return redisSlidingWindowLimit(key, time, count);
        }
    }
    /**
     * 本地缓存 - 固定窗口限流
     */
    private boolean localFixedWindowLimit(String key, int time, int count) {
        // 1. 获取当前窗口的请求次数
        Integer currentCount = localCache.getOrDefault(key, 0);
        // 2. 检查是否超过限流阈值
        if (currentCount >= count) {
            log.warn("本地固定窗口限流触发,key:{},当前次数:{},阈值:{}", key, currentCount, count);
            return true;
        }
        // 3. 第一次请求,设置窗口过期时间(time秒后清空)
        if (currentCount == 0) {
            localWindowCache.put(key, System.currentTimeMillis() + time * 1000);
        } else {
            // 检查窗口是否过期,过期则重置次数和窗口时间
            Long expireTime = localWindowCache.get(key);
            if (System.currentTimeMillis() > expireTime) {
                localCache.put(key, 1);
                localWindowCache.put(key, System.currentTimeMillis() + time * 1000);
                return false;
            }
        }
        // 4. 未超过阈值,请求次数+1
        localCache.put(key, currentCount + 1);
        return false;
    }
    /**
     * Redis - 固定窗口限流(分布式,集群部署用)
     */
    private boolean redisFixedWindowLimit(String key, int time, int count) {
        // 1. 拼接 Redis key(加上前缀,避免与其他key冲突)
        String redisKey = RateLimitConstant.RATE_LIMIT_KEY_PREFIX + key;
        // 2. 自增请求次数(原子操作,避免并发问题)
        Long currentCount = stringRedisTemplate.opsForValue().increment(redisKey, 1);
        // 3. 第一次请求,设置过期时间(time秒)
        if (currentCount != null && currentCount == 1) {
            stringRedisTemplate.expire(redisKey, time, TimeUnit.SECONDS);
        }
        // 4. 检查是否超过限流阈值
        if (currentCount != null && currentCount > count) {
            log.warn("Redis固定窗口限流触发,key:{},当前次数:{},阈值:{}", redisKey, currentCount, count);
            return true;
        }
        return false;
    }
    /**
     * 本地缓存 - 滑动窗口限流
     */
    private boolean localSlidingWindowLimit(String key, int time, int count) {
        long now = System.currentTimeMillis();
        // 1. 计算当前窗口的起始时间(当前时间 - 时间窗口)
        long windowStart = now - time * 1000;
        // 2. 拼接滑动窗口的key(包含主key和小窗口时间)
        String windowKey = key + ":" + (now / (RateLimitConstant.SLIDING_WINDOW_INTERVAL * 1000));
        // 3. 获取当前小窗口的请求次数
        Integer currentWindowCount = localCache.getOrDefault(windowKey, 0);
        // 4. 遍历所有小窗口,统计整个滑动窗口内的总请求次数
        int totalCount = 0;
        for (String cacheKey : localCache.keySet()) {
            if (cacheKey.startsWith(key + ":")) {
                // 解析小窗口时间
                long windowTime = Long.parseLong(cacheKey.split(":")[2]);
                long windowTimeMillis = windowTime * RateLimitConstant.SLIDING_WINDOW_INTERVAL * 1000;
                // 只统计当前滑动窗口内的小窗口
                if (windowTimeMillis >= windowStart) {
                    totalCount += localCache.get(cacheKey);
                } else {
                    // 移除过期的小窗口缓存
                    localCache.remove(cacheKey);
                }
            }
        }
        // 5. 检查是否超过限流阈值
        if (totalCount >= count) {
            log.warn("本地滑动窗口限流触发,key:{},当前总次数:{},阈值:{}", key, totalCount, count);
            return true;
        }
        // 6. 未超过阈值,当前小窗口请求次数+1
        localCache.put(windowKey, currentWindowCount + 1);
        return false;
    }
    /**
     * Redis - 滑动窗口限流(分布式,集群部署用)
     */
    private boolean redisSlidingWindowLimit(String key, int time, int count) {
        long now = System.currentTimeMillis();
        // 1. 计算当前窗口的起始时间(当前时间 - 时间窗口)
        long windowStart = now - time * 1000;
        // 2. 拼接 Redis key(加上前缀)
        String redisKey = RateLimitConstant.RATE_LIMIT_KEY_PREFIX + key;
        // 3. 小窗口大小(默认10秒,可调整)
        int interval = RateLimitConstant.SLIDING_WINDOW_INTERVAL;
        // 4. 当前小窗口的时间戳(按小窗口大小取整)
        long currentWindow = now / (interval * 1000);
        // 5. Redis 原子操作:删除过期小窗口 + 统计当前窗口总次数 + 自增当前小窗口次数
        // 用 Lua 脚本实现原子操作,避免并发问题
        String luaScript = "local key = KEYS[1]\n" +
                "local windowStart = ARGV[1]\n" +
                "local currentWindow = ARGV[2]\n" +
                "local interval = ARGV[3]\n" +
                "local count = ARGV[4]\n" +
                "-- 删除过期的小窗口(小于windowStart的小窗口)\n" +
                "redis.call('ZREMRANGEBYSCORE', key, 0, windowStart)\n" +
                "-- 统计当前窗口内的总请求次数\n" +
                "local total = redis.call('ZCARD', key)\n" +
                "if total >= tonumber(count) then\n" +
                "    return 1\n" +
                "end\n" +
                "-- 自增当前小窗口的请求次数(将小窗口时间戳作为score,请求ID作为value)\n" +
                "redis.call('ZADD', key, currentWindow, currentWindow .. ':' .. redis.call('INCR', key .. ':seq'))\n" +
                "-- 设置过期时间(确保缓存自动清理)\n" +
                "redis.call('EXPIRE', key, tonumber(interval) + 1)\n" +
                "return 0";
        // 执行 Lua 脚本
        Long result = stringRedisTemplate.execute(
                new org.springframework.data.redis.core.script.DefaultRedisScript<>(luaScript, Long.class),
                Arrays.asList(redisKey),
                String.valueOf(windowStart),
                String.valueOf(currentWindow),
                String.valueOf(interval),
                String.valueOf(count)
        );
        // 6. 结果判断:1=触发限流,0=未触发
        if (result != null && result == 1) {
            log.warn("Redis滑动窗口限流触发,key:{},阈值:{}", redisKey, count);
            return true;
        }
        return false;
    }
    /**
     * 清除指定key的限流缓存(用于特殊场景,如用户注销、IP解封)
     */
    public void clearLimitCache(String key, RateLimit.StoreType storeType) {
        if (storeType == RateLimit.StoreType.LOCAL) {
            // 清除本地缓存(包含所有小窗口)
            localCache.keySet().removeIf(k -> k.startsWith(key) || k.equals(key));
            localWindowCache.remove(key);
        } else {
            // 清除Redis缓存
            String redisKey = RateLimitConstant.RATE_LIMIT_KEY_PREFIX + key;
            stringRedisTemplate.delete(redisKey);
            stringRedisTemplate.delete(redisKey + ":seq");
        }
    }
}

步骤5:辅助工具类(获取IP、用户上下文)

实现获取客户端IP、当前登录用户ID的工具类,用于生成限流key,贴合实战场景:

5.1 IP工具类(获取客户端真实IP,处理代理场景)

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
 * IP工具类(获取客户端真实IP,处理Nginx代理等场景)
 */
public class IpUtil {
    /**
     * 获取客户端真实IP
     */
    public static String getClientIp() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return "127.0.0.1"; // 非web环境,默认本地IP
        }
        HttpServletRequest request = attributes.getRequest();
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 处理多代理场景,取第一个非unknown的IP
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        // 本地环境默认IP(避免localhost解析问题)
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}

5.2 用户上下文

实际项目中,从JWT Token或Spring Security中获取用户ID,这里模拟实现,可直接替换为项目中的真实逻辑:

/**
 * 用户上下文(获取当前登录用户信息,用于按用户ID限流)
 */
public class UserContext {
    /**
     * 获取当前登录用户ID(模拟,实际从JWT/Token中解析)
     * @return 用户ID(未登录返回null)
     */
    public static Long getCurrentUserId() {
        // 模拟:登录用户ID为1001,未登录返回null
        // 实际项目替换为:JwtUtils.parseToken(token).getUserId()
        return 1001L;
    }
}

步骤6:AOP 限流切面

创建切面类,拦截所有添加了 @RateLimit 注解的接口,实现限流校验逻辑,优先于日志切面执行:

import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.Method;
/**
 * 接口限流切面(核心类)
 * @Aspect:标记此类为AOP切面
 * @Component:交给Spring管理,确保Spring能扫描到
 * @Order(1):优先级高于日志切面(避免限流请求被记录日志)
 * @Slf4j:日志输出
 */
@Aspect
@Component
@Order(1)
@Slf4j
public class RateLimitAspect {
    // 注入限流工具类
    @Resource
    private RateLimitUtil rateLimitUtil;
    // 全局默认配置(从配置文件读取)
    @Value("${rate-limit.default-time}")
    private int defaultTime;
    @Value("${rate-limit.default-count}")
    private int defaultCount;
    @Value("${rate-limit.default-type}")
    private RateLimit.LimitType defaultLimitType;
    @Value("${rate-limit.default-key-type}")
    private RateLimit.KeyType defaultKeyType;
    // 1. 定义切点:拦截所有添加了 @RateLimit 注解的方法
    @Pointcut("@annotation(com.example.demo.annotation.RateLimit)")
    public void rateLimitPointcut() {}
    // 2. 环绕通知:包裹目标方法,执行限流校验
    @Around("rateLimitPointcut()")
    public Object doRateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
        // 第一步:获取目标方法上的 @RateLimit 注解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = signature.getMethod();
        RateLimit rateLimitAnno = targetMethod.getAnnotation(RateLimit.class);
        // 第二步:获取注解配置的限流参数(无配置则用全局默认值)
        RateLimit.LimitType limitType = rateLimitAnno.type() == RateLimit.LimitType.FIXED_WINDOW ?
                rateLimitAnno.type() : defaultLimitType;
        RateLimit.KeyType keyType = rateLimitAnno.keyType() == RateLimit.KeyType.IP ?
                rateLimitAnno.keyType() : defaultKeyType;
        int time = rateLimitAnno.time() == 0 ? defaultTime : rateLimitAnno.time();
        int count = rateLimitAnno.count() == 0 ? defaultCount : rateLimitAnno.count();
        String message = rateLimitAnno.message();
        RateLimit.StoreType storeType = rateLimitAnno.storeType();
        // 第三步:生成限流key(根据keyType生成,确保唯一)
        String limitKey = generateLimitKey(joinPoint, keyType);
        log.info("接口限流校验,key:{},策略:{},时间窗口:{}秒,阈值:{}次",
                limitKey, limitType, time, count);
        // 第四步:执行限流校验(根据限流策略选择对应的方法)
        boolean isLimit = false;
        if (limitType == RateLimit.LimitType.FIXED_WINDOW) {
            isLimit = rateLimitUtil.fixedWindowLimit(limitKey, time, count, storeType);
        } else if (limitType == RateLimit.LimitType.SLIDING_WINDOW) {
            isLimit = rateLimitUtil.slidingWindowLimit(limitKey, time, count, storeType);
        }
        // 第五步:判断是否触发限流,触发则抛出异常
        if (isLimit) {
            throw new RateLimitException(429, message);
        }
        // 第六步:限流校验通过,执行目标方法(核心业务逻辑)
        return joinPoint.proceed();
    }
    /**
     * 生成限流key(确保唯一,避免不同接口/不同IP/不同用户的限流冲突)
     * @param joinPoint 切入点(获取接口路径)
     * @param keyType 限流key类型(IP/USER_ID)
     * @return 唯一限流key
     */
    private String generateLimitKey(ProceedingJoinPoint joinPoint, RateLimit.KeyType keyType) {
        // 获取接口路径(如 /api/auth/login)
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getDeclaringTypeName() + "." + signature.getMethod().getName();
        // 根据keyType生成不同的限流key
        if (keyType == RateLimit.KeyType.IP) {
            // 按IP限流:ip:接口路径(如 ip:127.0.0.1:com.example.demo.controller.AuthController.login)
            String ip = IpUtil.getClientIp();
            return "ip:" + ip + ":" + methodName;
        } else if (keyType == RateLimit.KeyType.USER_ID) {
            // 按用户ID限流:user:用户ID:接口路径(如 user:1001:com.example.demo.controller.UserController.edit)
            Long userId = UserContext.getCurrentUserId();
            if (userId == null) {
                // 未登录用户,按IP限流(避免key为空)
                String ip = IpUtil.getClientIp();
                return "ip:" + ip + ":" + methodName;
            }
            return "user:" + userId + ":" + methodName;
        }
        // 默认按IP限流
        String ip = IpUtil.getClientIp();
        return "ip:" + ip + ":" + methodName;
    }
}

步骤7:自定义限流异常 + 全局异常处理器

触发限流时,抛出自定义异常,通过全局异常处理器返回统一的 JSON 响应,便于前端统一处理:

7.1 自定义限流异常

import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 自定义限流异常(触发限流时抛出)
 * 429 状态码:Too Many Requests(请求过于频繁)
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class RateLimitException extends RuntimeException {
    // 错误码(429 标准限流状态码)
    private Integer code;
    // 错误信息(自定义提示)
    private String message;
    // 构造方法(简化异常抛出)
    public RateLimitException(Integer code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
}

7.2 全局异常处理器

import com.alibaba.fastjson2.JSONObject;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
 * 全局异常处理器(统一响应格式)
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    // 拦截限流异常(429 状态码)
    @ExceptionHandler(RateLimitException.class)
    public JSONObject handleRateLimitException(RateLimitException e) {
        JSONObject response = new JSONObject();
        response.put("code", e.getCode());
        response.put("msg", e.getMessage());
        response.put("data", null);
        return response;
    }
    // 拦截其他异常(兜底处理)
    @ExceptionHandler(Exception.class)
    public JSONObject handleException(Exception e) {
        JSONObject response = new JSONObject();
        response.put("code", 500);
        response.put("msg", "服务器内部异常,请联系管理员");
        response.put("data", null);
        return response;
    }
}

步骤8:接口使用示例

在需要限流的接口上添加 @RateLimit 注解,根据业务需求配置参数,无需修改接口内部业务代码:

import com.example.demo.annotation.RateLimit;
import com.example.demo.util.UserContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
 * 测试接口(覆盖限流多场景)
 */
@RestController
@RequestMapping("/api")
public class TestController {
    /**
     * 场景1:短信验证码接口(按IP限流,固定窗口,1分钟最多3次)
     * 高频场景,防止恶意刷短信
     */
    @RateLimit(
            type = RateLimit.LimitType.FIXED_WINDOW,
            keyType = RateLimit.KeyType.IP,
            time = 60, // 1分钟
            count = 3, // 最多3次
            message = "短信发送过于频繁,请1分钟后再试!"
    )
    @PostMapping("/sms/send")
    public String sendSms(String phone) {
        // 核心业务逻辑:发送短信验证码
        return "短信已发送至:" + phone;
    }
    /**
     * 场景2:登录接口(按IP限流,滑动窗口,10秒最多2次)
     * 防止恶意暴力破解密码,滑动窗口避免临界问题
     */
    @RateLimit(
            type = RateLimit.LimitType.SLIDING_WINDOW,
            keyType = RateLimit.KeyType.IP,
            time = 10, // 10秒
            count = 2, // 最多2次
            message = "登录请求过于频繁,请10秒后再试!"
    )
    @PostMapping("/auth/login")
    public String login(String username, String password) {
        // 核心业务逻辑:用户登录
        return "登录成功,欢迎您:" + username;
    }
    /**
     * 场景3:个人中心接口(按用户ID限流,固定窗口,1分钟最多10次)
     * 登录后接口,按用户ID限流,避免单个用户恶意请求
     */
    @RateLimit(
            type = RateLimit.LimitType.FIXED_WINDOW,
            keyType = RateLimit.KeyType.USER_ID,
            time = 60,
            count = 10,
            message = "操作过于频繁,请1分钟后再试!"
    )
    @GetMapping("/user/profile")
    public String userProfile() {
        Long userId = UserContext.getCurrentUserId();
        // 核心业务逻辑:查询用户个人信息
        return "用户ID:" + userId + ",个人信息查询成功";
    }
    /**
     * 场景4:分布式限流(Redis存储,滑动窗口,5秒最多5次)
     * 集群部署场景,确保多节点限流统一
     */
    @RateLimit(
            type = RateLimit.LimitType.SLIDING_WINDOW,
            keyType = RateLimit.KeyType.IP,
            time = 5,
            count = 5,
            storeType = RateLimit.StoreType.REDIS,
            message = "请求过于频繁,请5秒后再试!"
    )
    @GetMapping("/test/distributed")
    public String distributedLimit() {
        // 核心业务逻辑:分布式场景测试
        return "分布式限流测试成功";
    }
}

四、测试验证

用 Postman 测试以下核心场景,验证限流效果,确保符合预期:

测试场景1:短信接口限流(固定窗口,IP限流)

请求地址:http://localhost:8080/api/sms/send?phone=13800138000
请求方式:POST
测试操作:1分钟内连续请求4次
测试结果:前3次正常返回“短信已发送”,第4次返回限流响应(code=429,msg=短信发送过于频繁),符合预期。

测试场景2:登录接口限流(滑动窗口,IP限流)

请求地址:http://localhost:8080/api/auth/login?username=test&password=123456
请求方式:POST
测试操作:第1次请求(0秒)、第2次请求(5秒)、第3次请求(8秒)
测试结果:前2次正常返回,第3次触发限流(10秒内超过2次),符合预期,无临界问题。

测试场景3:个人中心接口(用户ID限流)

请求地址:http://localhost:8080/api/user/profile
请求方式:GET
测试操作:1分钟内连续请求11次
测试结果:前10次正常返回,第11次触发限流,符合预期。

测试场景4:分布式限流(Redis存储)

启动2个项目节点(端口8080、8081),用同一IP分别向两个节点请求5次(共10次),时间窗口5秒
测试结果:两个节点合计请求超过5次后,触发限流,说明Redis分布式限流生效,多节点限流统一。

文末小结

SpringBoot + AOP 实现接口限流,是企业项目中保障系统稳定性的必备方案,核心逻辑就是「注解标记 + AOP 拦截 + 限流校验」,不侵入业务代码,灵活适配单机、分布式等多种场景。

以上就是基于SpringBoot+AOP实现接口限流的详细内容,更多关于SpringBoot AOP接口限流的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文