Boost Your Spring Boot APIs with a DIY Anti‑Duplicate and Rate‑Limiting Starter
Guardian is a lightweight Spring Boot starter that provides independent anti‑duplicate submission and rate‑limiting modules, offering annotation and YAML configuration, multi‑dimensional key scopes, customizable response modes, Redis or local storage, and built‑in monitoring, all illustrated with step‑by‑step code examples.
Overview
Guardian is a lightweight Spring Boot starter (v1.3.0) that provides two independent modules: anti‑duplicate submission and API rate limiting. Both modules can be added separately via Maven dependencies and support Redis or local in‑memory storage.
Anti‑duplicate submission
Quick start
Add the starter dependency:
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-repeat-submit-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>Annotate the controller method:
@PostMapping("/submit")
@RepeatSubmit(interval = 10, message = "订单正在处理,请勿重复提交")
public Result submitOrder(@RequestBody OrderDTO order) {
return orderService.submit(order);
}Run the application; the interceptor becomes active. Within the configured interval (e.g., 10 seconds) a second request with the same user, endpoint and request body is blocked.
Why not use a raw Redis lock?
Key composition : Simple userId+url cannot distinguish different request bodies. Guardian serializes the request body to JSON, encodes it with Base64, and includes it in the lock key.
User identification : When userId is null, Guardian falls back to sessionId and then to client IP, guaranteeing a non‑null key.
Exception handling : If business code throws an exception, the lock would otherwise remain for the full interval. Guardian releases the lock in afterCompletion when an exception occurs.
Selective disabling : A global whitelist ( exclude-urls) with Ant‑style patterns has the highest priority, preventing health‑check or public endpoints from being blocked.
YAML batch configuration
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/healthYAML rules have higher priority than annotations.
Whitelist entries have the highest priority. key-scope controls the dimension: user, ip or global.
Response handling
guardian:
repeat-submit:
response-mode: exception # default, throws RepeatSubmitException
# response-mode: json # directly returns JSONException mode requires a global exception handler:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RepeatSubmitException.class)
public Result handleRepeatSubmit(RepeatSubmitException e) {
return Result.fail(e.getMessage());
}
}JSON mode returns a default payload {"code":500,"msg":"...","timestamp":...}. A custom RepeatSubmitResponseHandler bean can override the format.
Running without Redis
Set storage: local to use an in‑memory ConcurrentHashMap with a scheduled cleanup thread. Production environments are still recommended to use Redis for distributed scenarios.
Context‑path handling
Guardian matches both the full request URI (including server.servlet.context-path) and the path stripped of the context‑path, so rules work regardless of how the URI is written in YAML.
Internal flow
Request → RepeatableRequestFilter (caches body) → RepeatSubmitInterceptor
├─ Check whitelist → allow if matched
├─ Match YAML rules
├─ Check @RepeatSubmit annotation → allow if none
▼
KeyGenerator (creates key based on scope)
KeyEncrypt (optional MD5)
Storage.tryAcquire()
├─ Success → proceed, store key with TTL
└─ Failure → response-mode handling (exception or JSON)
Business execution
├─ Normal → key expires naturally
└─ Exception → afterCompletion releases lockAPI rate limiting
Quick start
<dependency>
<groupId>io.github.biggg-guardian</groupId>
<artifactId>guardian-rate-limit-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>Annotation examples:
Sliding window (max 10 QPS): @RateLimit(qps = 10) Token bucket (5 QPS, burst capacity 20):
@RateLimit(qps = 5, capacity = 20, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)YAML batch configuration
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/**YAML rules have higher priority than annotations; whitelist has the highest priority.
Sliding window vs. token bucket
Sliding window counts requests within a fixed time window; it strictly enforces the limit (e.g., max 10 requests per second). Suitable for SMS sending or login attempts.
Token bucket refills tokens at a fixed rate; bursts up to the bucket capacity are allowed. Example: 5 QPS, capacity 20 permits a sudden burst of 20 requests, then returns to 5 QPS.
Illustrative comparison (both configured with qps=10 and a burst of 20 requests):
Sliding window: first 10 pass, next 10 rejected, thereafter max 10 per second.
Token bucket: first 10 pass, next 10 also pass (burst), thereafter max 10 per second.
Token refill control
qps=10, window=1s→ 10 tokens added each second. qps=10, window=1min → 10 tokens added each minute (≈1 token every 6 seconds).
@RateLimit(qps = 10, window = 1, windowUnit = TimeUnit.MINUTES,
capacity = 10, algorithm = RateLimitAlgorithm.TOKEN_BUCKET)Rate‑limit dimensions
GLOBAL (default): a single counter for the entire endpoint.
IP : separate counters per client IP (ideal for SMS, captcha).
USER : separate counters per authenticated user.
@RateLimit(qps = 1, rateLimitScope = RateLimitKeyScope.IP,
message = "短信发送过于频繁")Response handling
guardian:
rate-limit:
response-mode: exception # default, throws RateLimitException
# response-mode: json # directly returns JSONCustom RateLimitResponseHandler beans can override the JSON format.
Design details
Concurrency safety
Redis: both sliding window and token bucket use Lua scripts, guaranteeing atomic execution.
Local cache: synchronized on a per‑key basis, avoiding cross‑key blocking.
Duplicate‑submission also uses atomic SET NX EX in Redis and atomic methods of ConcurrentHashMap for local storage.
Local cache memory management
A daemon guard thread runs every 5 minutes to purge expired entries, preventing unbounded memory growth.
Pluggable architecture
Core components are defined as interfaces. Spring’s @ConditionalOnMissingBean supplies default implementations, which can be overridden by user‑provided beans.
Component mapping:
Key generation: RepeatSubmitKeyGenerator / RateLimitKeyGenerator Key encryption: AbstractKeyEncrypt Storage: RepeatSubmitStorage / RateLimitStorage Response handling: RepeatSubmitResponseHandler / RateLimitResponseHandler User context (shared):
UserContextObservability
Logging can be enabled with log-enabled: true to record both blocked and passed requests.
Actuator endpoints expose statistics:
GET /actuator/guardian-repeat-submit → duplicate‑submission stats
GET /actuator/guardian-rate-limit → rate‑limit stats {
"totalRequestCount": 5560,
"totalPassCount": 5432,
"totalBlockCount": 128,
"blockRate": "2.30%",
"topBlockedApis": { "/api/sms/send": 56 },
"topRequestApis": { "/api/search": 3200 }
}Project structure
guardian-parent
├── guardian-core # shared utilities
├── 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-storage-redis # Redis storage shared by both modules
└── guardian-example # example applicationModules are independent; include only the starter you need.
SpringMeng
Focused on software development, sharing source code and tutorials for various systems.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
