Ruoyi框架防重复提交组件深度解析:从注解到拦截器的完整实现

目录

前言

一、架构设计及核心源码剖析

1、防重复架构设计

2、核心源码剖析

3、设计亮点

二、实战应用指南

1、基础配置

2、在控制器中使用

3、实战结果

三、总结


前言

在当今高并发的互联网应用环境中,重复提交问题已成为Web开发中不可忽视的技术痛点。无论是由于用户网络延迟导致的多次点击,还是前端表单提交后的页面刷新,甚至是恶意攻击者的重放攻击,重复提交都可能引发严重的业务异常。想象一下这样的场景:用户在电商平台上点击"立即支付"按钮,由于网络卡顿,心急的用户连续点击了三次,结果账户被扣款三次,订单却只有一个;又或者是在金融系统中,一笔转账请求因重复提交而导致资金异常流转。这些真实案例背后,暴露的是系统在并发控制和请求幂等性设计上的缺陷。

防重复提交组件的设计与实现,其技术价值远不止于解决表面的重复请求问题。首先,从数据一致性角度看,它是保障业务正确性的最后一道防线,特别是在涉及资金交易、库存扣减、积分发放等关键业务场景下,幂等性控制直接关系到系统的数据完整性。其次,从系统性能优化层面,有效的防重机制能够显著降低服务器的无效计算负载,避免数据库的重复写入操作,提升整体吞吐量。更重要的是,优秀的防重复设计体现了软件工程中的关注点分离原则------通过AOP(面向切面编程)将横切关注点从业务代码中剥离,使得开发者能够以声明式的方式解决技术问题,而非在每个业务方法中编写冗长的防重逻辑。这种设计不仅提高了代码的可维护性,也为团队协作建立了统一的技术规范。在Spring生态日益成熟的今天,基于注解(Annotation)和拦截器(Interceptor)的防重方案已成为业界主流实践,它完美契合了"约定优于配置"的设计理念。

Ruoyi作为一款广受欢迎的开源后台管理系统框架,其自带了防重复提交组件。该框架采用"注解驱动+拦截器拦截"的架构,构建了一套完整且高效的防重解决方案。具体而言,开发者只需在Controller方法上添加@RepeatSubmit注解,即可声明该方法需要进行防重复提交校验,这种极低侵入性的使用方式极大提升了开发效率。在底层实现上,Ruoyi通过自定义拦截器拦截所有带注解的请求,结合请求URL、用户会话标识及请求参数生成唯一指纹,确保同一请求在指定时间窗口内只能被处理一次。此外,框架还提供了灵活的配置项,如防重时间间隔、是否忽略参数等,以适应不同的业务需求。接下来,本文将从源码层面深入剖析这一组件的实现原理,带您领略从注解定义、拦截器注册的完整技术链路。

一、架构设计及核心源码剖析

本节就来深入剖析Ruoyi框架中的防重复提交组件,看看它是如何通过注解和拦截器机制,实现高效、灵活的防重功能的。通过对防重复架构的设计和核心源码的剖析以及对设计的亮点总结,让大家对若依的防重复提交组件有一个简单的认识。

1、防重复架构设计

Ruoyi的防重复提交组件采用了经典的模板方法模式,将通用逻辑与具体实现分离:

bash 复制代码
RepeatSubmitInterceptor(抽象拦截器)
        ↑
        | 继承
        ↓
SameUrlDataInterceptor(具体实现:基于Session)

使用模板方法的方式进行扩展,实现了很好的灵活性和扩展性。这种设计具有以下两大优势:第一个是扩展性:可以轻松实现基于Redis、Token等不同策略的防重方案。第二个是一致性:所有防重实现遵循相同的接口规范。

2、核心源码剖析

首先需要创建一个注解类来实现防重复提交的基本设置,比较时间间隔和触发重复提交的消息信息。通过定义防重行为的元数据,注解是防重功能的入口,它允许开发者:

  • 指定时间间隔:默认5000毫秒内不允许重复提交

  • 自定义提示消息:当检测到重复提交时返回给客户端的消息

  • 按方法粒度控制:精确控制哪些接口需要防重保护

注解的核心源码如下:

java 复制代码
package com.yelang.framework.interceptor.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
 * 自定义注解防止表单重复提交
 * @author 夜郎king
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
    /**
     * 间隔时间(ms),小于此时间视为重复提交
     */
    public int interval() default 5000;
    /**
     * 提示消息
     */
    public String message() default "不允许重复提交,请稍后再试";
}

接下来需要定义一个防止重复提交的拦截器,在拦截器中定义验证的逻辑和定义验证是否重复提交由子类实现具体的防重复提交的规则,当发生重复提交的事件时,在验证逻辑中就会触发相应的提醒功能,返回提醒消息。该拦截器的设计思路**:**

  • 开闭原则:抽象拦截器对扩展开放(新增防重策略),对修改关闭(不改动核心流程)

  • 责任链模式:与Spring MVC拦截器链完美集成

  • 注解驱动:通过注解元数据控制行为,代码侵入性低

防重复拦截器的源码如下:

java 复制代码
package com.yelang.framework.interceptor;
import java.lang.reflect.Method;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import com.alibaba.fastjson.JSONObject;
import com.yelang.common.utils.ServletUtils;
import com.yelang.framework.interceptor.annotation.RepeatSubmit;
import com.yelang.framework.web.domain.AjaxResult;

/**
 * 防止重复提交拦截器
 * @author 夜郎king
 */
@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, JSONObject.toJSONString(ajaxResult));
                    return false;
                }
            }
            return true;
        }
        else
        {
            return true;
        }
    }

    /**
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     *
     * @param request 请求对象
     * @param annotation 防复注解
     * @return 结果
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
}

定义了核心拦截器,接下来就是具体的实现。Ruoyi框架默认提供的防重实现,其核心思想是:"相同URL + 相同参数 + 时间间隔内" = 重复提交。因此,相同的参数定义和时间间隔的设置将是决定请求是否重复提交的重要条件。其核心代码如下:

java 复制代码
package com.yelang.framework.interceptor.impl;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import com.yelang.framework.interceptor.RepeatSubmitInterceptor;
import com.yelang.framework.interceptor.annotation.RepeatSubmit;
/**
 * 判断请求url和数据是否和上一次相同, 
 * 如果和上次相同,则是重复提交表单。 有效时间为10秒内。
 * @author 夜郎king
 */
@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)
    {
        // 本次参数及系统时间
        String nowParams = JSONObject.toJSONString(request.getParameterMap());
        Map<String, Object> nowDataMap = new HashMap<String, Object>();
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
        // 请求地址(作为存放session的key值)
        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;
                }
            }
        }
        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);
        if ((time1 - time2) < interval)
        {
            return true;
        }
        return false;
    }
}

简单介绍一下这个算法的实现逻辑:

bash 复制代码
1. 序列化当前请求参数 → JSON字符串
2. 检查Session中是否存在相同URL的记录
   ↓ 存在
3. 比对参数JSON是否完全相等
   ↓ 相等
4. 计算时间差是否小于interval
   ↓ 小于
5. 判定为重复提交

3、设计亮点

Ruoyi框架的防重复提交组件展示了优秀框架设计的几个重要特点:

  1. 开闭原则的典范:抽象拦截器为扩展提供了无限可能。

  2. 注解驱动:最小化代码侵入,提升开发体验。

  3. 模板方法模式:分离不变部分(拦截流程)与可变部分(防重策略)。

  4. 配置灵活:支持按方法粒度的精细化控制。

二、实战应用指南

在了解了若依的防重复架构设计及其核心源码之后,接下来就是进入实战配置设置。是骡子是马,总是要拉出来溜溜,一试便知晓。本节将重点介绍在实战当中如何接入这个防重复提交的组件,并基于一个具体的例子,设置一个时间和提醒消息来进行演示。只有通过实际的运行例子,大家才能了解和掌握组件的运行情况。

1、基础配置

在SprinBoot当中,首先需要要注册Bean对象,并且注入我们设计的防重复提交组件。Bean注册的方法比较简单,直接在SpringBoot注入即可,方法如下:

java 复制代码
package com.yelang.framework.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.yelang.common.constant.Constants;
import com.yelang.framework.interceptor.RepeatSubmitInterceptor;
/**
 * 通用配置
 * @author 夜郎king
 */
@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{
    @Autowired
    private RepeatSubmitInterceptor repeatSubmitInterceptor;
    /**
     * 自定义拦截规则
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
    }
}

2、在控制器中使用

将Bean注入之后,接下来就是在业务实现的控制器中进行集成绑定,注册绑定防重复组件的方法也很简单,直接使用注解的形式进行集成。这里我们以一个之前实现过的一个学生管理系统的编辑方法微粒子进行说明,首先在控制器中引用防重复提交组件,核心代码如下:

java 复制代码
/**
* 修改保存学生管理-演示
*/
@RequiresPermissions("extend:student:edit")
@Log(title = "学生管理-演示", businessType = BusinessType.UPDATE)
@PostMapping("/edit")
@ResponseBody
@RepeatSubmit(interval = 3000, message = "请求过于频繁")
public AjaxResult editSave(Student student){
    return toAjax(studentService.updateStudent(student));
}

这里的核心就是:@RepeatSubmit(interval = 3000, message = "请求过于频繁")。这里表示使用间隔时间是3秒内提交重复参数都将视为重复提交。下面来看看实际的案例。

3、实战结果

下面我们结合这个编辑保存的案例来看看效果如何,我们首先打开信息编辑页面,如下图:

将信息输入完毕后,点击确定进行信息的保存。第一次时提示保存成功,如下图:

接下来我们先点编辑信息,然后再点提交,重复操作,由于我们之前设置了3秒的时间间隔,因此很容易重现。当再次打开编辑并且提交时,页面就会报以下错误:

至此整个防重复提交组件就已经生效,并且在实际的案例中进行运用。

三、总结

以上就是本文的主要内容,本文详细介绍了Ruoyi自带的防重复提交组件。该框架采用"注解驱动+拦截器拦截"的架构,构建了一套完整且高效的防重解决方案。具体而言,开发者只需在Controller方法上添加@RepeatSubmit注解,即可声明该方法需要进行防重复提交校验,这种极低侵入性的使用方式极大提升了开发效率。本文将从源码层面深入剖析这一组件的实现原理,带您领略从注解定义、拦截器注册的完整技术链路。行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激。