RuoYi重复提交:防重复请求拦截器
引言:为什么需要防重复提交?
在Web应用开发中,重复提交(Repeat Submit)是一个常见但容易被忽视的问题。想象一下这样的场景:用户点击"提交订单"按钮后,由于网络延迟或页面响应慢,用户可能会重复点击,导致同一订单被创建多次。这不仅会造成数据冗余,还可能引发业务逻辑错误和用户体验问题。
RuoYi框架通过精心设计的防重复提交拦截器,为开发者提供了一套完整的解决方案。本文将深入解析RuoYi的防重复提交机制,从原理到实践,帮助你全面掌握这一重要功能。
防重复提交的核心架构
RuoYi的防重复提交机制采用经典的拦截器模式,结合自定义注解和Session存储,构建了一个灵活高效的防护体系。
核心组件说明
| 组件名称 | 类型 | 职责描述 |
|---|---|---|
RepeatSubmitInterceptor | 抽象类 | 防重复提交拦截器基类,定义核心拦截逻辑 |
SameUrlDataInterceptor | 实现类 | 具体防重复提交实现,基于URL和参数比较 |
@RepeatSubmit | 注解 | 标记需要防重复提交的方法 |
ResourcesConfig | 配置类 | 注册拦截器到Spring MVC |
深入源码解析
1. 自定义注解 @RepeatSubmit
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
/**
* 间隔时间(ms),小于此时间视为重复提交
* 默认值:5000毫秒(5秒)
*/
public int interval() default 5000;
/**
* 提示消息
* 默认值:"不允许重复提交,请稍后再试"
*/
public String message() default "不允许重复提交,请稍后再试";
}
2. 抽象拦截器基类
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
if (handler instanceof HandlerMethod)
{
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null)
{
if (this.isRepeatSubmit(request, annotation))
{
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false; // 拦截请求
}
}
return true; // 放行请求
}
else
{
return true;
}
}
public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception;
}
3. 具体实现:SameUrlDataInterceptor
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{
public final String REPEAT_PARAMS = "repeatParams";
public final String REPEAT_TIME = "repeatTime";
public final String SESSION_REPEAT_KEY = "repeatData";
@SuppressWarnings("unchecked")
@Override
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception
{
// 获取当前请求参数和时间
String nowParams = JSON.toJSONString(request.getParameterMap());
Map<String, Object> nowDataMap = new HashMap<String, Object>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
String url = request.getRequestURI();
HttpSession session = request.getSession();
Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY);
if (sessionObj != null)
{
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url))
{
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
if (compareParams(nowDataMap, preDataMap) &&
compareTime(nowDataMap, preDataMap, annotation.interval()))
{
return true; // 判定为重复提交
}
}
}
// 更新Session数据
Map<String, Object> sessionMap = new HashMap<String, Object>();
sessionMap.put(url, nowDataMap);
session.setAttribute(SESSION_REPEAT_KEY, sessionMap);
return false; // 非重复提交
}
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
{
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}
private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval)
{
long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME);
return (time1 - time2) < interval;
}
}
防重复提交的工作流程
配置和使用指南
1. 拦截器配置
在ResourcesConfig中自动配置拦截器:
@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
@Autowired
private RepeatSubmitInterceptor repeatSubmitInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
}
}
2. 在Controller中使用
@RestController
@RequestMapping("/order")
public class OrderController
{
@PostMapping("/create")
@RepeatSubmit(interval = 3000, message = "请勿重复提交订单")
public AjaxResult createOrder(@RequestBody Order order)
{
// 订单创建业务逻辑
orderService.createOrder(order);
return AjaxResult.success("订单创建成功");
}
}
3. 配置参数说明
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
interval | int | 5000 | 防重复时间间隔(毫秒) |
message | String | "不允许重复提交,请稍后再试" | 重复提交时的提示消息 |
高级应用场景
场景1:不同业务的不同防重复策略
@RestController
public class BusinessController
{
// 支付操作:2秒内防重复
@PostMapping("/payment")
@RepeatSubmit(interval = 2000, message = "支付请求处理中,请勿重复操作")
public AjaxResult payment(@RequestBody PaymentRequest request) {
// 支付逻辑
}
// 数据提交:5秒内防重复
@PostMapping("/submitData")
@RepeatSubmit(interval = 5000, message = "数据提交中,请稍后重试")
public AjaxResult submitData(@RequestBody DataModel data) {
// 数据提交逻辑
}
// 重要操作:10秒内防重复
@PostMapping("/criticalOperation")
@RepeatSubmit(interval = 10000, message = "重要操作执行中,请勿重复提交")
public AjaxResult criticalOperation(@RequestBody OperationRequest request) {
// 重要操作逻辑
}
}
场景2:自定义防重复策略
如果需要实现不同的防重复逻辑,可以创建新的拦截器实现:
@Component
public class TokenBasedInterceptor extends RepeatSubmitInterceptor
{
@Override
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception
{
// 基于Token的防重复提交实现
String token = request.getHeader("X-CSRF-Token");
if (token != null) {
// 检查Token是否已使用
return tokenService.isTokenUsed(token);
}
return false;
}
}
性能优化建议
1. Session存储优化
对于高并发场景,可以考虑使用Redis替代Session存储:
@Component
public class RedisBasedInterceptor extends RepeatSubmitInterceptor
{
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception
{
String key = "repeat_submit:" + request.getRequestURI() + ":" +
JSON.toJSONString(request.getParameterMap());
if (redisTemplate.hasKey(key)) {
return true;
}
redisTemplate.opsForValue().set(key, "1", annotation.interval(), TimeUnit.MILLISECONDS);
return false;
}
}
2. 参数比较优化
对于大型参数,可以使用MD5摘要进行比较:
private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap)
{
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
// 使用MD5摘要比较,避免大字符串比较的性能问题
String nowMd5 = DigestUtils.md5DigestAsHex(nowParams.getBytes());
String preMd5 = DigestUtils.md5DigestAsHex(preParams.getBytes());
return nowMd5.equals(preMd5);
}
常见问题解答
Q1: 防重复提交拦截器会影响性能吗?
A: 拦截器本身开销很小,主要性能消耗在于Session操作和参数比较。建议合理设置时间间隔,避免过短的间隔增加不必要的检查。
Q2: 如何处理分布式环境下的重复提交?
A: 在分布式环境中,Session存储可能不适用。建议使用Redis等分布式缓存来存储防重复提交的状态信息。
Q3: 是否可以自定义错误响应格式?
A: 可以重写RepeatSubmitInterceptor的preHandle方法,自定义错误响应的格式和内容。
Q4: 如何测试防重复提交功能?
A: 可以使用Postman或JMeter等工具模拟快速重复请求,验证拦截器是否正常工作。
最佳实践总结
- 合理设置时间间隔:根据业务场景设置合适的防重复时间,一般建议2-10秒
- 明确错误提示:提供清晰的错误消息,帮助用户理解操作状态
- 选择性使用:只在真正需要防重复的业务方法上使用注解
- 性能监控:在高并发场景下监控拦截器的性能表现
- 分布式适配:在微服务架构中使用分布式存储方案
RuoYi的防重复提交拦截器提供了一个灵活、可扩展的解决方案,通过合理的配置和使用,可以有效防止重复提交问题,提升系统的稳定性和用户体验。
扩展思考
未来改进方向
- 支持多种防重复策略:如Token机制、时间戳签名等
- 提供可视化配置:通过管理界面配置防重复规则
- 集成限流功能:结合限流组件提供更全面的防护
- 支持异步请求:优化对Ajax请求的防重复处理
通过深入理解和合理运用RuoYi的防重复提交机制,开发者可以构建更加健壮和用户友好的Web应用程序。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



