适合人群:具备 Spring Web 基础、希望在接口层做统一治理的开发者或技术爱好者。
建议先看:软件工程实践二(Spring Boot 知识回顾)、实践三(RESTful API 设计原则)。

1. 方案选型速览

  • Filter(Servlet 层):最靠前,处理包括静态资源;适合全局跨域、XSS、统一编码等。
  • HandlerInterceptor(Spring MVC 层):在进入 Controller 前后执行;适合接口打点、鉴权、限流、审计。
  • AOP(方法层):对注解/切点进行横切;适合注解式限流、审计、幂等等。
  • API Gateway(边界层):最靠外;适合全局流控、黑白名单、熔断、灰度,一处管多服务。

一般应用内优先用 Interceptor + AOP;如果有网关,也可在网关做粗粒度限流,在应用内做精细化限流与监控。

国内比较熟悉的:Sentinel


2. 基于 HandlerInterceptor 的接口监控

目标:记录每次请求的关键信息与耗时,并输出到日志或指标系统。

示例:

// RequestLoggingInterceptor.java
// 功能:为每个进入的 HTTP 请求生成请求ID、记录入站/出站日志与耗时
package com.example.demo.web;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import java.util.UUID;

public class RequestLoggingInterceptor implements HandlerInterceptor {
    private static final Logger log = LoggerFactory.getLogger(RequestLoggingInterceptor.class);
    private static final String REQ_ID = "reqId";          // MDC 中的请求 ID 键
    private static final String START = "startAt";         // 请求开始时间属性键

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String requestId = UUID.randomUUID().toString();     // 生成请求唯一 ID,便于链路追踪
        MDC.put(REQ_ID, requestId);                          // 放入日志 MDC
        request.setAttribute(START, System.currentTimeMillis()); // 记录起始时间
        String method = request.getMethod();
        String uri = request.getRequestURI();
        String ip = getClientIp(request);                    // 识别真实客户端 IP(支持代理场景)
        log.info("[IN] {} {} ip={} ua={}", method, uri, ip, request.getHeader("User-Agent"));
        return true;                                         // 返回 true 放行请求
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
        // 可在此处添加对响应体或模型的额外处理
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        Long start = (Long) request.getAttribute(START);
        long costMs = start == null ? -1 : (System.currentTimeMillis() - start); // 计算耗时
        int status = response.getStatus();
        if (ex != null) {
            log.warn("[OUT] status={} costMs={} ex={}", status, costMs, ex.toString()); // 异常时告警
        } else {
            log.info("[OUT] status={} costMs={}", status, costMs);                        // 正常返回
        }
        MDC.remove(REQ_ID); // 清理 MDC,避免线程复用造成污染
    }

    private String getClientIp(HttpServletRequest request) {
        String xff = request.getHeader("X-Forwarded-For"); // 代理链:第一个为原始客户端 IP
        if (xff != null && !xff.isEmpty()) {
            return xff.split(",")[0].trim();
        }
        String realIp = request.getHeader("X-Real-IP");    // 一些代理使用该头
        if (realIp != null && !realIp.isEmpty()) {
            return realIp;
        }
        return request.getRemoteAddr();                      // 直接连接时的远端地址
    }
}

注册拦截器:

// WebMvcConfig.java
// 功能:注册全局拦截器,并对健康检查/文档/静态资源等路径放行
package com.example.demo.config;

import com.example.demo.web.RequestLoggingInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RequestLoggingInterceptor())    // 注册请求日志拦截器
                .addPathPatterns("/**")                             // 默认拦截所有路径
                .excludePathPatterns(                               // 放行以下常见非业务路径
                        "/actuator/**",
                        "/swagger-resources/**",
                        "/v3/api-docs/**",
                        "/doc.html",
                        "/swagger-ui/**",
                        "/error",
                        "/static/**"
                );
    }
}

3. log配置文件 logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 控制台输出 -->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- %X 代表输出全部MDC内容,%X{key}代表指定key -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %X%n</pattern>
        </encoder>
    </appender>

    <!-- 文件输出,可以按需配置 -->
    <appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>logs/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg %X%n</pattern>
        </encoder>
    </appender>

    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="file"/>
    </root>
</configuration>

4. 流量控制的常见策略与算法

  • 维度:全局、按接口、按 IP、按用户、按来源(AppId)、按租户等
  • 算法

    • 固定窗口(simple, 可能边界抖动)
    • 滑动窗口(更平滑)
    • 令牌桶(Token Bucket,允许突发)
    • 漏桶(Leaky Bucket,恒定速率)
  • 选型建议

    • 单机快速落地:Guava RateLimiter 或 Bucket4j
    • 分布式一致性:Redis
    • 网关层粗粒度 + 应用内细粒度

5. 本地单机限流:Guava RateLimiter(简单上手)

<!-- Maven 依赖:Guava 提供本地令牌桶(RateLimiter) -->
<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>33.0.0-jre</version>
</dependency>
// SimpleRateLimitInterceptor.java
// 功能:针对 URI 维度基于 Guava RateLimiter 的单机限流
package com.example.demo.limit;

import com.google.common.util.concurrent.RateLimiter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class SimpleRateLimitInterceptor implements HandlerInterceptor {
    private final ConcurrentMap<String, RateLimiter> uriToLimiter = new ConcurrentHashMap<>(); // 每个 URI 一个限流器
    private final double permitsPerSecond; // 每秒允许的请求数(QPS)

    public SimpleRateLimitInterceptor(double permitsPerSecond) {
        this.permitsPerSecond = permitsPerSecond;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String key = request.getRequestURI(); // 维度选择:可替换为 URI+IP/用户ID 等
        RateLimiter limiter = uriToLimiter.computeIfAbsent(key, k -> RateLimiter.create(permitsPerSecond));
        boolean allowed = limiter.tryAcquire(); // 非阻塞获取令牌,失败立即返回 429
        if (!allowed) {
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            byte[] body = "Too Many Requests".getBytes(StandardCharsets.UTF_8);
            response.getOutputStream().write(body);
            return false;
        }
        return true;
    }
}

注册:

// 将限流器应用到 /api/** 的请求路径
registry.addInterceptor(new SimpleRateLimitInterceptor(50.0)) // 每秒 50 次/接口
        .addPathPatterns("/api/**");

优点:简单易用;缺点:单机生效,不适合分布式一致性。


6. 本地单机限流:Bucket4j(功能更强)

<!-- Bucket4j:功能丰富的令牌桶库,支持多种后端集成(Redis/Hazelcast) -->
<dependency>
    <groupId>com.bucket4j</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.10.1</version>
</dependency>
// Bucket4jRateLimitInterceptor.java
// 功能:基于 Bucket4j 的单机令牌桶限流
package com.example.demo.limit;

import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;

import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

public class Bucket4jRateLimitInterceptor implements HandlerInterceptor {
    private final ConcurrentMap<String, Bucket> bucketMap = new ConcurrentHashMap<>(); // 每个 URI 一个桶

    private Bucket newBucket() {
        // 令牌桶配置:容量 100,按每秒补充 50 个令牌;允许一定突发
        Bandwidth limit = Bandwidth.classic(100, Refill.intervally(50, Duration.ofSeconds(1)));
        return Bucket.builder().addLimit(limit).build();
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String key = request.getRequestURI();
        Bucket bucket = bucketMap.computeIfAbsent(key, k -> newBucket()); // 延迟创建桶
        if (bucket.tryConsume(1)) { // 消耗 1 个令牌
            return true;
        }
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
        response.getOutputStream().write("Too Many Requests".getBytes(StandardCharsets.UTF_8));
        return false;
    }
}

优势:策略多、可配额外指标;劣势:默认仍是单机。可选用 bucket4j-redis 或 Hazelcast 实现分布式令牌桶。


7. 分布式限流

思路:使用固定窗口计数法(Fixed Window)。对某个限流 key(如 接口+IP),在一个窗口期内使用 INCR 计数,首次创建时设置 EXPIRE 为窗口大小,到期自动清除。无需 Lua,保证实现简单、易维护。

特性:

  • 简单高效;适合大多数分布式限流需求。
  • 边界在窗口切换时可能出现短暂突发(固定窗口特性)。若需更平滑的滑动窗口,可使用 Redis 事务(WATCH/MULTI/EXEC)或 Lua(本文不使用 Lua)。
// RedisFixedWindowRateLimitInterceptor.java
// 功能:基于 Redis 的固定窗口限流(纯 Redis 命令:INCR + EXPIRE)
package com.example.demo.limit;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.HandlerInterceptor;

import java.nio.charset.StandardCharsets;
import java.time.Duration;

public class RedisFixedWindowRateLimitInterceptor implements HandlerInterceptor {
    private final StringRedisTemplate redis;
    private final int maxReq;        // 窗口内最大请求数
    private final Duration window;   // 窗口大小

    public RedisFixedWindowRateLimitInterceptor(StringRedisTemplate redis, int maxReq, Duration window) {
        this.redis = redis;
        this.maxReq = maxReq;
        this.window = window;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String baseKey = buildBaseKey(request); // 例如: rl:{uri}:{ip}
        // 将窗口时间片纳入 key,避免首次请求起始时间的不确定性
        long windowMs = window.toMillis();
        long bucket = System.currentTimeMillis() / windowMs;
        String key = baseKey + ":" + bucket; // 形如 rl:/api/xx:1.2.3.4:19736622

        Long count = redis.opsForValue().increment(key);
        if (count != null && count == 1L) {
            // 首次创建时设置过期时间,避免键长期滞留
            redis.expire(key, window);
        }
        if (count != null && count <= maxReq) {
            return true;
        }
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
        response.getOutputStream().write("Too Many Requests".getBytes(StandardCharsets.UTF_8));
        return false;
    }

    private String buildBaseKey(HttpServletRequest request) {
        String uri = request.getRequestURI();
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) ip = request.getRemoteAddr();
        return "rl:" + uri + ":" + ip; // key 结构:rl:/api/xxx:1.2.3.4
    }
}

注册:

// 在 /api/** 启用分布式固定窗口限流:1 秒内最多 100 次
registry.addInterceptor(new RedisFixedWindowRateLimitInterceptor(stringRedisTemplate, 100, Duration.ofSeconds(1)))
        .addPathPatterns("/api/**");

说明:

  • key 维度可按需自定义(接口/用户/租户/来源等)。
  • 如果需要更平滑的限流,可在应用层做“本地桶 + Redis 合并”或升级为滑动窗口(仍可用纯 Redis 事务实现)。
  • 返回 429(Too Many Requests)。

8. 注解式限流(AOP)

定义注解:

// RateLimit.java
// 功能:声明式限流参数,支持在类或方法上使用
package com.example.demo.limit;

import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    int max() default 100;                   // 窗口内最大请求数
    long windowMs() default 1000;            // 窗口大小(毫秒)
    String key() default "#request.requestURI"; // 基于 SpEL 的 key 表达式
}

切面实现(示例改为每个 key 一个 Guava RateLimiter;按注解参数换算速率):

@Aspect
@Component
public class RateLimitAspect {
    private final HttpServletRequest request;
    //    private final ConcurrentMap<String, RateLimiter> uriToLimiter = new ConcurrentHashMap<>();// 
    private final ConcurrentMap<String, Bucket> bucketMap = new ConcurrentHashMap<>(); // 每个 URI 一个桶

    public RateLimitAspect(HttpServletRequest request) {
        this.request = request;
    }

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
        String key = buildKey(rateLimit); // 可结合 SpEL 动态生成
        double permitsPerSecond = calcPermitsPerSecond(rateLimit.max(), rateLimit.windowMs());
//        RateLimiter limiter = uriToLimiter.computeIfAbsent(key, k -> RateLimiter.create(permitsPerSecond));
        Bucket bucket = bucketMap.computeIfAbsent(key, (k)->{
            Bandwidth limit = Bandwidth.classic(Long.parseLong(permitsPerSecond + ""),
                    Refill.intervally(Long.parseLong(permitsPerSecond + ""), Duration.ofSeconds(1)));
             return Bucket.builder().addLimit(limit).build();
        });
        
        boolean allowed = bucket.tryConsume(1);
        if (allowed) {
            return pjp.proceed();
        }
        throw new ResponseStatusException(HttpStatus.TOO_MANY_REQUESTS, "Too Many Requests");
    }

    private String buildKey(RateLimit rateLimit) {
        String uri = request.getRequestURI();
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty()) ip = request.getRemoteAddr();
        return "rl:" + uri + ":" + ip;
    }

    private double calcPermitsPerSecond(int max, long windowMs) {
        if (windowMs <= 0) return Double.POSITIVE_INFINITY;
        double sec = windowMs / 1000.0;
        double qps = max / sec;
        return Math.max(qps, 0.000001); // 避免 0
    }
}

使用:

// 在 1 秒窗口内限制最多 20 次
@RateLimit(max = 20, windowMs = 1000)
@GetMapping("/api/resource")
public String resource() { return "ok"; }

9. 统一异常与统一返回

// GlobalExceptionHandler.java
// 功能:统一将异常转换为标准响应体,便于前端与调用方处理
package com.example.demo.web;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.server.ResponseStatusException;

import java.util.HashMap;
import java.util.Map;

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(ResponseStatusException.class)
    public ResponseEntity<Map<String, Object>> handle(ResponseStatusException ex) {
        Map<String, Object> body = new HashMap<>();
        body.put("code", ex.getStatusCode().value());  // 与 HTTP 状态一致
        body.put("message", ex.getReason());           // 由抛出方提供的原因
        return new ResponseEntity<>(body, ex.getStatusCode());
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handle(Exception ex) {
        Map<String, Object> body = new HashMap<>();
        body.put("code", 500);
        body.put("message", ex.getMessage());          // 生产可隐藏或映射为通用提示
        return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

10. 网关与真实 IP

  • 使用 Nginx/Ingress 时,转发需设置并在应用端读取 X-Forwarded-ForX-Real-IP
  • 若部署在网关后:

    • 网关做全局粗粒度限流(防 DDoS),
    • 应用内做细粒度限流(按用户/接口)。

11. 压测与验证

  • curl 验证:

    # 并发 100,持续 10s(需要已安装 hey)
    hey -z 10s -c 100 http://localhost:8080/api/resource
  • 观察日志中 [IN]/[OUT] 与 429;
  • 查看 /actuator/prometheus 指标,并在 Grafana 绘制 QPS、P95、限流次数等。

12. 最佳实践清单

  • 日志与指标:所有入口统一打点(请求、状态、耗时、异常)。
  • 限流维度:按场景选择(全局/接口/用户/IP/租户)。
  • 算法选择:突发流量多用令牌桶;需要平滑用滑动窗口。
  • 分布式一致性:使用 Redis/Lua 或 Bucket4j 分布式支持。
  • 白名单/开关:在限流前置判断白名单;提供动态阈值与紧急开关。
  • 安全:注意绕过手段(多 IP、代理);对敏感接口更严格。
  • 性能:Redis 压力大时做本地缓存合并、批量/管道、热点降级。
  • 观测:Actuator + Micrometer + Prometheus + Grafana,统一观察服务健康。

13. 参考依赖清单(择需)

<dependencies>
  <!-- Spring Boot Web -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>

  <!-- Actuator:健康检查/指标暴露 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
  </dependency>

  <!-- Micrometer Prometheus:注册 Prometheus 指标 -->
  <dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
  </dependency>

  <!-- Redis:用于分布式限流、缓存等 -->
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
  </dependency>

  <!-- 可选:Guava 或 Bucket4j(本地限流) -->
  <dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>33.0.0-jre</version>
  </dependency>
  <dependency>
    <groupId>com.github.vladimir-bukhtoyarov</groupId>
    <artifactId>bucket4j-core</artifactId>
    <version>8.10.1</version>
  </dependency>
</dependencies>
最后修改:2025 年 09 月 18 日
如果觉得我的文章对你有用,请随意赞赏
END
本文作者:
文章标题:软件工程实践五:Spring Boot 接口拦截与 API 监控、流量控制
本文地址:https://blog.ybyq.wang/archives/1115.html
版权说明:若无注明,本文皆Xuan's blog原创,转载请保留文章出处。