防止惡意刷單、爬蟲、DDoS 等高頻調用,需在控制器方法級別聲明限流規則,例如「同一 IP 5 秒內最多 10 次」。本文實現基於 AOP 的 @RequestLimit 註解,採用本地 HashMap 存儲計數器,適合單體/小規模微服務;分布式場景請改用 Redis + Lua 腳本。
2. 異常類
public class RequestLimitException extends RuntimeException { public RequestLimitException() { super("請求頻率超過限制"); } public RequestLimitException(String msg) { super(msg); } }3. 切面實現(核心邏輯)
@Aspect @Component public class RequestLimitAspect { private static final Logger log = LoggerFactory.getLogger(RequestLimitAspect.class); // Key: "req_limit_" + URL + IP private final Map<String, Integer> counter = new ConcurrentHashMap<>(); @Before("@annotation(limit) && within(@org.springframework.stereotype.Controller *)") public void doLimit(JoinPoint jp, RequestLimit limit) { HttpServletRequest req = extractRequest(jp); if (req == null) throw new RequestLimitException("缺少 HttpServletRequest 參數"); String key = "req_limit_" + req.getRequestURL() + "_" + req.getRemoteAddr(); int current = counter.compute(key, (k, v) -> v == null ? 1 : v + 1); // 首次請求啟動定時清理任務 if (current == 1) { new Timer().schedule(new TimerTask() { @Override public void run() { counter.remove(key); } }, limit.time()); } if (current > limit.count()) { log.warn("IP [{}] 訪問 [{}] 超過限制 {} 次/{}ms", req.getRemoteAddr(), req.getRequestURL(), limit.count(), limit.time()); throw new RequestLimitException(); } } private HttpServletRequest extractRequest(JoinPoint jp) { return Arrays.stream(jp.getArgs()) .filter(HttpServletRequest.class::isInstance) .map(HttpServletRequest.class::cast) .findFirst().orElse(null); } }4. 控制器使用示例
5. 全局異常處理(返回友好 JSON)
handleLimit(RequestLimitException e) { return Map.of("code", 429, "msg", e.getMessage(), "retryAfter", "5s"); } }局限性與擴展方向
| 局限 | 擴展方案 |
|---|---|
| 單機 HashMap,重啟丟失、多實例不共享 | 改用 Redis INCR + EXPIRE 或 Redisson RateLimiter |
| Timer 任務積累可能導致 OOM | 改用 ScheduledExecutorService 或 Caffeine 過期策略 |
| 僅支援 Controller 方法,Filter/Interceptor 層面無效 | 實現 OncePerRequestFilter 統一攔截所有入口 |
| 無區分用戶維度(僅 IP) | 結合 JWT Token 解析 userId 作為 Key 一部分 |