当前位置: 首页 > news >正文

软件工程实践五:Spring Boot 接口拦截与 API 监控、流量控制

适合人群:具备 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";         // 请求开始时间属性键@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {String requestId = UUID.randomUUID().toString();     // 生成请求唯一 ID,便于链路追踪MDC.put(REQ_ID, requestId);                          // 放入日志 MDCrequest.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 放行请求}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {// 可在此处添加对响应体或模型的额外处理}@Overridepublic 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"); // 代理链:第一个为原始客户端 IPif (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 {@Overridepublic 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;}@Overridepublic 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(); // 非阻塞获取令牌,失败立即返回 429if (!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();}@Overridepublic 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;}@Overridepublic 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:19736622Long 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>

作者:xuan
个人博客:https://blog.ybyq.wang
欢迎访问我的博客,获取更多技术文章和教程。

http://www.dtcms.com/a/391002.html

相关文章:

  • 【LINUX网络】NAT _ 代理_ 内网穿透
  • 智慧养老+数字大健康:当科技为“银发时代”按下温暖加速键
  • rook-ceph的ssd类osd的纠删码rgw存储池在迁移时的异常处理
  • Http升级Https使用Certbot申请证书并免费续期
  • scTenifoldKnk:“虚拟敲除基因”,查看转录组其他基因的变化幅度(升高or降低)
  • 牛客算法基础noob47 校门外的树
  • AD-GS:稀疏视角 3D Gaussian Splatting 的“交替致密化”,同时抑制浮游物与保留细节
  • maven package多出来一个xxx.jar.original和一个xxx-shaded.jar是什么?怎么去掉
  • Gin 框架中使用 Validator 进行参数校验的完整指南
  • apt install nvidia-cuda-toolkit后cuda不在/usr/local/cuda怎么办
  • SpringBoot整合Kafka总结
  • Parasoft C/C++test 针对 CMake 项目的自动化测试配置
  • LED强光手电筒MCU控制方案开发分析
  • linux中为什么 rm 命令能删除自己 | linux使用rm命令删自己会怎样?
  • django登录注册案例(下)
  • 【TES600G】基于JFM7K325T FPGA+FT-M6678 DSP的全国产化信号处理平台
  • 卷积神经网络深度解析:从基础原理到实战应用的完整指南
  • 企业档案管理系统:精准破局制造行业档案管理困境
  • 【完整源码+数据集+部署教程】考古坑洞私挖盗洞图像分割系统: yolov8-seg-act
  • MMDB详解
  • TC8:SOMEIP_ETS_130测试用例解析
  • 等效学习率翻倍?梯度累积三连坑:未除以 accum_steps、调度器步进错位、梯度裁剪/正则标度错误(含可复现实验与修复模板)
  • 嵌入式学习笔记(44)IMX6ULL
  • OpenStack 学习笔记(五):网络管理和虚拟网络实践与存储管理实验(下)
  • 博睿数据携手华为共筑智能未来,深度参与HUAWEI CONNECT 2025并发表主题演讲
  • 陈童理论物理新讲1 哈密顿力学初步
  • 9.19 Sass
  • 设计模式详解:单例模式、工厂方法模式、抽象工厂模式
  • 终端同居物语:Shell咏唱术式与权限结界の完全解析书
  • XeLaTeX 中文删除线自动换行问题的解决方案