最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

基于SpringBoot+AOP實(shí)現(xiàn)接口限流

 更新時(shí)間:2026年04月17日 09:36:30   作者:希望永不加班  
本文介紹了使用SpringBoot和AOP實(shí)現(xiàn)接口限流的方法,通過自定義注解、限流工具類、AOP切面等實(shí)現(xiàn)限流功能,支持多種限流策略、粒度和存儲(chǔ)方式,并提供了詳細(xì)的代碼示例和測試驗(yàn)證方法,需要的朋友可以參考下

前面我們用 AOP 實(shí)現(xiàn)了操作日志和權(quán)限校驗(yàn),徹底擺脫了代碼冗余的困擾;今天繼續(xù) AOP 實(shí)例——SpringBoot + AOP 實(shí)現(xiàn)接口限流。

做后端開發(fā)的同學(xué)都知道,接口限流是系統(tǒng)穩(wěn)定性的“第一道防線”:

比如登錄接口、短信驗(yàn)證碼接口、支付接口,很容易被惡意請求刷爆(比如頻繁調(diào)用發(fā)送短信、惡意登錄試錯(cuò)),導(dǎo)致系統(tǒng)響應(yīng)變慢、服務(wù)崩潰,甚至產(chǎn)生額外的費(fèi)用(短信費(fèi)、接口調(diào)用費(fèi))。

如果在每個(gè)接口中手動(dòng)寫限流邏輯,不僅代碼冗余,還難以統(tǒng)一管理和擴(kuò)展。而用 AOP 實(shí)現(xiàn)接口限流,只需一行注解,就能靈活控制接口的請求頻率,不侵入業(yè)務(wù)代碼,兼顧優(yōu)雅和實(shí)用。

一、接口限流的核心場景

接口限流的核心是“限制單位時(shí)間內(nèi)的請求次數(shù)”,結(jié)合企業(yè)實(shí)戰(zhàn)場景,本次需求覆蓋以下核心點(diǎn),可直接適配大部分項(xiàng)目:

  1. 多限流策略:支持固定窗口限流(簡單易實(shí)現(xiàn))和滑動(dòng)窗口限流(精準(zhǔn)度高,避免臨界問題),可靈活選擇;
  2. 自定義限流key:支持按 IP 限流(限制單個(gè)IP的請求頻率)、按用戶ID限流(限制單個(gè)用戶的請求頻率),適配不同場景;
  3. 自定義限流參數(shù):可靈活配置“單位時(shí)間”和“最大請求次數(shù)”(如 1分鐘內(nèi)最多請求10次、10秒內(nèi)最多請求3次);
  4. 統(tǒng)一限流響應(yīng):觸發(fā)限流時(shí),返回統(tǒng)一的 JSON 格式,包含錯(cuò)誤碼、錯(cuò)誤信息,便于前端提示用戶“請求過于頻繁”;
  5. 不侵入業(yè)務(wù)代碼:通過 AOP 增強(qiáng)實(shí)現(xiàn),業(yè)務(wù)接口無需修改,降低耦合度;
  6. 分布式適配:支持單機(jī)限流(本地緩存)和分布式限流(Redis),適配集群部署場景;
  7. 異常處理:限流邏輯異常時(shí),不影響接口正常訪問,保證系統(tǒng)穩(wěn)定性。

二、設(shè)計(jì)思路

在寫代碼前,先搞懂兩個(gè)核心限流策略(新手也能輕松理解),以及整體設(shè)計(jì)思路,避免寫代碼時(shí)邏輯混亂。

1. 兩種核心限流策略

(1)固定窗口限流

原理:將時(shí)間劃分為固定的窗口(如 1分鐘一個(gè)窗口),統(tǒng)計(jì)每個(gè)窗口內(nèi)的請求次數(shù),超過最大次數(shù)則觸發(fā)限流。

舉例:配置“1分鐘內(nèi)最多請求10次”,第一個(gè)窗口(0-60秒)請求10次后,后續(xù)請求被限流;60秒后進(jìn)入新窗口,請求次數(shù)重置,可再次請求。

優(yōu)點(diǎn):實(shí)現(xiàn)簡單、性能高;缺點(diǎn):存在臨界問題(比如59秒請求10次,61秒再請求10次,2秒內(nèi)請求20次,突破限流閾值)。

(2)滑動(dòng)窗口限流

原理:將固定窗口拆分為多個(gè)小窗口(如 1分鐘拆分為6個(gè)10秒小窗口),每次請求時(shí),只統(tǒng)計(jì)“當(dāng)前時(shí)間往前推1分鐘”內(nèi)的請求次數(shù),超過閾值則限流。

舉例:同樣配置“1分鐘內(nèi)最多請求10次”,59秒請求10次后,61秒請求時(shí),統(tǒng)計(jì)的是1-61秒內(nèi)的請求次數(shù)(仍為10次),會(huì)被限流,避免臨界問題。

優(yōu)點(diǎn):限流精準(zhǔn),無臨界問題;缺點(diǎn):實(shí)現(xiàn)稍復(fù)雜,性能略低于固定窗口。

2. 整體設(shè)計(jì)思路

  1. 自定義注解:創(chuàng)建 @RateLimit 注解,用于標(biāo)記需要限流的接口,配置限流策略、限流key、時(shí)間窗口、最大請求次數(shù);
  2. 限流工具類:分別實(shí)現(xiàn)固定窗口和滑動(dòng)窗口的限流邏輯,支持本地緩存(單機(jī))和 Redis(分布式)存儲(chǔ)請求次數(shù);
  3. AOP 切面:定義切點(diǎn)(攔截所有添加了 @RateLimit 注解的方法),用環(huán)繞通知實(shí)現(xiàn)限流校驗(yàn)邏輯;
  4. 限流key生成:根據(jù)注解配置,生成不同的限流key(IP/用戶ID),實(shí)現(xiàn)精準(zhǔn)限流;
  5. 統(tǒng)一異常與響應(yīng):觸發(fā)限流時(shí),拋出自定義限流異常,通過全局異常處理器返回統(tǒng)一 JSON 響應(yīng);
  6. 多場景測試:覆蓋單機(jī)/分布式、不同限流策略、不同限流key,驗(yàn)證限流效果。

三、完整代碼

本次實(shí)戰(zhàn)基于 SpringBoot 2.7.x 版本,整合 Redis(支持分布式限流),所有代碼添加詳細(xì)注釋,新手也能輕松理解每一步的作用,無需修改核心邏輯,直接適配項(xiàng)目。

步驟1:導(dǎo)入核心依賴

需要導(dǎo)入 AOP 依賴、Redis 依賴、工具包,pom.xml 如下:

<!-- Spring AOP 核心依賴(限流核心) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Redis 依賴(分布式限流必備,單機(jī)可省略) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 工具包(JSON 響應(yīng)、緩存操作,簡化代碼) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.32</version>
</dependency>
<!--  lombok 依賴(簡化實(shí)體類、工具類代碼) -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

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

配置 Redis、服務(wù)器端口,單機(jī)限流可省略 Redis 配置:

server:
  port: 8080 # 服務(wù)器端口
# Redis 配置(分布式限流必備)
spring:
  redis:
    host: localhost # Redis 地址(本地測試用)
    port: 6379 # Redis 端口
    password: # Redis 密碼(無密碼則留空)
    database: 0 # 數(shù)據(jù)庫索引
    lettuce:
      pool:
        max-active: 100 # 最大連接數(shù)
        max-idle: 10 # 最大空閑連接
        min-idle: 5 # 最小空閑連接
# 限流全局配置(可選,可在注解中覆蓋)
rate-limit:
  default-time: 60 # 默認(rèn)時(shí)間窗口(秒)
  default-count: 10 # 默認(rèn)最大請求次數(shù)
  default-type: FIXED_WINDOW # 默認(rèn)限流策略(FIXED_WINDOW:固定窗口,SLIDING_WINDOW:滑動(dòng)窗口)
  default-key-type: IP # 默認(rèn)限流key類型(IP:按IP限流,USER_ID:按用戶ID限流)

步驟3:自定義限流注解

創(chuàng)建 @RateLimit 注解,用于標(biāo)記需要限流的接口,可靈活配置限流參數(shù),貼合企業(yè)實(shí)戰(zhàn)需求:

import java.lang.annotation.*;
/**
 * 自定義接口限流注解
 * @Target(ElementType.METHOD):僅作用于方法(接口方法)
 * @Retention(RetentionPolicy.RUNTIME):運(yùn)行時(shí)保留,AOP 切面可獲取注解屬性
 * @Documented:生成 API 文檔時(shí),顯示該注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    /**
     * 限流策略(固定窗口/滑動(dòng)窗口)
     * 默認(rèn)為全局配置的策略,可在接口上單獨(dú)配置覆蓋
     */
    LimitType type() default LimitType.FIXED_WINDOW;
    /**
     * 限流key類型(按IP/按用戶ID)
     * 默認(rèn)為全局配置的key類型,可單獨(dú)覆蓋
     */
    KeyType keyType() default KeyType.IP;
    /**
     * 時(shí)間窗口(單位:秒)
     * 默認(rèn)為全局配置的時(shí)間,可單獨(dú)覆蓋(如 60=1分鐘,10=10秒)
     */
    int time() default 0;
    /**
     * 單位時(shí)間內(nèi)的最大請求次數(shù)(限流閾值)
     * 默認(rèn)為全局配置的次數(shù),可單獨(dú)覆蓋
     */
    int count() default 0;
    /**
     * 限流提示信息(觸發(fā)限流時(shí)返回)
     */
    String message() default "請求過于頻繁,請稍后再試!";
    /**
     * 限流存儲(chǔ)方式(本地緩存/Redis)
     * 默認(rèn)為 Redis,單機(jī)部署可改為 LOCAL
     */
    StoreType storeType() default StoreType.REDIS;
    /**
     * 限流策略枚舉
     */
    enum LimitType {
        FIXED_WINDOW,  // 固定窗口限流
        SLIDING_WINDOW // 滑動(dòng)窗口限流
    }
    /**
     * 限流key類型枚舉
     */
    enum KeyType {
        IP,        // 按請求IP限流(最常用)
        USER_ID    // 按當(dāng)前登錄用戶ID限流(需結(jié)合用戶上下文)
    }
    /**
     * 存儲(chǔ)方式枚舉
     */
    enum StoreType {
        LOCAL,  // 本地緩存(單機(jī)部署用)
        REDIS   // Redis(分布式部署用)
    }
}

注解屬性說明:

  • type:選擇限流策略,固定窗口簡單,滑動(dòng)窗口精準(zhǔn),可根據(jù)場景選擇;
  • keyType:選擇限流粒度,IP 用于匿名接口(如登錄、短信),USER_ID 用于登錄后接口(如個(gè)人中心);
  • time + count:共同定義限流規(guī)則,如 time=60、count=10 → 1分鐘內(nèi)最多請求10次;
  • storeType:單機(jī)部署用 LOCAL(本地緩存),集群部署用 REDIS(分布式緩存),保證限流統(tǒng)一。

步驟4:核心工具類

這部分是限流的核心,分別實(shí)現(xiàn)固定窗口、滑動(dòng)窗口的限流邏輯,支持本地緩存和 Redis 存儲(chǔ),代碼可直接復(fù)用:

4.1 限流常量類(統(tǒng)一管理key前綴)

/**
 * 限流常量類(統(tǒng)一管理 Redis/本地緩存的key前綴,避免混亂)
 */
public class RateLimitConstant {
    // 限流key前綴(Redis中使用,如 rate_limit:ip:127.0.0.1:接口路徑)
    public static final String RATE_LIMIT_KEY_PREFIX = "rate_limit:";
    // 滑動(dòng)窗口小窗口大?。J(rèn)10秒,可根據(jù)需求調(diào)整)
    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;
/**
 * 限流工具類(實(shí)現(xiàn)固定窗口、滑動(dòng)窗口限流,支持本地/Redis存儲(chǔ))
 */
@Slf4j
@Component
public class RateLimitUtil {
    // 本地緩存(單機(jī)限流用,ConcurrentHashMap 線程安全)
    private final ConcurrentHashMap&lt;String, Integer&gt; localCache = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Long> localWindowCache = new ConcurrentHashMap<>();
    // Redis 模板(分布式限流用)
    @Autowired(required = false) // 單機(jī)部署時(shí),Redis 可省略,避免報(bào)錯(cuò)
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 固定窗口限流(核心方法)
     * @param key 限流key(如 ip:127.0.0.1:接口路徑)
     * @param time 時(shí)間窗口(秒)
     * @param count 最大請求次數(shù)
     * @param storeType 存儲(chǔ)方式(本地/Redis)
     * @return true=觸發(fā)限流,false=未觸發(fā)限流
     */
    public boolean fixedWindowLimit(String key, int time, int count, RateLimit.StoreType storeType) {
        if (storeType == RateLimit.StoreType.LOCAL) {
            // 本地緩存實(shí)現(xiàn)固定窗口
            return localFixedWindowLimit(key, time, count);
        } else {
            // Redis 實(shí)現(xiàn)固定窗口(分布式)
            return redisFixedWindowLimit(key, time, count);
        }
    }
    /**
     * 滑動(dòng)窗口限流(核心方法)
     * @param key 限流key(如 ip:127.0.0.1:接口路徑)
     * @param time 時(shí)間窗口(秒)
     * @param count 最大請求次數(shù)
     * @param storeType 存儲(chǔ)方式(本地/Redis)
     * @return true=觸發(fā)限流,false=未觸發(fā)限流
     */
    public boolean slidingWindowLimit(String key, int time, int count, RateLimit.StoreType storeType) {
        if (storeType == RateLimit.StoreType.LOCAL) {
            // 本地緩存實(shí)現(xiàn)滑動(dòng)窗口
            return localSlidingWindowLimit(key, time, count);
        } else {
            // Redis 實(shí)現(xiàn)滑動(dòng)窗口(分布式)
            return redisSlidingWindowLimit(key, time, count);
        }
    }
    /**
     * 本地緩存 - 固定窗口限流
     */
    private boolean localFixedWindowLimit(String key, int time, int count) {
        // 1. 獲取當(dāng)前窗口的請求次數(shù)
        Integer currentCount = localCache.getOrDefault(key, 0);
        // 2. 檢查是否超過限流閾值
        if (currentCount >= count) {
            log.warn("本地固定窗口限流觸發(fā),key:{},當(dāng)前次數(shù):{},閾值:{}", key, currentCount, count);
            return true;
        }
        // 3. 第一次請求,設(shè)置窗口過期時(shí)間(time秒后清空)
        if (currentCount == 0) {
            localWindowCache.put(key, System.currentTimeMillis() + time * 1000);
        } else {
            // 檢查窗口是否過期,過期則重置次數(shù)和窗口時(shí)間
            Long expireTime = localWindowCache.get(key);
            if (System.currentTimeMillis() > expireTime) {
                localCache.put(key, 1);
                localWindowCache.put(key, System.currentTimeMillis() + time * 1000);
                return false;
            }
        }
        // 4. 未超過閾值,請求次數(shù)+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. 自增請求次數(shù)(原子操作,避免并發(fā)問題)
        Long currentCount = stringRedisTemplate.opsForValue().increment(redisKey, 1);
        // 3. 第一次請求,設(shè)置過期時(shí)間(time秒)
        if (currentCount != null && currentCount == 1) {
            stringRedisTemplate.expire(redisKey, time, TimeUnit.SECONDS);
        }
        // 4. 檢查是否超過限流閾值
        if (currentCount != null && currentCount > count) {
            log.warn("Redis固定窗口限流觸發(fā),key:{},當(dāng)前次數(shù):{},閾值:{}", redisKey, currentCount, count);
            return true;
        }
        return false;
    }
    /**
     * 本地緩存 - 滑動(dòng)窗口限流
     */
    private boolean localSlidingWindowLimit(String key, int time, int count) {
        long now = System.currentTimeMillis();
        // 1. 計(jì)算當(dāng)前窗口的起始時(shí)間(當(dāng)前時(shí)間 - 時(shí)間窗口)
        long windowStart = now - time * 1000;
        // 2. 拼接滑動(dòng)窗口的key(包含主key和小窗口時(shí)間)
        String windowKey = key + ":" + (now / (RateLimitConstant.SLIDING_WINDOW_INTERVAL * 1000));
        // 3. 獲取當(dāng)前小窗口的請求次數(shù)
        Integer currentWindowCount = localCache.getOrDefault(windowKey, 0);
        // 4. 遍歷所有小窗口,統(tǒng)計(jì)整個(gè)滑動(dòng)窗口內(nèi)的總請求次數(shù)
        int totalCount = 0;
        for (String cacheKey : localCache.keySet()) {
            if (cacheKey.startsWith(key + ":")) {
                // 解析小窗口時(shí)間
                long windowTime = Long.parseLong(cacheKey.split(":")[2]);
                long windowTimeMillis = windowTime * RateLimitConstant.SLIDING_WINDOW_INTERVAL * 1000;
                // 只統(tǒng)計(jì)當(dāng)前滑動(dòng)窗口內(nèi)的小窗口
                if (windowTimeMillis >= windowStart) {
                    totalCount += localCache.get(cacheKey);
                } else {
                    // 移除過期的小窗口緩存
                    localCache.remove(cacheKey);
                }
            }
        }
        // 5. 檢查是否超過限流閾值
        if (totalCount >= count) {
            log.warn("本地滑動(dòng)窗口限流觸發(fā),key:{},當(dāng)前總次數(shù):{},閾值:{}", key, totalCount, count);
            return true;
        }
        // 6. 未超過閾值,當(dāng)前小窗口請求次數(shù)+1
        localCache.put(windowKey, currentWindowCount + 1);
        return false;
    }
    /**
     * Redis - 滑動(dòng)窗口限流(分布式,集群部署用)
     */
    private boolean redisSlidingWindowLimit(String key, int time, int count) {
        long now = System.currentTimeMillis();
        // 1. 計(jì)算當(dāng)前窗口的起始時(shí)間(當(dāng)前時(shí)間 - 時(shí)間窗口)
        long windowStart = now - time * 1000;
        // 2. 拼接 Redis key(加上前綴)
        String redisKey = RateLimitConstant.RATE_LIMIT_KEY_PREFIX + key;
        // 3. 小窗口大?。J(rèn)10秒,可調(diào)整)
        int interval = RateLimitConstant.SLIDING_WINDOW_INTERVAL;
        // 4. 當(dāng)前小窗口的時(shí)間戳(按小窗口大小取整)
        long currentWindow = now / (interval * 1000);
        // 5. Redis 原子操作:刪除過期小窗口 + 統(tǒng)計(jì)當(dāng)前窗口總次數(shù) + 自增當(dāng)前小窗口次數(shù)
        // 用 Lua 腳本實(shí)現(xiàn)原子操作,避免并發(fā)問題
        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" +
                "-- 統(tǒng)計(jì)當(dāng)前窗口內(nèi)的總請求次數(shù)\n" +
                "local total = redis.call('ZCARD', key)\n" +
                "if total >= tonumber(count) then\n" +
                "    return 1\n" +
                "end\n" +
                "-- 自增當(dāng)前小窗口的請求次數(shù)(將小窗口時(shí)間戳作為score,請求ID作為value)\n" +
                "redis.call('ZADD', key, currentWindow, currentWindow .. ':' .. redis.call('INCR', key .. ':seq'))\n" +
                "-- 設(shè)置過期時(shí)間(確保緩存自動(dòng)清理)\n" +
                "redis.call('EXPIRE', key, tonumber(interval) + 1)\n" +
                "return 0";
        // 執(zhí)行 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. 結(jié)果判斷:1=觸發(fā)限流,0=未觸發(fā)
        if (result != null && result == 1) {
            log.warn("Redis滑動(dòng)窗口限流觸發(fā),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、用戶上下文)

實(shí)現(xiàn)獲取客戶端IP、當(dāng)前登錄用戶ID的工具類,用于生成限流key,貼合實(shí)戰(zhàn)場景:

5.1 IP工具類(獲取客戶端真實(shí)IP,處理代理場景)

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
/**
 * IP工具類(獲取客戶端真實(shí)IP,處理Nginx代理等場景)
 */
public class IpUtil {
    /**
     * 獲取客戶端真實(shí)IP
     */
    public static String getClientIp() {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return "127.0.0.1"; // 非web環(huán)境,默認(rèn)本地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();
        }
        // 處理多代理場景,取第一個(gè)非unknown的IP
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        // 本地環(huán)境默認(rèn)IP(避免localhost解析問題)
        return "0:0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : ip;
    }
}

5.2 用戶上下文

實(shí)際項(xiàng)目中,從JWT Token或Spring Security中獲取用戶ID,這里模擬實(shí)現(xiàn),可直接替換為項(xiàng)目中的真實(shí)邏輯:

/**
 * 用戶上下文(獲取當(dāng)前登錄用戶信息,用于按用戶ID限流)
 */
public class UserContext {
    /**
     * 獲取當(dāng)前登錄用戶ID(模擬,實(shí)際從JWT/Token中解析)
     * @return 用戶ID(未登錄返回null)
     */
    public static Long getCurrentUserId() {
        // 模擬:登錄用戶ID為1001,未登錄返回null
        // 實(shí)際項(xiàng)目替換為:JwtUtils.parseToken(token).getUserId()
        return 1001L;
    }
}

步驟6:AOP 限流切面

創(chuàng)建切面類,攔截所有添加了 @RateLimit 注解的接口,實(shí)現(xiàn)限流校驗(yàn)邏輯,優(yōu)先于日志切面執(zhí)行:

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:標(biāo)記此類為AOP切面
 * @Component:交給Spring管理,確保Spring能掃描到
 * @Order(1):優(yōu)先級(jí)高于日志切面(避免限流請求被記錄日志)
 * @Slf4j:日志輸出
 */
@Aspect
@Component
@Order(1)
@Slf4j
public class RateLimitAspect {
    // 注入限流工具類
    @Resource
    private RateLimitUtil rateLimitUtil;
    // 全局默認(rèn)配置(從配置文件讀?。?
    @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. 定義切點(diǎn):攔截所有添加了 @RateLimit 注解的方法
    @Pointcut("@annotation(com.example.demo.annotation.RateLimit)")
    public void rateLimitPointcut() {}
    // 2. 環(huán)繞通知:包裹目標(biāo)方法,執(zhí)行限流校驗(yàn)
    @Around("rateLimitPointcut()")
    public Object doRateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
        // 第一步:獲取目標(biāo)方法上的 @RateLimit 注解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = signature.getMethod();
        RateLimit rateLimitAnno = targetMethod.getAnnotation(RateLimit.class);
        // 第二步:獲取注解配置的限流參數(shù)(無配置則用全局默認(rèn)值)
        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(根據(jù)keyType生成,確保唯一)
        String limitKey = generateLimitKey(joinPoint, keyType);
        log.info("接口限流校驗(yàn),key:{},策略:{},時(shí)間窗口:{}秒,閾值:{}次",
                limitKey, limitType, time, count);
        // 第四步:執(zhí)行限流校驗(yàn)(根據(jù)限流策略選擇對應(yīng)的方法)
        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);
        }
        // 第五步:判斷是否觸發(fā)限流,觸發(fā)則拋出異常
        if (isLimit) {
            throw new RateLimitException(429, message);
        }
        // 第六步:限流校驗(yàn)通過,執(zhí)行目標(biāo)方法(核心業(yè)務(wù)邏輯)
        return joinPoint.proceed();
    }
    /**
     * 生成限流key(確保唯一,避免不同接口/不同IP/不同用戶的限流沖突)
     * @param joinPoint 切入點(diǎn)(獲取接口路徑)
     * @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();
        // 根據(jù)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;
        }
        // 默認(rèn)按IP限流
        String ip = IpUtil.getClientIp();
        return "ip:" + ip + ":" + methodName;
    }
}

步驟7:自定義限流異常 + 全局異常處理器

觸發(fā)限流時(shí),拋出自定義異常,通過全局異常處理器返回統(tǒng)一的 JSON 響應(yīng),便于前端統(tǒng)一處理:

7.1 自定義限流異常

import lombok.Data;
import lombok.EqualsAndHashCode;
/**
 * 自定義限流異常(觸發(fā)限流時(shí)拋出)
 * 429 狀態(tài)碼:Too Many Requests(請求過于頻繁)
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class RateLimitException extends RuntimeException {
    // 錯(cuò)誤碼(429 標(biāo)準(zhǔn)限流狀態(tài)碼)
    private Integer code;
    // 錯(cuò)誤信息(自定義提示)
    private String message;
    // 構(gòu)造方法(簡化異常拋出)
    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;
/**
 * 全局異常處理器(統(tǒng)一響應(yīng)格式)
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
    // 攔截限流異常(429 狀態(tài)碼)
    @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", "服務(wù)器內(nèi)部異常,請聯(lián)系管理員");
        response.put("data", null);
        return response;
    }
}

步驟8:接口使用示例

在需要限流的接口上添加 @RateLimit 注解,根據(jù)業(yè)務(wù)需求配置參數(shù),無需修改接口內(nèi)部業(yè)務(wù)代碼:

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:短信驗(yàn)證碼接口(按IP限流,固定窗口,1分鐘最多3次)
     * 高頻場景,防止惡意刷短信
     */
    @RateLimit(
            type = RateLimit.LimitType.FIXED_WINDOW,
            keyType = RateLimit.KeyType.IP,
            time = 60, // 1分鐘
            count = 3, // 最多3次
            message = "短信發(fā)送過于頻繁,請1分鐘后再試!"
    )
    @PostMapping("/sms/send")
    public String sendSms(String phone) {
        // 核心業(yè)務(wù)邏輯:發(fā)送短信驗(yàn)證碼
        return "短信已發(fā)送至:" + phone;
    }
    /**
     * 場景2:登錄接口(按IP限流,滑動(dòng)窗口,10秒最多2次)
     * 防止惡意暴力破解密碼,滑動(dòng)窗口避免臨界問題
     */
    @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) {
        // 核心業(yè)務(wù)邏輯:用戶登錄
        return "登錄成功,歡迎您:" + username;
    }
    /**
     * 場景3:個(gè)人中心接口(按用戶ID限流,固定窗口,1分鐘最多10次)
     * 登錄后接口,按用戶ID限流,避免單個(gè)用戶惡意請求
     */
    @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();
        // 核心業(yè)務(wù)邏輯:查詢用戶個(gè)人信息
        return "用戶ID:" + userId + ",個(gè)人信息查詢成功";
    }
    /**
     * 場景4:分布式限流(Redis存儲(chǔ),滑動(dòng)窗口,5秒最多5次)
     * 集群部署場景,確保多節(jié)點(diǎn)限流統(tǒng)一
     */
    @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() {
        // 核心業(yè)務(wù)邏輯:分布式場景測試
        return "分布式限流測試成功";
    }
}

四、測試驗(yàn)證

用 Postman 測試以下核心場景,驗(yàn)證限流效果,確保符合預(yù)期:

測試場景1:短信接口限流(固定窗口,IP限流)

請求地址:http://localhost:8080/api/sms/send?phone=13800138000
請求方式:POST
測試操作:1分鐘內(nèi)連續(xù)請求4次
測試結(jié)果:前3次正常返回“短信已發(fā)送”,第4次返回限流響應(yīng)(code=429,msg=短信發(fā)送過于頻繁),符合預(yù)期。

測試場景2:登錄接口限流(滑動(dòng)窗口,IP限流)

請求地址:http://localhost:8080/api/auth/login?username=test&password=123456
請求方式:POST
測試操作:第1次請求(0秒)、第2次請求(5秒)、第3次請求(8秒)
測試結(jié)果:前2次正常返回,第3次觸發(fā)限流(10秒內(nèi)超過2次),符合預(yù)期,無臨界問題。

測試場景3:個(gè)人中心接口(用戶ID限流)

請求地址:http://localhost:8080/api/user/profile
請求方式:GET
測試操作:1分鐘內(nèi)連續(xù)請求11次
測試結(jié)果:前10次正常返回,第11次觸發(fā)限流,符合預(yù)期。

測試場景4:分布式限流(Redis存儲(chǔ))

啟動(dòng)2個(gè)項(xiàng)目節(jié)點(diǎn)(端口8080、8081),用同一IP分別向兩個(gè)節(jié)點(diǎn)請求5次(共10次),時(shí)間窗口5秒
測試結(jié)果:兩個(gè)節(jié)點(diǎn)合計(jì)請求超過5次后,觸發(fā)限流,說明Redis分布式限流生效,多節(jié)點(diǎn)限流統(tǒng)一。

文末小結(jié)

SpringBoot + AOP 實(shí)現(xiàn)接口限流,是企業(yè)項(xiàng)目中保障系統(tǒng)穩(wěn)定性的必備方案,核心邏輯就是「注解標(biāo)記 + AOP 攔截 + 限流校驗(yàn)」,不侵入業(yè)務(wù)代碼,靈活適配單機(jī)、分布式等多種場景。

以上就是基于SpringBoot+AOP實(shí)現(xiàn)接口限流的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot AOP接口限流的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Spring解決循環(huán)依賴問題的四種方法匯總

    Spring解決循環(huán)依賴問題的四種方法匯總

    這篇文章主要介紹了Spring解決循環(huán)依賴問題的四種方法匯總,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧
    2024-07-07
  • java實(shí)現(xiàn)網(wǎng)頁爬蟲的示例講解

    java實(shí)現(xiàn)網(wǎng)頁爬蟲的示例講解

    下面小編就為大家?guī)硪黄猨ava實(shí)現(xiàn)網(wǎng)頁爬蟲的示例講解。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-08-08
  • Java中的== 和equals()方法詳解與實(shí)例

    Java中的== 和equals()方法詳解與實(shí)例

    本篇文章介紹了,在java中"==" 與equals方法的使用及其實(shí)例,需要的朋友可以參考下
    2017-04-04
  • 教你java面試時(shí)如何聊單例模式

    教你java面試時(shí)如何聊單例模式

    這篇文章主要給大家介紹了關(guān)于Java單例模式推薦的幾種模式,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用Java具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-06-06
  • 使用spring?security?BCryptPasswordEncoder接入系統(tǒng)

    使用spring?security?BCryptPasswordEncoder接入系統(tǒng)

    這篇文章主要介紹了使用spring?security?BCryptPasswordEncoder接入系統(tǒng)方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-08-08
  • SpringBoot?自定義注解實(shí)現(xiàn)涉密字段脫敏

    SpringBoot?自定義注解實(shí)現(xiàn)涉密字段脫敏

    關(guān)于數(shù)據(jù)脫敏,網(wǎng)上的文章都是硬編碼規(guī)則,比如對身份證,手機(jī)號(hào),郵件地址等固定寫法脫敏。本文在此基礎(chǔ)上,拓展動(dòng)態(tài)從數(shù)據(jù)庫查出涉密關(guān)鍵字執(zhí)行脫敏操作。感興趣的同學(xué)可以參考閱讀
    2023-03-03
  • 告別無盡等待:Java中的輪詢終止技巧

    告別無盡等待:Java中的輪詢終止技巧

    在Java中,輪詢是一種常見的處理方式,用于檢查某個(gè)條件是否滿足,直到滿足條件或達(dá)到一定的時(shí)間限制,本文將介紹Java中常用的輪詢結(jié)束方式,包括使用循環(huán)、定時(shí)器和線程池等方法,需要的朋友可以參考下
    2023-10-10
  • java @Data布爾值boolean的坑及解決

    java @Data布爾值boolean的坑及解決

    本文介紹了在使用Spring框架時(shí),遇到的一個(gè)屬性命名規(guī)則問題,在Spring框架中,如果類的屬性名稱第一個(gè)字母小寫,第二個(gè)字母大寫,那么在調(diào)用set方法時(shí),Spring會(huì)將屬性的后面的字母轉(zhuǎn)換為小寫,這種情況下,如果下游消費(fèi)端調(diào)用得到的返回json串
    2024-10-10
  • 解決response.setHeader設(shè)置下載文件名無效的問題

    解決response.setHeader設(shè)置下載文件名無效的問題

    這篇文章主要介紹了解決response.setHeader設(shè)置下載文件名無效的問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-01-01
  • 解決java.lang.ClassCastException的java類型轉(zhuǎn)換異常的問題

    解決java.lang.ClassCastException的java類型轉(zhuǎn)換異常的問題

    這篇文章主要介紹了解決java.lang.ClassCastException的java類型轉(zhuǎn)換異常的問題,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-09-09

最新評(píng)論

炉霍县| 收藏| 惠安县| 鄱阳县| 金寨县| 高邮市| 基隆市| 汉川市| 沂源县| 普格县| 高邮市| 瓮安县| 枣强县| 宜黄县| 定结县| 松滋市| 华坪县| 昭苏县| 郧西县| 南郑县| 鄢陵县| 河源市| 东宁县| 邵阳县| 东光县| 诸暨市| 灌南县| 兴化市| 台州市| 岳西县| 张家港市| 尼玛县| 南开区| 玛多县| 响水县| 绥化市| 北京市| 白水县| 三原县| 罗田县| 呼伦贝尔市|