在某些状况下,因为网速慢,用户操做有误(连续点击两下提交按钮),页面卡顿等缘由,可能会出现表单数据重复提交形成数据库保存多条重复数据。前端
存在如上问题能够交给前端解决,判断多长时间内不能再次点击保存按钮,固然,若是存在聪明的用户可以绕过前端验证,后端更应该去进行拦截处理,下面小编将基于 SpringBoot 2.1.8.RELEASE
环境经过 AOP切面
+ 自定义校验注解
+ Redis缓存
来解决这一问题。java
pom.xml
中引入所需依赖<!-- ================== 校验表单重复提交所需依赖 ===================== --> <!-- AOP依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- Redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
application.yml
中引入Redis配置spring: redis: # Redis数据库索引(默认为0) database: 0 # Redis服务器地址 host: 127.0.0.1 # Redis服务器链接端口 port: 6379 timeout: 6000 # Redis服务器链接密码(默认为空) # password: jedis: pool: max-active: 1000 # 链接池最大链接数(使用负值表示没有限制) max-wait: -1 # 链接池最大阻塞等待时间(使用负值表示没有限制) max-idle: 10 # 链接池中的最大空闲链接 min-idle: 5 # 链接池中的最小空闲链接
@NoRepeatSubmit
// 做用到方法上 @Target(ElementType.METHOD) // 运行时有效 @Retention(RetentionPolicy.RUNTIME) public @interface NoRepeatSubmit { /** * 默认时间3秒 */ int time() default 3 * 1000; }
注:这里redis存储的key
值可由我的具体业务灵活发挥,这里只是示例
ex:单用户登陆状况下能够组合 token + url请求路径
, 多个用户能够同时登陆的话,能够再加上 ip地址
git
@Slf4j @Aspect @Component public class NoRepeatSubmitAop { @Autowired RedisUtil redisUtil; /** * <p> 【环绕通知】 用于拦截指定方法,判断用户表单保存操做是否属于重复提交 <p> * * 定义切入点表达式: execution(public * (…)) * 表达式解释: execution:主体 public:可省略 *:标识方法的任意返回值 任意包+类+方法(…) 任意参数 * * com.zhengqing.demo.modules.*.api : 标识AOP所切服务的包名,即须要进行横切的业务类 * .*Controller : 标识类名,*即全部类 * .*(..) : 标识任何方法名,括号表示参数,两个点表示任何参数类型 * * @param pjp:切入点对象 * @param noRepeatSubmit:自定义的注解对象 * @return: java.lang.Object */ @Around("execution(* com.zhengqing.demo.modules.*.api.*Controller.*(..)) && @annotation(noRepeatSubmit)") public Object doAround(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) { try { HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); // 拿到ip地址、请求路径、token String ip = IpUtils.getIpAdrress(request); String url = request.getRequestURL().toString(); String token = request.getHeader(Constants.REQUEST_HEADERS_TOKEN); // 如今时间 long now = System.currentTimeMillis(); // 自定义key值方式 String key = "REQUEST_FORM_" + ip; if (redisUtil.hasKey(key)) { // 上次表单提交时间 long lastTime = Long.parseLong(redisUtil.get(key)); // 若是如今距离上次提交时间小于设置的默认时间 则 判断为重复提交 不然 正常提交 -> 进入业务处理 if ((now - lastTime) > noRepeatSubmit.time()) { // 非重复提交操做 - 从新记录操做时间 redisUtil.set(key, String.valueOf(now)); // 进入处理业务 ApiResult result = (ApiResult) pjp.proceed(); return result; } else { return ApiResult.fail("请勿重复提交!"); } } else { // 这里是第一次操做 redisUtil.set(key, String.valueOf(now)); ApiResult result = (ApiResult) pjp.proceed(); return result; } } catch (Throwable e) { log.error("校验表单重复提交时异常: {}", e.getMessage()); return ApiResult.fail("校验表单重复提交时异常!"); } } }
因为太多,这里就不直接贴出来了,可参考文末给出的案例demo源码redis
在须要校验的方法上加上自定义的校验注解 @NoRepeatSubmit
便可spring
@RestController public class IndexController extends BaseController { @NoRepeatSubmit @GetMapping(value = "/index", produces = "application/json;charset=utf-8") public ApiResult index() { return ApiResult.ok("Hello World ~ "); } }
这里重复访问此 index
api请求以模拟提交表单测试数据库
第一次访问 http://127.0.0.1:8080/index
屡次刷新此请求,则提示请勿重复提交!
json
AOP切面
在进入方法前拦截
进行表单重复提交校验逻辑处理Redis
的 key-value键值对
存储 须要的逻辑判断数据 【ex:key存储用户提交表单的api请求路径,value存储提交时间】逻辑处理
:若是api听从的是严格的Restful风格
即 @PostMapping
用于表单提交操做,则可不用自定义注解方式去判断须要校验重复提交的路径,直接在aop切面拦截该请求路径后,经过反射拿到该方法上的注解是否存在 @PostMapping
若是存在则是提交表单的api,即进行校验处理,若是不存在便是其它的 @GetMapping
、 @PutMapping
、@DeleteMapping
操做 ...后端