六合一 Spring Boot API 防护框架:防重、限流、幂等、自动Trim、慢接口检测、链路追踪,一个 Starter 搞定

前言

之前写了个轻量级的 Spring Boot 接口防护框架 Guardian,陆续做了防重复提交、接口限流、接口幂等三个功能,发到了 Maven Central 开源。

最近又加了三个实用功能:参数自动 Trim慢接口检测请求链路追踪。现在 Guardian v1.5.0 一共六个模块,覆盖了 API 请求层最常见的防护需求。

每个模块独立 Starter,用哪个引哪个,互不依赖。最快只需要引个依赖就能用,零配置。

项目地址(源码 + 示例 + 文档全在里面):


功能一览

功能 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 日志串联

下面一个一个说。


一、防重复提交

什么场景需要?

用户点了提交按钮,前端没做防抖,或者网络慢用户多点了几下。后端收到三个一模一样的请求,创建了三个订单。

防重复提交就是解决这个问题:同一个请求短时间内别让它提交两次

先看效果

三步搞定。

第一步,引依赖:

xml 复制代码
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-repeat-submit-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

第二步,加注解:

java 复制代码
@PostMapping("/submit")
@RepeatSubmit(interval = 10, message = "订单正在处理,请勿重复提交")
public Result submitOrder(@RequestBody OrderDTO order) {
    return orderService.submit(order);
}

第三步,没了。启动项目就生效了。

10 秒内同一个用户、同一个接口、同样的请求参数,第二次请求会被直接拦截。

完整可运行的示例代码在仓库的 guardian-example 模块里,各种场景都有,clone 下来直接跑。

YAML 批量配置

单个接口用注解挺方便,但如果有 50 个接口都要配防重,一个一个加注解就有点累了。支持在 YAML 里用 AntPath 通配符批量配置:

yaml 复制代码
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

几个要点:

  • YAML 规则的优先级高于注解,同一个接口两边都配了以 YAML 为准
  • 白名单(exclude-urls优先级最高,命中直接放行
  • key-scope 控制防重维度:user(按用户)、ip(按 IP)、global(全局)

全量配置

yaml 复制代码
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]
  • ActuatorGET /actuator/guardianRepeatSubmit
json 复制代码
{
  "totalBlockCount": 128,
  "totalPassCount": 5432,
  "topBlockedApis": {
    "/api/order/submit": 56,
    "/api/sms/send": 42
  }
}

扩展点

核心组件均可替换,注册同类型 Bean 即可覆盖默认实现。

自定义用户上下文(所有模块共享):

java 复制代码
@Bean
public UserContext userContext() {
    return () -> SecurityUtils.getCurrentUserId();
}

不实现也能用,框架会自动以 SessionId / IP 作为用户标识。

自定义 Key 生成策略:

java 复制代码
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,代码不多,感兴趣可以看看。

自定义存储 / 自定义响应处理器:

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 那么重的东西。

先看效果

xml 复制代码
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-rate-limit-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>
java 复制代码
// 滑动窗口:每秒最多 10 次
@RateLimit(qps = 10)

// 令牌桶:每秒补 5 个令牌,桶容量 20,允许瞬间突发 20 次
@RateLimit(qps = 5, capacity = 20, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)

同样支持 YAML 批量配置:

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 个

全量配置

yaml 复制代码
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 的处理:

可观测性

  • 拦截日志log-enabled: true,前缀 [Guardian-Rate-Limit]
  • ActuatorGET /actuator/guardianRateLimit
json 复制代码
{
  "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 行。

扩展点

java 复制代码
@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)));
    };
}

三、接口幂等

什么场景需要?

防重和幂等经常被搞混,但它们解决的是不同的问题:

  • 防重复提交:同一个请求短时间内别提交两次(锁一段时间就行)
  • 接口幂等:同一个操作不管执行几次,结果都一样(比如支付,扣一次钱就行)

防重是"不让你提交",幂等是"提交了也没事"。

典型场景:

  1. 支付回调:第三方平台通知支付成功,网络超时重发,不做幂等用户可能被扣两次钱
  2. 订单提交:前端没做防抖,用户多点了几下,创建了多个订单
  3. MQ 重试:消息消费失败重试,消息被消费两次,用户多拿了积分

先看效果

xml 复制代码
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-idempotent-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

1. 获取 Token:

vbnet 复制代码
GET /guardian/idempotent/token?key=order-submit

返回:

json 复制代码
{
  "code": 200,
  "data": {
    "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "expireIn": 300,
    "expireUnit": "SECONDS"
  }
}

2. 业务接口携带 Token:

java 复制代码
@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 机制是怎么工作的?

vbnet 复制代码
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,代码不多,感兴趣可以看看。

结果缓存

默认行为是重复请求直接拒绝。但有些场景下需要返回首次执行的结果,比如支付回调平台重发通知时期望收到正常的成功响应。

yaml 复制代码
guardian:
  idempotent:
    result-cache: true

开启后,首次请求的返回值自动缓存(实现在 IdempotentResultCacheAdvice.java)。后续拿同一个 Token 再请求时,直接返回缓存的结果而非报错。

css 复制代码
首次请求:
  Token 消费成功 → 执行业务 → 返回 {"code":200,"data":"订单创建成功"}
                                    ↓
                              缓存返回值到 Redis

重复请求:
  Token 已消费 → 查缓存 → 命中 → 直接返回 {"code":200,"data":"订单创建成功"}
                        → 未命中 → 正常拒绝

两种传 Token 的方式

Header 方式(默认): Token 放在请求头 X-Idempotent-Token 里。

Param 方式: Token 作为参数传递,PARAM 模式会依次查找:URL 查询参数 → 表单字段 → JSON Body 字段。

java 复制代码
// 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}

全量配置

yaml 复制代码
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]
  • ActuatorGET /actuator/guardianIdempotent
json 复制代码
{
  "totalRequestCount": 1200,
  "totalPassCount": 1100,
  "totalBlockCount": 100,
  "blockRate": "8.33%",
  "topBlockedApis": {
    "/order/submit": 60,
    "/pay/confirm": 40
  }
}

扩展点

java 复制代码
// 自定义 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-redisIdempotentLocalStorage.java


四、参数自动Trim(v1.5.0 新增)

什么场景需要?

这个功能是从实际踩坑来的。

用户注册时用户名输了个 " zhangsan "(前后带空格),存进了数据库。后来登录输 "zhangsan" 死活登不上。运维排查半天,发现数据库里的用户名前面多了个空格。

更隐蔽的是不可见字符。用户从某些网页复制粘贴内容,看起来一模一样,但实际上带了零宽空格(\u200B)或 BOM(\uFEFF)。这种字符肉眼看不见,但程序比较字符串时会失败。

Guardian 的参数自动 Trim 就是解决这个问题:引个依赖,全局生效,所有请求参数自动去空格 + 可选清除不可见字符

先看效果

xml 复制代码
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-auto-trim-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

引完就能用了,零配置。所有请求参数(表单参数 + JSON Body)自动去除首尾空格。

表单 Trim、JSON Trim、排除字段、不可见字符替换的测试示例都在 AutoTrimController.java,clone 下来直接跑。

不可见字符替换

如果你还需要清除一些不可见字符,配一下 character-replacements

yaml 复制代码
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 的字段可以排除:

yaml 复制代码
guardian:
  auto-trim:
    exclude-fields:
      - password
      - signature

exclude-fields 同时作用于表单参数名和 JSON Body 字段名。

全量配置

yaml 复制代码
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.javaAutoTrimRequestWrapper.java,字符替换逻辑在 CharacterSanitizer.java,总共不到 200 行,实现很清晰。


五、慢接口检测(v1.5.0 新增)

什么场景需要?

线上一个接口平时响应 200ms,某天突然变成 5 秒。如果没有监控,你可能要等到用户投诉了才知道。

Sentinel 能做,但太重了。APM(SkyWalking 之类的)也能做,但不是每个项目都上了 APM。

Guardian 的慢接口检测就是一个轻量方案:超过阈值自动打 WARN 日志 + 记录统计 + Actuator 端点查看排行

先看效果

xml 复制代码
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-slow-api-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

零配置即可使用,默认阈值 3000ms。接口响应超过 3 秒就会自动打印 WARN 日志。测试示例在 SlowApiController.java

ini 复制代码
WARN [Guardian-Slow-Api] @SlowApiThreshold 慢接口检测 | Method=GET | URI=/api/detail | 耗时=3521ms | 阈值=3000ms

注解自定义阈值

全局阈值 3 秒太粗了?可以用注解给单个接口设置不同的阈值:

java 复制代码
@SlowApiThreshold(1000)  // 这个接口超过 1 秒就算慢
@GetMapping("/detail")
public Result getDetail(@RequestParam Long id) {
    return detailService.query(id);
}

注解优先级高于全局配置。没有注解的接口使用全局阈值。

全量配置

yaml 复制代码
guardian:
  slow-api:
    enabled: true                      # 总开关(默认 true)
    threshold: 3000                    # 全局阈值(毫秒,默认 3000)
    interceptor-order: -1000           # 拦截器排序
    exclude-urls:                      # 白名单(命中跳过检测)
      - /api/health
      - /api/public/**

Actuator 端点

bash 复制代码
GET /actuator/guardianSlowApi
json 复制代码
{
  "totalSlowCount": 15,
  "topSlowApis": {
    "/api/detail": { "count": 8, "maxDuration": 5230 },
    "/api/export": { "count": 7, "maxDuration": 12500 }
  }
}

可以看到哪些接口触发了慢接口告警、触发了多少次、最慢一次用了多久。运维大盘一目了然。

工作原理

通过 HandlerInterceptor 实现:

  1. preHandle:记录请求开始时间
  2. afterCompletion:计算耗时,超过阈值则打日志并记录统计

统计数据存在内存中(ConcurrentHashMap + AtomicLong),不依赖外部存储。

拦截器源码在 SlowApiInterceptor.java,统计逻辑在 SlowApiStatistics.java,两个文件加起来不到 100 行,非常适合学习 Spring Boot 拦截器的封装思路。


六、请求链路追踪(v1.5.0 新增)

什么场景需要?

线上出了问题,你打开日志搜索,发现几十个接口的日志混在一起,根本分不清哪些日志属于同一个请求。

或者前端报了个错,你拿到日志一看,Controller 层的日志找到了,但 Service 层、DAO 层的日志散落在各处,拼凑不起来。

TraceId 就是解决这个问题:给每个请求分配一个唯一 ID,同一个请求的所有日志都带上这个 ID,搜索时按 ID 过滤就能串联起来。

先看效果

xml 复制代码
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-trace-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

零配置即可使用。测试示例在 TraceController.java。只需要在 Logback 的日志格式里加 %X{traceId}

xml 复制代码
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%X{traceId}] [%thread] %-5level %logger{36} - %msg%n</pattern>

效果:

ini 复制代码
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 同时写入响应头。

less 复制代码
服务 A 收到请求 → 生成 traceId=abc123 → 调用服务 B 时带上 X-Trace-Id: abc123
服务 B 收到请求 → 从请求头取出 abc123 → 复用同一个 traceId

这样同一条链路上所有服务的日志都带同一个 TraceId,排查问题时按 ID 搜索即可。

全量配置

yaml 复制代码
guardian:
  trace:
    enabled: true                      # 总开关(默认 true)
    filter-order: -20000               # Filter 排序(最先执行,覆盖全链路)
    header-name: X-Trace-Id            # 请求头/响应头名称

工作原理

csharp 复制代码
请求进入
  │
  ▼
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 自定义:

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

切换方式:

yaml 复制代码
guardian:
  repeat-submit:
    storage: local  # 或 redis
  rate-limit:
    storage: local
  idempotent:
    storage: local

本地缓存底层用 ConcurrentHashMap,带守护线程定期清理过期 Key,不会内存泄漏。

不用 Redis 也想跑起来?clone 仓库后把 storage 改成 local 就行,guardian-example 里有完整的示例配置。


项目结构

r 复制代码
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 放公共类(UserContextGuardianResponseHandler 等),guardian-storage-redis 是 Redis 存储的共享实现。

完整可运行的示例代码在 guardian-example 模块里,六个模块的各种场景都有,clone 下来直接跑。示例配置在 application.yml,里面每个配置项都有注释。


总结

Guardian v1.5.0 现在覆盖了六种 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 那么重的东西,可以试试。


Maven Central 坐标(最新 v1.5.0):

xml 复制代码
<!-- 防重复提交 -->
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-repeat-submit-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

<!-- 接口限流 -->
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-rate-limit-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

<!-- 接口幂等 -->
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-idempotent-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

<!-- 参数自动Trim -->
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-auto-trim-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

<!-- 慢接口检测 -->
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-slow-api-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

<!-- 请求链路追踪 -->
<dependency>
    <groupId>io.github.biggg-guardian</groupId>
    <artifactId>guardian-trace-spring-boot-starter</artifactId>
    <version>1.5.0</version>
</dependency>

项目地址(README 里有完整配置文档和更新日志):

整个项目源码不多,没有复杂的抽象层,每个模块核心代码就几个文件。如果你正在学 Spring Boot Starter 的封装思路,或者想看看 Filter / Interceptor / Lua 脚本 / MDC 这些东西在实际项目中怎么用的,clone 下来翻翻挺有收获的。

有问题直接提 Issue,我基本都会回。也欢迎 PR,一起把这个轮子打磨得更好。

觉得有用的话,去 GitHub 点个 Star 支持一下,这对开源作者真的很重要。

相关推荐
PieroPC1 小时前
生成 自己喜欢 Fastapi 写法的文件和目录
后端
HoneyMoose2 小时前
Jenkins 更新时候提示 Key 错误
java·开发语言
rannn_1112 小时前
【苍穹外卖|Day10】Spring Task、订单状态定时处理、WebSocket、来单提醒、客户催单
java·后端·websocket·苍穹外卖
cqbzcsq2 小时前
MC Forge 1.20.1 mod开发学习笔记(战利品、标签、配方)
java·笔记·学习·mod·mc
追随者永远是胜利者2 小时前
(LeetCode-Hot100)461. 汉明距离
java·算法·leetcode·职场和发展·go
人道领域2 小时前
SpringBoot多环境配置实战指南
java·开发语言·spring boot·github
捷利迅分享2 小时前
Android TV 4分屏独立播放电视应用完整开发方案
java
马猴烧酒.2 小时前
【JAVA算法|hot100】栈类型题目详解笔记
java·笔记
Dragon Wu2 小时前
SpringCloud 多模块下引入独立bom模块的正确架构方案
java·spring boot·后端·spring cloud·架构·springboot