ruoyi-vue-plus RepeatSubmit源码解读

news/2024/10/18 0:23:57/

RepeatSubmit作用

同一个用户的统一操作, 在规定的时间内 只能做一次相同数据的请求,防止重复提交,超时重试等一些问题

RepeatSubmit使用注意事项

查询和删除是天然的幂等操作,故一般不建议使用。在有更新和写入的操作时,建议使用,例如订单提交,修改订单状态。
幂等性: 多次执行的结果和一次执行成功的结果对资源的作用是相同的,这里的资源一般指的是数据库的数据。

使用

interval 间隔时间 默认单位是ms message 为默认返回信息

@RepeatSubmit(interval = 10000,message = "不能重复提交")

ruoyi 脚手架的实现

使用 拦截器 + 过滤器 实现的

  1. 拦截器 : 对 带有RepeatSubmit 接口进行重复性校验,将 ( key标识 + 请求url + base64(用户请求数据)) 作为key,时间戳 + 请求数据 组成map 作为value, 当然了 key 可以自行进行修改 。
  2. 过滤器: 对请求进行过滤,将请求转化为可重复读取inputStream的request。

具体代码如下(本人已修改):

package com.example.interceptor;import java.lang.annotation.*;/*** 自定义注解防止表单重复提交* 建议查询和删除不要使用此注解。* @author ruoyi**/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit
{/*** 间隔时间(ms),小于此时间视为重复提交*/public int interval() default 5000;/*** 提示消息*/public String message() default "不允许重复提交,请稍候再试";
}
  1. 定义拦截器
package com.example.interceptor;import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;/*** 防止重复提交拦截器** @author ruoyi*/
@Component
@Slf4j
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor
{@Overridepublic 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)){log.info("重复提交了!!!可自行定义返回格式");// 重复提交了
//                    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);
}
  1. 具体实现机制
    可自定义key
package com.example.interceptor.impl;import com.alibaba.fastjson2.JSON;import com.example.interceptor.RepeatSubmit;
import com.example.interceptor.RepeatSubmitInterceptor;
import com.example.interceptor.RepeatedlyRequestWrapper;
import com.example.util.redis.RedisCache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import sun.misc.BASE64Encoder;import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** 判断请求url和数据是否和上一次相同,* 如果和上次相同,则是重复提交表单。 有效时间为10秒内。* * @author ruoyi*/
@Slf4j
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor
{public final String REPEAT_PARAMS = "repeatParams";public final String REPEAT_TIME = "repeatTime";/*** 防重提交 redis key*/public final String REPEAT_SUBMIT_KEY = "repeat_submit:";// 令牌自定义标识@Value("${token.header}")private String header;@Autowiredprivate RedisCache redisCache;@SuppressWarnings("unchecked")@Overridepublic boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation){BASE64Encoder encoder = new BASE64Encoder();String nowParams = "";if (request instanceof RepeatedlyRequestWrapper){// 注意 一定要使用过滤器 将 request 转换为 RepeatedlyRequestWrapper 不然无法获取请求参数RepeatedlyRequestWrapper repeatedlyRequest = (RepeatedlyRequestWrapper) request;nowParams = getBodyString(repeatedlyRequest);}// body参数为空,获取Parameter的数据  nowParamsif ("".equals(nowParams.trim())){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 str = request.getHeader(header);// 唯一值(没有消息头则使用请求地址)String submitKey =  str == null ? "" : str.trim();// 可以获取 不同的 IP地址 或者 mac地址 进行 主机识别  加上此注解的 通过id查询可能会受影响 所以建议查询和删除不要使用此注解即可。// 使用 BASE64 对 submitKey 和 请求数据 加密String dataEncryption = encoder.encode((submitKey+nowParams).getBytes(StandardCharsets.UTF_8));// 唯一标识(指定key + url + 请求数据 )String cacheRepeatKey = REPEAT_SUBMIT_KEY + url + dataEncryption;Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);// 判断 redis 中 有无重复的keyif (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;}/*** 判断参数是否相同*/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;}public 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){log.warn("getBodyString出现问题!");}finally{if (reader != null){try{reader.close();}catch (IOException e){log.error("关闭文件错误");}}}return sb.toString();}
}
  1. 可重复读取inputStream的request
package com.example.interceptor;import lombok.extern.slf4j.Slf4j;import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;
import java.nio.charset.StandardCharsets;/*** 构建可重复读取inputStream的request* * @author ruoyi*/
@Slf4j
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper
{private final byte[] body;public RepeatedlyRequestWrapper(HttpServletRequest request, ServletResponse response) throws IOException{super(request);request.setCharacterEncoding("UTF-8");response.setCharacterEncoding("UTF-8");body = getBodyString(request).getBytes(StandardCharsets.UTF_8);}@Overridepublic BufferedReader getReader() throws IOException{return new BufferedReader(new InputStreamReader(getInputStream()));}@Overridepublic ServletInputStream getInputStream() throws IOException{final ByteArrayInputStream bais = new ByteArrayInputStream(body);return new ServletInputStream(){@Overridepublic int read() throws IOException{return bais.read();}@Overridepublic int available() throws IOException{return body.length;}@Overridepublic boolean isFinished(){return false;}@Overridepublic boolean isReady(){return false;}@Overridepublic void setReadListener(ReadListener readListener){}};}public 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){log.warn("getBodyString出现问题!");}finally{if (reader != null){try{reader.close();}catch (IOException e){log.error("关闭文件错误");}}}return sb.toString();}
}
  1. 将拦截器注册进入系统配置中
package com.example.interceptor;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;/*** 通用配置* * @author ruoyi*/
@Configuration
public class ResourcesConfig implements WebMvcConfigurer
{@Autowiredprivate RepeatSubmitInterceptor repeatSubmitInterceptor;/*** 自定义拦截规则*/@Overridepublic void addInterceptors(InterceptorRegistry registry){// 把重复校验添加到系统配置中// 重复校验拦截所有请求   自行判断是否重复校验 有注解则校验 没有则不校验registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");}/*** 跨域配置*/@Beanpublic CorsFilter corsFilter(){CorsConfiguration config = new CorsConfiguration();config.setAllowCredentials(true);// 设置访问源地址config.addAllowedOriginPattern("*");// 设置访问源请求头config.addAllowedHeader("*");// 设置访问源请求方法config.addAllowedMethod("*");// 有效期 1800秒config.setMaxAge(1800L);// 添加映射路径,拦截一切请求UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();source.registerCorsConfiguration("/**", config);// 返回新的CorsFilterreturn new CorsFilter(source);}
}
  1. 定义过滤器
package com.example.filter;import com.example.interceptor.RepeatedlyRequestWrapper;
import org.springframework.http.MediaType;import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;/*** Repeatable 过滤器* * @author ruoyi*/
public class RepeatableFilter implements Filter
{@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException{ServletRequest requestWrapper = null;if (request instanceof HttpServletRequest&& startsWithIgnoreCase(request.getContentType(), MediaType.APPLICATION_JSON_VALUE)){requestWrapper = new RepeatedlyRequestWrapper((HttpServletRequest) request, response);}if (null == requestWrapper){chain.doFilter(request, response);}else{chain.doFilter(requestWrapper, response);}}public static boolean startsWithIgnoreCase(CharSequence str, CharSequence prefix) {return startsWith(str, prefix);}private static boolean startsWith(CharSequence str, CharSequence prefix) {if (str != null && prefix != null) {int preLen = prefix.length();return preLen <= str.length() && regionMatches(str, prefix, preLen);} else {return str == prefix;}}static boolean regionMatches(CharSequence cs, CharSequence substring, int length) {if (cs instanceof String && substring instanceof String) {return ((String)cs).regionMatches(true, 0, (String)substring, 0, length);} else {int index1 = 0;int index2 = 0;int tmpLen = length;int srcLen = cs.length();int otherLen = substring.length();if (length >= 0) {if (srcLen >= length && otherLen >= length) {while(tmpLen-- > 0) {char c1 = cs.charAt(index1++);char c2 = substring.charAt(index2++);if (c1 != c2) {char u1 = Character.toUpperCase(c1);char u2 = Character.toUpperCase(c2);if (u1 != u2 && Character.toLowerCase(u1) != Character.toLowerCase(u2)) {return false;}}}return true;} else {return false;}} else {return false;}}}
}
  1. 将过滤器注册进入系统中
package com.example.filter;import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;/*** Filter配置** @author ruoyi*/
@Configuration
public class FilterConfig
{@SuppressWarnings({ "rawtypes", "unchecked" })@Beanpublic FilterRegistrationBean someFilterRegistration(){FilterRegistrationBean registration = new FilterRegistrationBean();registration.setFilter(new RepeatableFilter());registration.addUrlPatterns("/*");registration.setName("repeatableFilter");registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);return registration;}
}

http://www.ppmy.cn/news/1448482.html

相关文章

配置及使用OpenCV(Python)

python配置OpenCV相对于c的配置方法容易的多&#xff0c;但建议在Anaconda中的Python虚拟环境中使用&#xff0c;这样更方便进行包管理和环境管理&#xff1a; 先激活Anaconda的python虚拟环境&#xff1a; conda activate GGBoy 随后下载 opencv 包&#xff1a; conda ins…

【二叉树——数据结构】

文章目录 1.二叉树1.基本概念.几种特殊的二叉树 2.考点3.二叉树的存储结构4.二叉树的遍历5.线索二叉树 1.二叉树 1.基本概念. 二叉树是n(n>0)个结点的有限集合 或者为空二叉树&#xff0c;即n0 或者由一个根结点和两个互不相交的被称作根的左子树和右子树组成。 每个结点至…

CSS中不固定大小的图片怎样做到在所在的块元素里垂直居中

对于不固定大小的图片&#xff0c;在块元素中实现垂直居中可以有多种方法。以下是一些常用的方法&#xff1a; 使用Flexbox&#xff08;弹性盒子&#xff09;: Flexbox 是一个非常强大的布局工具&#xff0c;可以轻松实现元素的垂直居中。你只需要将块元素设置为 flex 容器&a…

Go Web 开发基础【用户登录、注册、验证】

前言 这篇文章主要是学习怎么用 Go 语言&#xff08;Gin&#xff09;开发Web程序&#xff0c;前端太弱了&#xff0c;得好好补补课&#xff0c;完了再来更新。 1、环境准备 新建项目&#xff0c;生成 go.mod 文件&#xff1a; 出现报错&#xff1a;go: modules disabled by G…

网盘—下载文件

本文主要讲解网盘文件操作的下载文件部分&#xff0c;具体步骤如下&#xff1a; 目录 1、实施步骤 2、代码实现 2.1、添加下载文件的协议 2.2、添加下载文件函数 2.3、添加信号槽 2.4、实现槽函数 2.5、设置download状态 2.6、添加定义 2.7、服务器接收数据 2.8、添…

C语言中的指针常量和常量指针

指针常量和常量指针是C/C编程语言中两个重要的概念&#xff0c;它们都与指针有关&#xff0c;但具有不同的含义和用途。 1. 指针常量&#xff08;Pointer to Constant&#xff09; 指针常量指的是一个指针的值&#xff08;即它所指向的地址&#xff09;在初始化之后不能再被改…

vue 组件组件通信方法

1、父组件props传值给子组件。 子组件中定义props字段&#xff0c;类型为Array&#xff08;如需限制字段值类型&#xff0c;也可以定义为Object的形式&#xff09;。如下例子&#xff0c;父组件挂载子组件helloWorld&#xff0c;在组件标签上给title赋值&#xff0c;子组件hel…

【项目纪实】某国有航空公司人力资源系统诊断咨询项目

公司的人力资源管理问题一直都比较严重&#xff0c;比如人员冗余、员工工作积极性差等问题&#xff0c;虽然经过多次的管理尝试&#xff0c;存在的问题仍然没有缓解。华恒智信人力资源咨询公司的老师特别专业&#xff0c;帮我们系统、全面的诊断了人力资源管理上存在的问题&…