若依框架——防重复提交自定义注解

28 阅读9分钟

防重复提交

1、自定义防重复提交注解

/**
 * 自定义注解防止表单重复提交
 * 
 * @author ruoyi
 *
 */
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{
    /**
     * 间隔时间(ms),小于此时间视为重复提交
     */
    public int interval() default 5000;

    /**
     * 提示消息
     */
    public String message() default "不允许重复提交,请稍候再试";
}
@Inherited:
该元注解表示如果一个类使用了这个 RepeatSubmit 注解,那么它的子类也会自动继承这个注解。这在某些需要对一组相关的控制器方法进行统一重复提交检查的场景下很有用,子类无需再次显式添加该注解。

@Target(ElementType.METHOD):
表明这个注解只能应用在方法上。在实际应用中,通常会将其添加到控制器类的处理请求的方法上,比如 Spring MVC 的 @RequestMapping 注解修饰的方法。

@Retention(RetentionPolicy.RUNTIME):
意味着该注解在运行时仍然存在,可以通过反射机制获取到。这样在运行时,通过 AOP(面向切面编程)等技术拦截方法调用时,就能够读取到注解的属性值,从而实现重复提交的检查逻辑。

@Documented:
这个元注解用于将注解包含在 JavaDoc 中。当生成项目文档时,使用了该注解的方法会在文档中显示该注解及其属性,方便其他开发者了解该方法具有防止重复提交的功能以及相关的配置参数。

    /**
     * 间隔时间(ms),小于此时间视为重复提交
     */
    public int interval() default 5000;

定义了一个名为 interval 的属性,类型为 int,表示两次提交之间允许的最小时间间隔,单位是毫秒。默认值为 5000,即 5 秒。如果两次提交的时间间隔小于这个值,就会被视为重复提交。

    /**
     * 提示消息
     */
    public String message() default "不允许重复提交,请稍候再试";

定义了一个名为 message 的属性,类型为 String,用于在检测到重复提交时返回给客户端的提示消息。默认消息为 “不允许重复提交,请稍候再试”。开发者可以根据具体业务需求,在使用注解时自定义这个提示消息。

2、防止重复提交的抽象类

抽象类可以自己有 具体方法

/**
 * 防止重复提交拦截器
 *
 * @author ruoyi
 */
@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;
        }
    }

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

2.1、preHandle 方法

自定义抽象类拦截器 RepeatSubmitInterceptor 实现了 HandlerInterceptor 接口,重写 preHandle 方法

preHandle方法是负责拦截请求的

  • 如果isRepeatSubmit方法返回true,表示当前请求是重复提交。此时会创建一个包含错误信息的AjaxResult对象,错误信息就是RepeatSubmit注解中设置的message。然后通过ServletUtils.renderString方法将AjaxResult对象转换为 JSON 字符串,并将其作为响应返回给客户端,同时返回false,阻止请求继续处理。
  • 如果方法上不存在RepeatSubmit注解,或者isRepeatSubmit方法返回false,表示当前请求不是重复提交,就返回true,允许请求继续执行后续的处理流程。
@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;
        }
    }

参数说明:

  • HttpServletRequest request:提供了关于当前 HTTP 请求的信息,如请求头、请求参数、请求方法等。
  • HttpServletResponse response:用于设置 HTTP 响应,例如设置响应头、响应状态码、写入响应内容等。
  • Object handler:代表即将被执行的处理器对象,在 Spring MVC 中,它通常是一个 HandlerMethod,但也可能是其他类型。

方法解释:

 if (handler instanceof HandlerMethod)
        {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            Method method = handlerMethod.getMethod();
            RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);

首先检查 handler 是否是 HandlerMethod 类型的 , 不是的话,直接放行,不做重复提交检查, 因为该拦截器主要针对被 @RepeatSubmit 注解标记的方法进行处理。

如果 handler 是 HandlerMethod 类型的话,将 handler 转换成为 HandlerMethod 并获取对应的 Method 对象。

然后通过 getMethod() 方法 获取 方法,并通过 getAnnotation 方法获取 RepeatSubmit 注解 ,

if (annotation != null) {
                if (this.isRepeatSubmit(request, annotation)) {
                    AjaxResult ajaxResult = AjaxResult.error(annotation.message());
                    ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
                    return false;
                }
            }
            return true;

判断是否获取到 RepeatSubmit 注解,没有获取到,返回 true , 允许请求继续执行后续的处理流程。

运用 isRepeatSubmit 方法 判断是否是 重复提交

如果当前请求是重复提交 将注解的 错误信息 封装给结果映射对象

并调用 renderString 方法 将字符串渲染到客户端

    /**
     * 将字符串渲染到客户端
     * 
     * @param response 渲染对象
     * @param string 待渲染的字符串
     */
    public static void renderString(HttpServletResponse response, String string)
    {
        try
        {
            response.setStatus(200);
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            response.getWriter().print(string);
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }

2.2、isRepeatSubmit 方法

判断是否重复提交 true 重复提交 false 不重复提交

    /**
     * 验证是否重复提交由子类实现具体的防重复提交的规则
     *
     * @param request 请求信息
     * @param annotation 防重复注解参数
     * @return 结果
     * @throws Exception
     */
    public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation);
    public final String REPEAT_PARAMS = "repeatParams";

    public final String REPEAT_TIME = "repeatTime";

    // 令牌自定义标识
    @Value("${token.header}")
    private String header;   // token.header = "Authorization"

    @Autowired
    private RedisCache redisCache;	


	@SuppressWarnings("unchecked")
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation)
    {
        String nowParams = "";
        if (request instanceof RepeatedlyRequestWrapper)
        {
            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
        }

        // body参数为空,获取Parameter的数据
        if (StringUtils.isEmpty(nowParams))
        {
            nowParams = JSON.toJSONString(request.getParameterMap());
        }
        Map<String, Object> nowDataMap = new HashMap<String, Object>();
        nowDataMap.put(REPEAT_PARAMS, nowParams);
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());

        // 请求地址(作为存放cache的key值)
        String url = request.getRequestURI();

        // 唯一值(没有消息头则使用请求地址)
        String submitKey = StringUtils.trimToEmpty(request.getHeader(header));

        // 唯一标识(指定key + url + 消息头)
        String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;

        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
        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> cacheMap = new HashMap<String, Object>();
        cacheMap.put(url, nowDataMap);
        redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
        return false;
    }
	@SuppressWarnings("unchecked")

注解 @SuppressWarnings("unchecked"): 这个注解用于抑制编译器的 “unchecked” 警告。在代码中,可能存在一些未经检查的类型转换操作,使用该注解可以告诉编译器忽略这些警告。

String nowParams = "";

初始化一个字符串变量 nowParams 用于存储当前请求的参数。

 if (request instanceof RepeatedlyRequestWrapper)
        {
            RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;
            nowParams = HttpHelper.getBodyString(repeatedlyRequest);
        }

判断 当前请求是否是 RepeatedlyRequestWrapper 类型的

RepeatedlyRequestWrapper 是自定义的 允许多次请求的请求体 (详情见备注)

如果是的话,强转对象,并且 通过 getBodyString 方法 (详情见备注) 获取请求体的字符串内容,并且赋值给 nowParams

        // body参数为空,获取Parameter的数据
        if (StringUtils.isEmpty(nowParams))
        {
            nowParams = JSON.toJSONString(request.getParameterMap());
        }
        Map<String, Object> nowDataMap = new HashMap<String, Object>();
        nowDataMap.put(REPEAT_PARAMS, nowParams); //REPEAT_PARAMS = "repeatParams"
        nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); //REPEAT_TIME = "repeatTime"

if (StringUtils.isEmpty(nowParams)):如果通过上述方式获取的 nowParams 为空,说明请求体可能为空,此时通过 JSON.toJSONString(request.getParameterMap()) 将请求参数转换为 JSON 字符串,并赋值给 nowParams。这样无论请求参数是在请求体中还是在 URL 参数中,都能获取到。

  • Map<String, Object> nowDataMap = new HashMap<String, Object>(); 创建一个新的 HashMap 用于存储当前请求的数据。
  • nowDataMap.put(REPEAT_ PARAMS, nowParams); 将获取到的请求参数存入 nowDataMap 中,使用常量 REPEAT_PARAMS 作为键。
  • nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); 将当前时间戳存入 nowDataMap 中,使用常量 REPEAT_TIME 作为键。
		// 请求地址(作为存放cache的key值)
        String url = request.getRequestURI();

        // 唯一值(没有消息头则使用请求地址)
        String submitKey = StringUtils.trimToEmpty(request.getHeader(header));

        // 唯一标识(指定key + url + 消息头)
        String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;
																// REPEAT_SUBMIT_KEY = "repeat_submit:"

  • String url = request.getRequestURI(); 获取当前请求的 URI。
  • String submitKey = StringUtils.trimToEmpty(request.getHeader(header)); 从请求头中获取指定的键值(header 变量可能是在类中定义的一个常量,表示要获取的请求头字段),并去除两端的空白字符。如果请求头中不存在该字段,则返回空字符串。
  • String cacheRepeatKey = CacheConstants . REPEAT_SUBMIT_KEY + url + submitKey; 使用一个常量 CacheConstants.REPEAT _SUBMIT_KEY 与请求 URI 和 submitKey 拼接生成一个唯一的缓存键 cacheRepeatKey。这个键用于在缓存中存储和检索与该请求相关的重复提交信息。
        Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
        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> cacheMap = new HashMap<String, Object>();
        cacheMap.put(url, nowDataMap);
        redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);
		rerurn false;

通过缓存键 先去 redis 中,看 是否存在相同的缓存信息 如果存在,说明之前有过类似的请求 ,进入判断

因为这里 redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS); 传了map,所以说 redisCache.getCacheObject(cacheRepeatKey); 得到的map,就是同样的类型的,所以键值就是 url 。

检查 sessionMap 这个 Map 中是否包含以当前请求的 url 作为键的记录。这一步是因为在缓存的数据结构中,url 被用作内层键来存储每个请求的具体数据。如果存在这个键,说明之前已经有针对该 url 的请求被缓存。

接下来

调用 compareParams 方法比较当前请求的数据 nowDataMap 和之前请求的数据 preDataMap 的参数是否相同,同时调用 compareTime 方法比较当前请求时间和之前请求时间的间隔是否小于 @RepeatSubmit 注解中配置的 interval 时间。如果参数相同且时间间隔小于设定值,说明当前请求可能是重复提交,返回 true。

如果缓存中不存在当前请求 url 的记录,或者当前请求不被判定为重复提交,则执行以下操作: Map<String, Object> cacheMap = new HashMap<String, Object>();:创建一个新的 HashMap 用于存储当前请求的数据。 cacheMap.put(url, nowDataMap);:将当前请求的 url 作为键,nowDataMap(包含当前请求参数和时间)作为值存入 cacheMap。 redisCache.setCacheObject(cacheRepeatKey, cacheMap, annotation.interval(), TimeUnit.MILLISECONDS);:将 cacheMap 以 cacheRepeatKey 为键存入 Redis 缓存中,缓存时间为 @RepeatSubmit 注解中配置的 interval 时间,时间单位为毫秒。这样下次相同 url 的请求过来时,就可以从缓存中获取到之前的请求数据进行比较。

2.2.1、compareParams 方法
    /**
     * 判断参数是否相同
     */
    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);
    }
2.2.2、compareTime 方法
   /**
     * 判断两次间隔时间
     */
    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;
    }

备注:

RepeatedlyRequestWrapper

一个自定义的请求包装类,允许多次读取请求体

/**
 * 构建可重复读取inputStream的request
 * 
 * @author ruoyi
 */
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{
    private final byte[] body;

    public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException
    {
        super(request);
        request.setCharacterEncoding(Constants.UTF8);
        response.setCharacterEncoding(Constants.UTF8);

        body = HttpHelper.getBodyString(request).getBytes(Constants.UTF8);
    }

    @Override
    public BufferedReader getReader() throws IOException
    {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public ServletInputStream getInputStream() throws IOException
    {
        final ByteArrayInputStream bais = new ByteArrayInputStream(body);
        return new ServletInputStream()
        {
            @Override
            public int read() throws IOException
            {
                return bais.read();
            }

            @Override
            public int available() throws IOException
            {
                return body.length;
            }

            @Override
            public boolean isFinished()
            {
                return false;
            }

            @Override
            public boolean isReady()
            {
                return false;
            }

            @Override
            public void setReadListener(ReadListener readListener)
            {

            }
        };
    }
}

getBodyString 方法

将二进制的输入流数据转换为易于处理的字符串形式,方便后续对请求体内容进行解析和处理

 public static String getBodyString(ServletRequest request)
    {
        StringBuilder sb = new StringBuilder();
        BufferedReader reader = null;
        try (InputStream inputStream = request.getInputStream())
        {
            reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
            String line = "";
            while ((line = reader.readLine()) != null)
            {
                sb.append(line);
            }
        }
        catch (IOException e)
        {
            LOGGER.warn("getBodyString出现问题!");
        }
        finally
        {
            if (reader != null)
            {
                try
                {
                    reader.close();
                }
                catch (IOException e)
                {
                    LOGGER.error(ExceptionUtils.getMessage(e));
                }
            }
        }
        return sb.toString();
    }