前言
之前写了个轻量级的 Spring Boot 接口防护框架 Guardian,陆续做了防重复提交、接口限流、接口幂等三个功能,发到了 Maven Central 开源。
最近又加了三个实用功能:参数自动 Trim、慢接口检测、请求链路追踪,同时在 v1.5.2 新增了配置中心动态刷新能力。现在 Guardian 一共六个模块,覆盖了 API 请求层最常见的防护需求,所有 YAML 配置都支持通过 Nacos / Apollo 等配置中心动态修改、即时生效,无需重启。
每个模块独立 Starter,用哪个引哪个,互不依赖。最快只需要引个依赖就能用,零配置。
项目地址(源码 + 示例 + 文档全在里面):
- GitHub:github.com/BigGG-Guard… ← 顺手点个 Star,不迷路
- Gitee(镜像同步):gitee.com/BigGG-Guard…
功能一览
| 功能 | Starter | 注解 | YAML | 说明 |
|---|---|---|---|---|
| 防重复提交 | guardian-repeat-submit-spring-boot-starter | @RepeatSubmit | ✅ | 防止用户重复提交表单/请求 |
| 接口限流 | guardian-rate-limit-spring-boot-starter | @RateLimit | ✅ | 滑动窗口 + 令牌桶,双算法可选 |
| 接口幂等 | guardian-idempotent-spring-boot-starter | @Idempotent | — | Token 机制保证接口幂等性,支持结果缓存 |
| 参数自动Trim | guardian-auto-trim-spring-boot-starter | — | ✅ | 自动去除请求参数首尾空格 + 不可见字符替换 |
| 慢接口检测 | guardian-slow-api-spring-boot-starter | @SlowApiThreshold | ✅ | 慢接口自动告警 + Top N 统计 + Actuator 端点 |
| 请求链路追踪 | guardian-trace-spring-boot-starter | — | ✅ | 自动生成/透传 TraceId,MDC 日志串联 |
下面一个一个说。
一、防重复提交
什么场景需要?
用户点了提交按钮,前端没做防抖,或者网络慢用户多点了几下。后端收到三个一模一样的请求,创建了三个订单。
防重复提交就是解决这个问题:同一个请求短时间内别让它提交两次。
先看效果
三步搞定。
第一步,引依赖:
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-repeat-submit-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
第二步,加注解:
@PostMapping("/submit")
@RepeatSubmit(interval = 10, message = "订单正在处理,请勿重复提交")
public Result submitOrder(@RequestBody OrderDTO order) {
return orderService.submit(order);
}
第三步,没了。启动项目就生效了。
10 秒内同一个用户、同一个接口、同样的请求参数,第二次请求会被直接拦截。
完整可运行的示例代码在仓库的 guardian-example 模块里,各种场景都有,clone 下来直接跑。
YAML 批量配置
单个接口用注解挺方便,但如果有 50 个接口都要配防重,一个一个加注解就有点累了。支持在 YAML 里用 AntPath 通配符批量配置:
guardian:
repeat-submit:
storage: redis
key-encrypt: md5
urls:
- pattern: /api/order/**
interval: 10
key-scope: user
message: "订单正在处理,请勿重复提交"
- pattern: /api/sms/send
interval: 60
key-scope: ip
exclude-urls:
- /api/public/**
- /api/health
几个要点:
- 白名单(
exclude-urls)优先级最高,命中直接放行,可用于紧急情况下动态放行接口 - YAML 规则的优先级高于注解,同一个接口两边都配了以 YAML 为准
key-scope控制防重维度:user(按用户)、ip(按 IP)、global(全局)
全量配置
guardian:
repeatable-filter-order: -100 # 请求体缓存过滤器排序(全局共享,仅需配置一次)
repeat-submit:
storage: redis # redis / local
key-encrypt: md5 # none / md5
response-mode: exception # exception / json
log-enabled: false
interceptor-order: 2000 # 拦截器排序(值越小越先执行)
exclude-urls:
- /api/public/**
urls:
- pattern: /api/order/submit
interval: 10
time-unit: seconds
key-scope: user # user / ip / global
message: "请勿重复提交"
防重维度
| 维度 | YAML 值 | 注解值 | 效果 |
|---|---|---|---|
| 用户级 | user | KeyScope.USER | 同一用户 + 同一接口 + 同一参数(默认) |
| IP 级 | ip | KeyScope.IP | 同一 IP + 同一接口 + 同一参数 |
| 全局级 | global | KeyScope.GLOBAL | 同一接口 + 同一参数 |
响应模式
| 模式 | 配置值 | 行为 |
|---|---|---|
| 异常模式 | exception(默认) | 抛出 RepeatSubmitException,由全局异常处理器捕获 |
| JSON 模式 | json | 拦截器直接写入 JSON 响应 |
一些设计细节
Key 怎么拼?
userId + url 够不够?如果同一个用户对同一个接口传了不同的参数呢?比如下单接口,买商品 A 和买商品 B 应该算两次不同的请求,不能拦截。
所以防重 Key 把请求参数也算了进去。但 POST 请求的 body 是个流,读了一次就没了,框架内置了 RepeatableRequestFilter 自动缓存请求体,Key 生成时会把请求参数做 JSON 序列化 + Base64 编码拼进去。
用户没登录怎么办?
已登录用 userId → 没登录用 sessionId → 没 session 用客户端 IP。三级降级,永远不会出现 null。
业务异常了锁不释放怎么办?
拦截器的 afterCompletion 里做了处理:如果请求抛了异常,自动释放锁。正常完成的请求才让锁自然过期。
context-path 的坑:
匹配时同时尝试完整 URI 和去掉 context-path 后的路径,两者有一个匹配上就算命中。所以不管 YAML 里写的是 /order/submit 还是 /admin-api/order/submit,都能正确匹配。
可观测性
- 拦截日志:
log-enabled: true,前缀[Guardian-Repeat-Submit] - Actuator:
GET /actuator/guardianRepeatSubmit
{
"totalBlockCount": 128,
"totalPassCount": 5432,
"topBlockedApis": {
"/api/order/submit": 56,
"/api/sms/send": 42
}
}
扩展点
核心组件均可替换,注册同类型 Bean 即可覆盖默认实现。
自定义用户上下文(所有模块共享):
@Bean
public UserContext userContext() {
return () -> SecurityUtils.getCurrentUserId();
}
不实现也能用,框架会自动以 SessionId / IP 作为用户标识。
自定义 Key 生成策略:
public class MyKeyGenerator extends AbstractKeyGenerator {
public MyKeyGenerator(UserContext userContext, AbstractKeyEncrypt keyEncrypt) {
super(userContext, keyEncrypt);
}
@Override
protected String buildKey(RepeatSubmitKey key) {
return key.getServletUri() + ":" + key.getUserId();
}
}
@Bean
public MyKeyGenerator myKeyGenerator(UserContext userContext, AbstractKeyEncrypt keyEncrypt) {
return new MyKeyGenerator(userContext, keyEncrypt);
}
想看防重的完整实现?拦截器源码在 RepeatSubmitInterceptor.java,Redis 存储在 guardian-storage-redis,本地存储在 RepeatSubmitLocalStorage.java,代码不多,感兴趣可以看看。
自定义存储 / 自定义响应处理器:
@Bean
public RepeatSubmitStorage myStorage() {
return new RepeatSubmitStorage() {
@Override
public boolean tryAcquire(RepeatSubmitToken token) { /* ... */ }
@Override
public void release(RepeatSubmitToken token) { /* ... */ }
};
}
@Bean
public RepeatSubmitResponseHandler repeatSubmitResponseHandler() {
return (request, response, code, data, message) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONUtil.toJsonStr(CommonResult.result(code, data, message)));
};
}
二、接口限流
什么场景需要?
有人写个脚本一秒钟请求你的搜索接口 1000 次,防重拦不住(因为每次参数可能不一样),这时候就需要限流了。
Guardian 的限流就是冲着轻量场景来的:注解 + YAML 双模式、滑动窗口 + 令牌桶双算法可选。不需要引 Sentinel 那么重的东西。
先看效果
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-rate-limit-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
// 滑动窗口:每秒最多 10 次
@RateLimit(qps = 10)
// 令牌桶:每秒补 5 个令牌,桶容量 20,允许瞬间突发 20 次
@RateLimit(qps = 5, capacity = 20, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)
同样支持 YAML 批量配置:
guardian:
rate-limit:
urls:
- pattern: /api/sms/send
qps: 1
rate-limit-scope: ip
- pattern: /api/seckill/**
qps: 10
capacity: 50
algorithm: token_bucket
rate-limit-scope: global
exclude-urls:
- /api/public/**
滑动窗口 vs 令牌桶
| 滑动窗口(默认) | 令牌桶 | |
|---|---|---|
| 算法 | 统计窗口内请求数,超过阈值拒绝 | 按速率补充令牌,有令牌放行,无令牌拒绝 |
| 突发流量 | 不允许,窗口内严格限制 | 允许,桶满时可瞬间消耗所有令牌 |
| 适合场景 | 精确控速(短信、登录尝试) | 允许突发(秒杀、抢购) |
| 数据结构 | Local: Deque / Redis: ZSET | Local: double + synchronized / Redis: HASH |
举个直观的例子,都是 qps=10,突然来了 20 个请求:
| 滑动窗口 | 令牌桶(capacity=20) | |
|---|---|---|
| 第 1-10 个 | 通过 | 通过 |
| 第 11-20 个 | 全部拒绝 | 全部通过 |
| 之后每秒 | 最多 10 个 | 最多 10 个 |
全量配置
guardian:
rate-limit:
enabled: true # 总开关
storage: redis # redis / local
response-mode: exception # exception / json
log-enabled: false
interceptor-order: 1000 # 拦截器排序(值越小越先执行)
exclude-urls:
- /api/public/**
urls:
- pattern: /api/sms/send
qps: 1
window: 60
window-unit: seconds
rate-limit-scope: ip
- pattern: /api/seckill/**
qps: 10
capacity: 50
algorithm: token_bucket
rate-limit-scope: global
注解参数
| 参数 | 默认值 | 说明 |
|---|---|---|
qps | 10 | 滑动窗口=QPS,令牌桶=每 window 补充的令牌数 |
window | 1 | 滑动窗口=窗口跨度,令牌桶=补充周期 |
windowUnit | SECONDS | 时间单位 |
algorithm | SLIDING_WINDOW | 限流算法:SLIDING_WINDOW / TOKEN_BUCKET |
capacity | -1 | 令牌桶容量,≤0 时取 qps 值 |
rateLimitScope | GLOBAL | 限流维度:GLOBAL / IP / USER |
message | 请求过于频繁,请稍后再试 | 提示信息 |
限流维度
| 维度 | 效果 | 典型场景 |
|---|---|---|
GLOBAL(默认) | 整个接口共用一个计数器 | 全站搜索接口 |
IP | 每个 IP 独立计数 | 短信发送、验证码 |
USER | 每个用户独立计数 | 用户操作频率限制 |
并发安全
限流对并发安全的要求很高。Guardian 的处理:
- Redis:滑动窗口和令牌桶都用 Lua 脚本,Redis 单线程执行 Lua 天然原子
- 本地缓存:
synchronized锁到 Key 粒度,不同 Key 之间互不阻塞
可观测性
- 拦截日志:
log-enabled: true,前缀[Guardian-Rate-Limit] - Actuator:
GET /actuator/guardianRateLimit
{
"totalRequestCount": 5560,
"totalPassCount": 5432,
"totalBlockCount": 128,
"blockRate": "2.30%",
"topBlockedApis": { "/api/sms/send": 56 },
"topRequestApis": { "/api/search": 3200 },
"apiDetails": {
"/api/sms/send": { "requests": 200, "passes": 144, "blocks": 56, "blockRate": "28.00%" }
}
}
限流拦截器源码在 RateLimitInterceptor.java,Redis Lua 脚本在 guardian-storage-redis,clone 下来看看实现不到 200 行。
扩展点
@Bean
public UserContext userContext() {
return () -> SecurityUtils.getCurrentUserId();
}
@Bean
public RateLimitStorage myRateLimitStorage() {
return new RateLimitStorage() {
@Override
public boolean tryAcquire(RateLimitToken token) { /* ... */ }
};
}
@Bean
public RateLimitResponseHandler rateLimitResponseHandler() {
return (request, response, code, data, message) -> {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(JSONUtil.toJsonStr(CommonResult.result(code, data, message)));
};
}
三、接口幂等
什么场景需要?
防重和幂等经常被搞混,但它们解决的是不同的问题:
- 防重复提交:同一个请求短时间内别提交两次(锁一段时间就行)
- 接口幂等:同一个操作不管执行几次,结果都一样(比如支付,扣一次钱就行)
防重是"不让你提交",幂等是"提交了也没事"。
典型场景:
- 支付回调:第三方平台通知支付成功,网络超时重发,不做幂等用户可能被扣两次钱
- 订单提交:前端没做防抖,用户多点了几下,创建了多个订单
- MQ 重试:消息消费失败重试,消息被消费两次,用户多拿了积分
先看效果
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-idempotent-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
1. 获取 Token:
GET /guardian/idempotent/token?key=order-submit
返回:
{
"code": 200,
"data": {
"token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"expireIn": 300,
"expireUnit": "SECONDS"
}
}
2. 业务接口携带 Token:
@Idempotent("order-submit")
@PostMapping("/order/submit")
public Result submitOrder(@RequestBody OrderDTO order) {
return orderService.submit(order);
}
请求头带上 X-Idempotent-Token: {token},首次请求正常处理,重复请求直接拒绝。Token 是一次性的,用完就没了。
完整可运行的示例在 guardian-example 里,Header/Param/结果缓存/自定义提示等场景都有,clone 下来直接跑。
Token 机制是怎么工作的?
1. 客户端请求 Token
GET /guardian/idempotent/token?key=order-submit
│
▼
2. 服务端生成 UUID Token,存入 Redis/本地缓存,设置 TTL
Key: guardian:idempotent:order-submit:{uuid}
│
▼
3. 客户端携带 Token 发起业务请求
Header: X-Idempotent-Token: {uuid}
│
▼
4. 拦截器校验
├─ Token 存在 → 删除 Token(原子操作)→ 放行业务执行
└─ Token 不存在或已消费 → 拒绝请求
关键在第 4 步的删除操作是原子的。Redis 用 DEL 命令,返回 1 表示删除成功(首次消费),返回 0 表示 Key 不存在(重复请求)。本地缓存用 ConcurrentHashMap.remove(),也是原子的。
拦截器源码在 IdempotentInterceptor.java,代码不多,感兴趣可以看看。
结果缓存
默认行为是重复请求直接拒绝。但有些场景下需要返回首次执行的结果,比如支付回调平台重发通知时期望收到正常的成功响应。
guardian:
idempotent:
result-cache: true
开启后,首次请求的返回值自动缓存(实现在 IdempotentResultCacheAdvice.java)。后续拿同一个 Token 再请求时,直接返回缓存的结果而非报错。
首次请求:
Token 消费成功 → 执行业务 → 返回 {"code":200,"data":"订单创建成功"}
↓
缓存返回值到 Redis
重复请求:
Token 已消费 → 查缓存 → 命中 → 直接返回 {"code":200,"data":"订单创建成功"}
→ 未命中 → 正常拒绝
两种传 Token 的方式
Header 方式(默认): Token 放在请求头 X-Idempotent-Token 里。
Param 方式: Token 作为参数传递,PARAM 模式会依次查找:URL 查询参数 → 表单字段 → JSON Body 字段。
// URL 参数方式
@Idempotent(value = "pay-confirm", from = IdempotentTokenFrom.PARAM, tokenName = "token")
@PostMapping("/pay/confirm")
public Result confirm(@RequestParam String token, @RequestBody PayDTO pay) { ... }
// JSON Body 方式(Token 嵌在请求体里)
@Idempotent(value = "body-token", from = IdempotentTokenFrom.PARAM, tokenName = "token")
@PostMapping("/order/submit")
public Result submit(@RequestBody OrderDTO order) { ... }
// 请求体:{"token": "xxx", "orderId": "123", "amount": 1}
全量配置
guardian:
repeatable-filter-order: -100 # 请求体缓存过滤器排序(全局共享,仅需配置一次)
idempotent:
enabled: true # 总开关
storage: redis # redis / local
timeout: 300 # Token 有效期(默认 300)
time-unit: seconds # 有效期单位
response-mode: exception # exception / json
log-enabled: false
interceptor-order: 3000 # 拦截器排序
token-endpoint: true # 是否注册内置 Token 获取接口
result-cache: false # 是否启用结果缓存
注解参数
| 参数 | 默认值 | 说明 |
|---|---|---|
value | 必填 | 接口唯一标识,用于隔离不同接口的 Token |
from | HEADER | Token 来源:HEADER / PARAM |
tokenName | X-Idempotent-Token | Header 名 / URL 参数名 / JSON Body 字段名 |
message | 幂等Token无效或已消费 | 拒绝时的提示信息 |
可观测性
- 拦截日志:
log-enabled: true,前缀[Guardian-Idempotent] - Actuator:
GET /actuator/guardianIdempotent
{
"totalRequestCount": 1200,
"totalPassCount": 1100,
"totalBlockCount": 100,
"blockRate": "8.33%",
"topBlockedApis": {
"/order/submit": 60,
"/pay/confirm": 40
}
}
扩展点
// 自定义 Token 生成器(默认 UUID,可改为雪花 ID)
@Bean
public IdempotentTokenGenerator idempotentTokenGenerator() {
return () -> String.valueOf(IdUtil.getSnowflakeNextId());
}
// 自定义存储
@Bean
public IdempotentStorage myIdempotentStorage() {
return new IdempotentStorage() {
@Override
public void save(IdempotentToken token) { /* ... */ }
@Override
public boolean tryConsume(String tokenKey) { /* ... */ }
};
}
想看 Redis 存储和本地存储的具体实现?源码在 guardian-storage-redis 和 IdempotentLocalStorage.java。
四、参数自动Trim(v1.5.2 新增)
什么场景需要?
这个功能是从实际踩坑来的。
用户注册时用户名输了个 " zhangsan "(前后带空格),存进了数据库。后来登录输 "zhangsan" 死活登不上。运维排查半天,发现数据库里的用户名前面多了个空格。
更隐蔽的是不可见字符。用户从某些网页复制粘贴内容,看起来一模一样,但实际上带了零宽空格(\u200B)或 BOM(\uFEFF)。这种字符肉眼看不见,但程序比较字符串时会失败。
Guardian 的参数自动 Trim 就是解决这个问题:引个依赖,全局生效,所有请求参数自动去空格 + 可选清除不可见字符。
先看效果
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-auto-trim-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
引完就能用了,零配置。所有请求参数(表单参数 + JSON Body)自动去除首尾空格。
表单 Trim、JSON Trim、排除字段、不可见字符替换的测试示例都在 AutoTrimController.java,clone 下来直接跑。
不可见字符替换
如果你还需要清除一些不可见字符,配一下 character-replacements:
guardian:
auto-trim:
character-replacements:
- from: "\r" # 回车符
to: ""
- from: "\u200B" # 零宽空格
to: ""
- from: "\uFEFF" # BOM
to: ""
支持的转义格式:
| 转义写法 | 实际字符 | 说明 |
|---|---|---|
\r | \r | 回车符 |
\n | \n | 换行符 |
\t | \t | 制表符 |
\0 | \0 | 空字符 |
\uXXXX | Unicode 字符 | 如 \u200B = 零宽空格 |
执行顺序:先执行字符替换,再执行 trim。
排除字段
密码、签名等不应被 trim 的字段可以排除:
guardian:
auto-trim:
exclude-fields:
- password
- signature
exclude-fields 同时作用于表单参数名和 JSON Body 字段名。
全量配置
guardian:
auto-trim:
enabled: true # 总开关(默认 true)
filter-order: -10000 # Filter 排序(值越小越先执行)
exclude-fields: # 排除字段
- password
- signature
character-replacements: # 字符替换规则(先替换后 trim)
- from: "\r"
to: ""
- from: "\u200B"
to: ""
- from: "\uFEFF"
to: ""
工作原理
底层通过 OncePerRequestFilter + HttpServletRequestWrapper 实现:
- 表单参数:重写
getParameter()、getParameterValues()、getParameterMap(),返回 trim 后的值 - JSON Body:缓存请求体,解析 JSON 后递归 trim 所有 String 类型字段,再把处理后的 JSON 写回
对业务代码完全透明,Controller 拿到的参数已经是 trim 过的。
核心源码在 AutoTrimFilter.java 和 AutoTrimRequestWrapper.java,字符替换逻辑在 CharacterSanitizer.java,总共不到 200 行,实现很清晰。
五、慢接口检测(v1.5.2 新增)
什么场景需要?
线上一个接口平时响应 200ms,某天突然变成 5 秒。如果没有监控,你可能要等到用户投诉了才知道。
Sentinel 能做,但太重了。APM(SkyWalking 之类的)也能做,但不是每个项目都上了 APM。
Guardian 的慢接口检测就是一个轻量方案:超过阈值自动打 WARN 日志 + 记录统计 + Actuator 端点查看排行。
先看效果
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-slow-api-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
零配置即可使用,默认阈值 3000ms。接口响应超过 3 秒就会自动打印 WARN 日志。测试示例在 SlowApiController.java:
WARN [Guardian-Slow-Api] @SlowApiThreshold 慢接口检测 | Method=GET | URI=/api/detail | 耗时=3521ms | 阈值=3000ms
注解自定义阈值
全局阈值 3 秒太粗了?可以用注解给单个接口设置不同的阈值:
@SlowApiThreshold(1000) // 这个接口超过 1 秒就算慢
@GetMapping("/detail")
public Result getDetail(@RequestParam Long id) {
return detailService.query(id);
}
注解优先级高于全局配置。没有注解的接口使用全局阈值。
全量配置
guardian:
slow-api:
enabled: true # 总开关(默认 true)
threshold: 3000 # 全局阈值(毫秒,默认 3000)
interceptor-order: -1000 # 拦截器排序
exclude-urls: # 白名单(命中直接放行)
- /api/health
- /api/public/**
Actuator 端点
GET /actuator/guardianSlowApi
{
"totalSlowCount": 15,
"topSlowApis": {
"/api/detail": { "count": 8, "maxDuration": 5230 },
"/api/export": { "count": 7, "maxDuration": 12500 }
}
}
可以看到哪些接口触发了慢接口告警、触发了多少次、最慢一次用了多久。运维大盘一目了然。
工作原理
通过 HandlerInterceptor 实现:
preHandle:记录请求开始时间afterCompletion:计算耗时,超过阈值则打日志并记录统计
统计数据存在内存中(ConcurrentHashMap + AtomicLong),不依赖外部存储。
拦截器源码在 SlowApiInterceptor.java,统计逻辑在 SlowApiStatistics.java,两个文件加起来不到 100 行,非常适合学习 Spring Boot 拦截器的封装思路。
六、请求链路追踪(v1.5.2 新增)
什么场景需要?
线上出了问题,你打开日志搜索,发现几十个接口的日志混在一起,根本分不清哪些日志属于同一个请求。
或者前端报了个错,你拿到日志一看,Controller 层的日志找到了,但 Service 层、DAO 层的日志散落在各处,拼凑不起来。
TraceId 就是解决这个问题:给每个请求分配一个唯一 ID,同一个请求的所有日志都带上这个 ID,搜索时按 ID 过滤就能串联起来。
先看效果
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-trace-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
零配置即可使用。测试示例在 TraceController.java。只需要在 Logback 的日志格式里加 %X{traceId}:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n</pattern>
效果:
2026-02-20 14:30:01.123 [143025wkz8dxqn] [http-nio-8080-exec-1] INFO c.s.g.e.t.TraceController - [Controller] 接收请求
2026-02-20 14:30:01.145 [143025wkz8dxqn] [http-nio-8080-exec-1] INFO c.s.g.e.t.TraceController - [Service] 执行业务逻辑
2026-02-20 14:30:01.167 [143025wkz8dxqn] [http-nio-8080-exec-1] INFO c.s.g.e.t.TraceController - [Dao] 执行数据库操作
同一个请求的三条日志都带了 143025wkz8dxqn,搜索这个 ID 就能把整个请求链路串联起来。
跨服务链路串联
请求进来时,如果请求头有 X-Trace-Id,直接复用;没有则自动生成。TraceId 同时写入响应头。
服务 A 收到请求 → 生成 traceId=abc123 → 调用服务 B 时带上 X-Trace-Id: abc123
服务 B 收到请求 → 从请求头取出 abc123 → 复用同一个 traceId
这样同一条链路上所有服务的日志都带同一个 TraceId,排查问题时按 ID 搜索即可。
全量配置
guardian:
trace:
enabled: true # 总开关(默认 true)
filter-order: -20000 # Filter 排序(最先执行,覆盖全链路)
header-name: X-Trace-Id # 请求头/响应头名称
工作原理
请求进入
│
▼
TraceIdFilter(OncePerRequestFilter)
├─ 请求头有 X-Trace-Id → 复用
└─ 请求头没有 → 自动生成(时分秒 + 10位随机字符串)
│
▼
MDC.put("traceId", traceId) ← 写入 MDC,日志自动携带
response.setHeader(headerName) ← 写入响应头,客户端可获取
│
▼
业务执行(同一线程内所有日志都带 traceId)
│
▼
MDC.remove("traceId") ← 请求结束清理,防止线程复用污染
Filter 排序默认 -20000,确保在所有其他 Filter 之前执行,这样整个请求链路(包括其他 Guardian 模块的日志)都能带上 TraceId。
整个 TraceId 的实现就一个文件:TraceIdFilter.java,不到 60 行,MDC + Filter 的经典用法,可以直接参考。
七、拦截器执行顺序
如果你同时用了多个模块,它们的执行顺序是确定的,通过各自的 order 配置控制,值越小越先执行:
| 顺序 | 模块 | 类型 | 默认 order | 为什么这样排 |
|---|---|---|---|---|
| 1 | 链路追踪 | Filter | -20000 | 最先执行,确保全链路日志带 TraceId |
| 2 | 参数Trim | Filter | -10000 | 在业务 Filter 之前处理参数 |
| 3 | 请求体缓存 | Filter | -100 | 缓存 body,供防重和幂等读取 |
| 4 | 慢接口检测 | Interceptor | -1000 | 记录开始时间 |
| 5 | 限流 | Interceptor | 1000 | 先拦截超限流量 |
| 6 | 防重 | Interceptor | 2000 | 再判断是否重复请求 |
| 7 | 幂等 | Interceptor | 3000 | 最后消费 Token(不可逆) |
幂等放最后是关键——Token 一旦消费就没了,如果先消费 Token 再被限流拒绝,这个 Token 就浪费了。
每个模块的 order 都可以通过 YAML 自定义:
guardian:
repeatable-filter-order: -100
trace:
filter-order: -20000
auto-trim:
filter-order: -10000
slow-api:
interceptor-order: -1000
rate-limit:
interceptor-order: 1000
repeat-submit:
interceptor-order: 2000
idempotent:
interceptor-order: 3000
八、存储方式
防重、限流、幂等三个模块支持两种存储:
| Redis | Local | |
|---|---|---|
| 分布式 | 支持 | 仅单机 |
| 持久性 | Redis 持久化 | 重启丢失 |
| 推荐场景 | 生产环境 | 开发/单体应用 |
| 额外依赖 | 需要 Redis | 无 |
切换方式:
guardian:
repeat-submit:
storage: local # 或 redis
rate-limit:
storage: local
idempotent:
storage: local
本地缓存底层用 ConcurrentHashMap,带守护线程定期清理过期 Key,不会内存泄漏。
不用 Redis 也想跑起来?clone 仓库后把
storage改成local就行,guardian-example 里有完整的示例配置。
九、规则优先级
Guardian 各防护模块的规则匹配遵循以下优先级:
exclude-urls(白名单)> YAML 规则 > 注解 > 放行
| 场景 | 行为 |
|---|---|
URL 命中 exclude-urls | 直接放行,跳过所有检测(包括注解) |
URL 命中 YAML urls 规则 | YAML 规则生效 |
方法有注解(@RateLimit / @RepeatSubmit / @SlowApiThreshold) | 注解规则生效 |
| 以上均未命中 | 放行 |
为什么这样设计?
exclude-urls 作为白名单拥有最高优先级,可以在紧急情况下通过配置中心动态添加 URL 实现"一键放行",无需改代码重启。这在线上应急场景非常实用——比如某个接口被限流误伤,直接往 exclude-urls 加一条就解除了。
注解适合"长期固定"的保护策略(写在代码里不随配置变),YAML 规则适合"动态可调"的批量策略(配置中心随时改)。如果某个接口既有注解又有 YAML 规则,以 YAML 为准。
十、动态配置(v1.5.2 新增)
什么场景需要?
线上环境经常会遇到这种情况:限流阈值配小了,正常用户也被拦;防重间隔配长了,影响用户体验。改个配置还得走发版流程重启应用,明明就改了个数字。
Guardian v1.5.2 支持了配置中心动态刷新,所有模块的 YAML 配置都可以通过 Nacos / Apollo 等配置中心实时修改,改完即生效,不用重启。
哪些配置支持动态刷新?
简单说:运行期间会被读取的 YAML 配置都支持动态刷新。enabled、storage 这类只在启动时读取一次的参数,改完需要重启才生效,这也很合理——你不会在运行时把存储从 Redis 切到 Local。
下面列出每个模块可动态修改的完整参数,直接对着改就行,不用翻源码。
防重复提交(guardian.repeat-submit.*)
| YAML Key | 类型 | 默认值 | 说明 |
|---|---|---|---|
response-mode | exception / json | exception | 响应模式 |
log-enabled | boolean | false | 是否打印拦截日志 |
exclude-urls | List<String> | [] | 排除规则(白名单,优先级最高,AntPath) |
urls | List | [] | 防重规则列表,每项参数如下 |
urls[].pattern | String | — | 接口路径(AntPath 通配符,如 /api/order/**) |
urls[].interval | int | 5 | 防重间隔 |
urls[].time-unit | TimeUnit | seconds | 间隔时间单位 |
urls[].key-scope | user / ip / global | user | 防重维度 |
urls[].message | String | 您的请求过于频繁,请稍后再试 | 拦截提示信息 |
urls[].client-type | pc / app | pc | 客户端类型 |
接口限流(guardian.rate-limit.*)
| YAML Key | 类型 | 默认值 | 说明 |
|---|---|---|---|
response-mode | exception / json | exception | 响应模式 |
log-enabled | boolean | false | 是否打印拦截日志 |
exclude-urls | List<String> | [] | 排除规则(白名单,优先级最高,AntPath) |
urls | List | [] | 限流规则列表,每项参数如下 |
urls[].pattern | String | — | 接口路径(AntPath 通配符) |
urls[].qps | int | 10 | 滑动窗口=QPS;令牌桶=每 window 补充令牌数 |
urls[].window | int | 1 | 滑动窗口=窗口跨度;令牌桶=补充周期 |
urls[].window-unit | TimeUnit | seconds | 时间单位 |
urls[].algorithm | sliding_window / token_bucket | sliding_window | 限流算法 |
urls[].capacity | int | -1 | 令牌桶容量(≤0 时取 qps 值) |
urls[].rate-limit-scope | global / ip / user | global | 限流维度 |
urls[].message | String | 请求过于频繁,请稍后再试 | 拦截提示信息 |
接口幂等(guardian.idempotent.*)
| YAML Key | 类型 | 默认值 | 说明 |
|---|---|---|---|
timeout | long | 300 | Token 有效期 |
time-unit | TimeUnit | seconds | 有效期单位 |
response-mode | exception / json | exception | 响应模式 |
log-enabled | boolean | false | 是否打印拦截日志 |
参数自动Trim(guardian.auto-trim.*)
| YAML Key | 类型 | 默认值 | 说明 |
|---|---|---|---|
exclude-fields | Set<String> | [] | 排除字段(不做 trim 的字段名) |
character-replacements | List | [] | 字符替换规则列表,每项参数如下 |
character-replacements[].from | String | — | 待替换字符(支持 \r \n \t \uXXXX 转义) |
character-replacements[].to | String | "" | 替换目标 |
慢接口检测(guardian.slow-api.*)
| YAML Key | 类型 | 默认值 | 说明 |
|---|---|---|---|
threshold | long | 3000 | 全局慢接口阈值(毫秒) |
exclude-urls | List<String> | [] | 排除规则(白名单,优先级最高,AntPath) |
怎么用?
以 Nacos 为例。
第一步,加依赖:
<!-- Spring Cloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Cloud Bootstrap(Spring Cloud 2021.x 需要显式引入) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
第二步,加 bootstrap.yml:
spring:
application:
name: your-app
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yml
第三步,在 Nacos 控制台配置 Guardian 参数。
在 Data ID your-app.yml 里写 Guardian 的配置。下面给一个覆盖全模块的完整示例,直接复制过去改数值就行,只需保留你用到的模块:
guardian:
# ===== 防重复提交 =====
repeat-submit:
response-mode: exception # exception / json
log-enabled: false # 是否打印拦截日志
exclude-urls: # 白名单(优先级最高,命中直接放行)
- /api/public/**
urls: # 防重规则(优先级高于 @RepeatSubmit 注解)
- pattern: /api/order/** # 接口路径
interval: 10 # 防重间隔
time-unit: seconds # 时间单位
key-scope: user # user / ip / global
message: "请勿重复提交"
client-type: pc # pc / app
# ===== 接口限流 =====
rate-limit:
response-mode: exception
log-enabled: false
exclude-urls:
- /api/public/**
urls: # 限流规则(优先级高于 @RateLimit 注解)
- pattern: /api/sms/send
qps: 1 # 滑动窗口=QPS;令牌桶=每 window 补充令牌数
window: 60 # 滑动窗口=窗口跨度;令牌桶=补充周期
window-unit: seconds
rate-limit-scope: ip # global / ip / user
- pattern: /api/seckill/**
qps: 10
capacity: 50 # 令牌桶容量(≤0 时取 qps 值)
algorithm: token_bucket # sliding_window / token_bucket
rate-limit-scope: global
message: "抢购太火爆,请稍后重试"
# ===== 接口幂等 =====
idempotent:
timeout: 300 # Token 有效期
time-unit: seconds # 有效期单位
response-mode: exception
log-enabled: false
# ===== 参数自动Trim =====
auto-trim:
exclude-fields: # 排除字段(不做 trim)
- password
- signature
character-replacements: # 字符替换规则(先替换后 trim)
- from: "\u200B" # 零宽空格
to: ""
- from: "\uFEFF" # BOM
to: ""
- from: "\r" # 回车符
to: ""
# ===== 慢接口检测 =====
slow-api:
threshold: 3000 # 全局阈值(毫秒)
exclude-urls:
- /api/health
发布后应用自动加载。之后想改限流 QPS?直接在 Nacos 把 qps: 1 改成 qps: 5,点发布,立刻生效,不用重启。想临时关闭某个防重规则?把那条 url 规则删掉,发布,立刻生效。
实现原理
这里简单说下实现思路,感兴趣的可以看源码。
Guardian 的每个模块在 core 层定义了配置接口(如 RepeatSubmitConfig、RateLimitConfig、IdempotentConfig 等),starter 层的 @ConfigurationProperties 属性类实现这些接口。拦截器和过滤器的构造函数接收的是接口引用而非原始值:
// 拦截器持有配置接口引用,每次请求实时读取最新值
public class RateLimitInterceptor implements HandlerInterceptor {
private final RateLimitConfig rateLimitConfig;
public RateLimitInterceptor(..., RateLimitConfig rateLimitConfig, ...) {
this.rateLimitConfig = rateLimitConfig;
}
}
当配置中心推送变更时,Spring Cloud 的 ConfigurationPropertiesRebinder 监听到 EnvironmentChangeEvent,自动对 @ConfigurationProperties Bean 执行重新绑定。因为拦截器持有的是对象引用(而非构造时传入的原始值),重新绑定后下一次请求就能读到最新配置。
整个过程不需要 @RefreshScope,不需要自定义监听器,也不需要重建拦截器/过滤器实例。框架零侵入,业务零感知。
配置接口定义在各模块的
core包中,如 RateLimitConfig.java,公共基础接口在 BaseConfig.java,感兴趣可以看看。
项目结构
guardian-parent
├── guardian-core # 公共基础(共享类)
├── guardian-repeat-submit/ # 防重复提交
│ ├── guardian-repeat-submit-core/
│ └── guardian-repeat-submit-spring-boot-starter/
├── guardian-rate-limit/ # 接口限流
│ ├── guardian-rate-limit-core/
│ └── guardian-rate-limit-spring-boot-starter/
├── guardian-idempotent/ # 接口幂等
│ ├── guardian-idempotent-core/
│ └── guardian-idempotent-spring-boot-starter/
├── guardian-auto-trim/ # 参数自动Trim
│ ├── guardian-auto-trim-core/
│ └── guardian-auto-trim-spring-boot-starter/
├── guardian-slow-api/ # 慢接口检测
│ ├── guardian-slow-api-core/
│ └── guardian-slow-api-spring-boot-starter/
├── guardian-trace/ # 请求链路追踪
│ ├── guardian-trace-core/
│ └── guardian-trace-spring-boot-starter/
├── guardian-storage-redis/ # Redis 存储(多模块共享)
└── guardian-example/ # 示例工程
六个模块完全独立,用哪个引哪个,互不依赖。guardian-core 放公共类(UserContext、GuardianResponseHandler 等),guardian-storage-redis 是 Redis 存储的共享实现。
完整可运行的示例代码在 guardian-example 模块里,六个模块的各种场景都有,clone 下来直接跑。示例配置在 application.yml,里面每个配置项都有注释。
总结
Guardian v1.5.2 现在覆盖了六种 API 请求层防护场景,并且所有配置都支持动态刷新:
| 功能 | 解决什么问题 | 动态配置 | Starter |
|---|---|---|---|
| 防重复提交 | 用户手抖连点、表单重复提交 | ✅ | guardian-repeat-submit-spring-boot-starter |
| 接口限流 | 恶意刷接口、突发流量 | ✅ | guardian-rate-limit-spring-boot-starter |
| 接口幂等 | 支付回调重试、MQ 重复消费 | ✅ | guardian-idempotent-spring-boot-starter |
| 参数自动Trim | 前后空格、不可见字符导致数据异常 | ✅ | guardian-auto-trim-spring-boot-starter |
| 慢接口检测 | 接口变慢无感知、缺少轻量监控 | ✅ | guardian-slow-api-spring-boot-starter |
| 请求链路追踪 | 日志散乱无法串联、排查问题难 | ✅ | guardian-trace-spring-boot-starter |
如果你的 Spring Boot 项目需要这些能力,但又不想引 Sentinel 那么重的东西,可以试试。配合 Nacos 等配置中心使用,连重启都不用——改个配置发布一下就生效。
Maven Central 坐标(最新 v1.5.2):
<!-- 防重复提交 -->
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-repeat-submit-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<!-- 接口限流 -->
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-rate-limit-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<!-- 接口幂等 -->
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-idempotent-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<!-- 参数自动Trim -->
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-auto-trim-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<!-- 慢接口检测 -->
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-slow-api-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
<!-- 请求链路追踪 -->
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-trace-spring-boot-starter</artifactId>
<version>1.5.2</version>
</dependency>
项目地址(README 里有完整配置文档和更新日志):
- GitHub:github.com/BigGG-Guard…
- Gitee:gitee.com/BigGG-Guard…
整个项目源码不多,没有复杂的抽象层,每个模块核心代码就几个文件。如果你正在学 Spring Boot Starter 的封装思路,或者想看看 Filter / Interceptor / Lua 脚本 / MDC 这些东西在实际项目中怎么用的,clone 下来翻翻挺有收获的。
有问题直接提 Issue,我基本都会回。也欢迎 PR,一起把这个轮子打磨得更好。
觉得有用的话,去 GitHub 点个 Star 支持一下,这对开源作者真的很重要。