Spring Boot 接口防抖(防重复提交)实战:基于 @Lock4j + Hutool 实现
Spring Boot 接口防抖(防重复提交)实战:基于 @Lock4j + Hutool 实现
在实际开发中,接口重复提交是高频问题(如用户快速点击按钮、网络延迟导致的重试),尤其匿名接口(无需登录)更易出现。本文基于 @Lock4j 分布式锁 + Hutool 工具类,实现一套支持单机/集群环境的接口防抖方案,核心逻辑是通过「客户端IP + 接口标识」生成唯一锁键,拦截短时间内的重复请求。
👉 适用场景:匿名表单提交、游客评论、验证码发送等无需登录的接口;稍作修改也可适配登录接口(替换为用户ID+接口标识)。
一、方案核心思路
-
IP 识别:通过 Hutool 的
ServletUtil工具类,快速获取客户端真实 IP(支持反向代理/负载均衡场景); -
分布式锁拦截:用
@Lock4j注解为接口添加分布式锁,锁键由「IP + 接口标识」动态生成; -
防抖控制:设置锁过期时间(防抖窗口,如3秒),同一 IP 在窗口期内重复请求会因获取锁失败被拦截;
-
友好提示:全局异常捕获锁失败异常,返回标准化响应(429 状态码 + 提示语)。
二、环境准备
2.1 核心依赖(Maven)
需引入 Spring Boot Web、Lock4j 分布式锁、Redis(锁存储介质)、Hutool 工具包,直接复制到 pom.xml 即可:
<!-- Spring Boot Web 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Hutool 工具包(获取真实IP) -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version> <!-- 推荐最新稳定版 -->
</dependency>
<!-- Lock4j 分布式锁核心(注解式开发) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<!-- Redis 依赖(Lock4j 底层存储,必须) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.2 配置文件(application.yml)
配置 Redis 连接信息和 Lock4j 全局参数(防抖窗口、锁前缀等):
spring:
# Redis 配置(根据实际环境修改)
redis:
host: 127.0.0.1
port: 6379
password: 123456 # 无密码则省略
database: 0
timeout: 5000ms
# Spring MVC 路径匹配策略(Spring Boot 3.x+ 需配置)
mvc:
pathmatch:
matching-strategy: ant_path_matcher
# Lock4j 全局配置(防抖核心参数)
lock4j:
lock-type: redis # 锁类型:Redis(默认,支持Zookeeper)
redis:
key-prefix: API_DEBOUNCE_ # 锁键前缀,避免与其他业务锁冲突
expire: 3000 # 锁过期时间 = 防抖窗口(3秒内重复请求拦截)
acquire-timeout: 100 # 获取锁等待时间(100ms,重复请求直接失败)
time-unit: MILLISECONDS # 时间单位(默认毫秒)
三、核心实现代码
3.1 IP 工具类(基于 Hutool 封装)
统一封装 IP 获取逻辑,适配反向代理场景(如 Nginx),避免代码冗余:
import cn.hutool.extra.servlet.ServletUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
/**
* IP 工具类(基于 Hutool 封装,支持反向代理)
*/
@Component
public class IpHelper {
// 可信代理 IP 段(根据实际代理服务器配置,如 Nginx、网关)
// 内网网段参考:192.168.0.0/16、10.0.0.0/8、172.16.0.0/12
private static final String[] TRUST_PROXY_IPS = {"192.168.1.0/24", "10.0.0.0/8", "172.16.0.0/12"};
/**
* 获取客户端真实 IP
* @param request 请求对象
* @return 真实 IP 字符串
*/
public String getRealClientIp(HttpServletRequest request) {
// Hutool 核心方法:自动解析 X-Forwarded-For、X-Real-IP 等请求头
// 跳过可信代理 IP,获取真实客户端 IP
return ServletUtil.getClientIP(request, TRUST_PROXY_IPS);
}
}
3.2 通用封装类(DTO + 统一返回结果)
3.2.1 表单提交 DTO(请求参数)
import lombok.Data;
/**
* 表单提交请求参数 DTO
*/
@Data
public class FormSubmitDTO {
// 表单标题
private String title;
// 表单内容
private String content;
// 扩展字段:可根据业务添加(如联系方式、验证码等)
private String contact;
}
3.2.2 统一返回结果类
标准化接口响应格式,方便前端统一处理:
import lombok.Data;
/**
* 全局统一返回结果
*/
@Data
public class Result<T> {
// 响应码(200成功,429请求频繁,500服务器错误等)
private int code;
// 响应消息
private String msg;
// 响应数据
private T data;
// 成功响应(带数据)
public static <T> Result<T> success(T data) {
return new Result<>(200, "操作成功", data);
}
// 成功响应(无数据)
public static <T> Result<T> success() {
return new Result<>(200, "操作成功", null);
}
// 失败响应
public static <T> Result<T> fail(int code, String msg) {
return new Result<>(code, msg, null);
}
}
3.3 防抖接口实现(核心 Controller)
用@Lock4j 注解实现防抖,通过 SpEL 表达式动态生成锁键:
import com.baomidou.lock.annotation.Lock4j;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 接口防抖 Demo Controller(匿名表单提交)
*/
@RestController
@RequestMapping("/api/debounce")
public class DebounceController {
// 注入 IP 工具类
@Resource
private IpHelper ipHelper;
/**
* 匿名表单提交接口(防抖:同一IP 3秒内只能提交一次)
* 核心逻辑:锁键 = ip_ + 真实IP + _form_submit(保证IP+接口维度唯一)
*/
@PostMapping("/submit-form")
@Lock4j(
// SpEL 表达式:调用 IpHelper 获取真实IP,拼接接口标识
key = "'ip_' + @ipHelper.getRealClientIp(#request) + '_form_submit'",
// 锁过期时间(覆盖全局配置,可选)
expire = 3000,
// 获取锁失败提示语(前端展示)
msg = "操作太频繁啦,请3秒后再试~"
)
public Result<String> submitForm(@RequestBody FormSubmitDTO formDTO, HttpServletRequest request) {
// ========== 核心业务逻辑(根据实际需求修改) ==========
String clientIp = ipHelper.getRealClientIp(request);
System.out.printf("IP:%s 提交表单,标题:%s,内容:%s%n",
clientIp, formDTO.getTitle(), formDTO.getContent());
// 模拟业务处理(如保存表单到数据库)
// formService.save(formDTO);
// ========== 业务逻辑结束 ==========
return Result.success("表单提交成功,已收到您的反馈~");
}
/**
* 简化写法:不封装 IpHelper,直接在 SpEL 中调用 Hutool 工具类
* 适用场景:小型项目,无需复用 IP 获取逻辑
*/
@PostMapping("/submit-form-simple")
@Lock4j(
// 直接调用 Hutool 的 ServletUtil,指定可信代理 IP
key = "'ip_' + T(cn.hutool.extra.servlet.ServletUtil).getClientIP(#request, {'192.168.1.0/24'}) + '_form_simple'",
msg = "操作太频繁,请稍后再试~"
)
public Result<String> submitFormSimple(@RequestBody FormSubmitDTO formDTO, HttpServletRequest request) {
String clientIp = cn.hutool.extra.servlet.ServletUtil.getClientIP(request, new String[]{"192.168.1.0/24"});
System.out.printf("简化写法 - IP:%s 提交表单%n", clientIp);
return Result.success("简化版表单提交成功");
}
}
3.4 全局异常处理器(捕获防抖异常)
当重复请求获取锁失败时,@Lock4j 会抛出 LockFailureException,通过全局异常处理器返回友好提示:
import com.baomidou.lock.exception.LockFailureException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理器
*/
@RestControllerAdvice // 拦截所有 @RestController 接口
public class GlobalExceptionHandler {
/**
* 捕获 Lock4j 防抖异常(请求频繁)
*/
@ExceptionHandler(LockFailureException.class)
public Result<?> handleDebounceException(LockFailureException e) {
// 返回 429 状态码(HTTP 标准:请求过于频繁)
return Result.fail(429, e.getMessage());
}
/**
* 捕获其他异常(可选,增强鲁棒性)
*/
@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
return Result.fail(500, "服务器内部错误:" + e.getMessage());
}
}
四、反向代理配置(Nginx 示例)
如果服务部署在 Nginx 等反向代理之后,需在 Nginx 配置中添加以下参数,否则 ServletUtil 获取的是代理服务器 IP,而非客户端真实 IP:
server {
listen 80;
server_name your-domain.com; # 你的域名
location / {
# 转发请求到后端 Spring Boot 服务
proxy_pass http://127.0.0.1:8080;
# 传递真实客户端 IP 的核心配置(必须)
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr; # 客户端真实 IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 代理链 IP 列表
}
}
五、测试验证
5.1 测试步骤
-
启动 Redis 服务(确保配置文件中的 Redis 地址/密码正确);
-
启动 Spring Boot 项目;
-
用 Postman 或 curl 工具,连续 2 次请求接口:
POST http://localhost:8080/api/debounce/submit-form; -
请求体(JSON 格式):
{ "title": "测试接口防抖", "content": "这是第一次提交的内容", "contact": "test@example.com" }
5.2 预期结果
-
第一次请求:返回
{"code":200,"msg":"操作成功","data":"表单提交成功,已收到您的反馈~"}; -
3秒内第二次请求:返回
{"code":429,"msg":"操作太频繁啦,请3秒后再试~","data":null}; -
3秒后再次请求:恢复正常响应,返回成功信息。
六、关键注意事项(避坑指南)
-
锁键必须唯一:锁键公式为「固定前缀 + 请求主体标识 + 接口标识」,本文中是
ip_ + IP + _form_submit。如果锁键不唯一,会导致不同 IP/接口的请求互相拦截,或无法拦截重复请求; -
可信代理 IP 必配置:如果服务有反向代理,必须在
IpHelper中配置TRUST_PROXY_IPS,否则恶意用户可伪造X-Forwarded-For头,导致 IP 获取错误; -
防抖窗口合理设置:
expire参数(防抖窗口)建议设置为 2~5 秒。太短无法有效拦截重复请求,太长会影响用户体验; -
集群环境注意事项:所有服务实例必须连接同一个 Redis 集群,否则锁无法互通,防抖失效;
-
登录接口适配:如果是登录接口,只需将锁键中的「IP」替换为「用户ID」(如
user_ + #userId + _submit_order),即可实现同一用户的防抖。
七、方案优势对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 前端防抖(按钮置灰) | 简单、无后端压力 | 无法防绕过前端的恶意请求(如 Postman 直接调用) |
| 基于 Session 防抖 | 实现简单 | 不支持分布式集群,匿名接口无法使用 |
| 本文方案(@Lock4j + Hutool) | 1. 注解式开发,无侵入;2. 支持分布式集群;3. 适配匿名/登录接口;4. 自动释放锁,防死锁;5. 集成简单,代码复用性高 | 依赖 Redis 服务 |
八、扩展场景:登录接口防抖适配
如果需要适配登录接口(基于用户ID防抖),只需修改 Controller 中的锁键,示例如下:
import com.baomidou.lock.annotation.Lock4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* 登录接口防抖示例
*/
@RestController
@RequestMapping("/api/user")
public class UserController {
// 模拟获取当前登录用户ID(实际项目从 Token/Session 中获取)
private Long getCurrentUserId() {
// 示例:从 ThreadLocal 中获取用户ID(Spring Security/Shiro 可直接获取)
return 1001L;
}
/**
* 订单提交接口(防抖:同一用户 5 秒内只能提交一次)
*/
@PostMapping("/submit-order")
@Lock4j(
// 锁键:user_ + 用户ID + _submit_order
key = "'user_' + T(com.example.demo.util.UserContext).getUserId() + '_submit_order'",
expire = 5000,
msg = "订单提交中,请5秒后再试~"
)
public Result<String> submitOrder(@RequestBody OrderDTO orderDTO) {
Long userId = getCurrentUserId();
System.out.printf("用户 %d 提交订单:%s%n", userId, orderDTO.getOrderNo());
// 业务逻辑:创建订单、扣减库存等
return Result.success("订单提交成功");
}
}
总结
本文基于 @Lock4j 和 Hutool 实现的接口防抖方案,兼顾了简洁性和实用性,支持单机/集群环境,适配匿名/登录多种场景。核心是通过「唯一锁键」和「过期时间」控制请求频率,配合 Hutool 快速获取真实 IP,无需手动编写加锁/解锁逻辑,大幅提升开发效率。
👉 完整代码可直接复制到项目中使用,只需根据实际业务修改「业务逻辑」和「锁键规则」即可。
(注:文档部分内容可能由 AI 生成)
更多推荐


所有评论(0)