当前位置: 代码网 > it编程>编程语言>Java > 基于SpringBoot+AOP实现接口限流

基于SpringBoot+AOP实现接口限流

2026年04月17日 Java 我要评论
前面我们用 aop 实现了操作日志和权限校验,彻底摆脱了代码冗余的困扰;今天继续 aop 实例——springboot + 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(分布式部署用)
    }
}

注解属性说明:

  • type:选择限流策略,固定窗口简单,滑动窗口精准,可根据场景选择;
  • keytype:选择限流粒度,ip 用于匿名接口(如登录、短信),user_id 用于登录后接口(如个人中心);
  • time + count:共同定义限流规则,如 time=60、count=10 → 1分钟内最多请求10次;
  • storetype:单机部署用 local(本地缓存),集群部署用 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接口限流的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com