表单重复提交是在web中存在的一个很常见,会带来很多麻烦的一个问题。尤其是在表单新增的时候,如果重复提交了多条一样的数据,带来的麻烦更大。
 实现防止表单重复提交的方法有前端限制和后台限制
1、前端限制就是当点击了提交按钮之后,就给按钮添加属性disabled,然后等后台返回提交信息之后再将disabled移除掉2、后台实现是否重复提交的判断
前端限制按钮的方法比较简单,这里就不再介绍,这里主要介绍的是后台实现防止重复提交,利用Spring
AOP的面向切面编程的特点,可以实现不修改原代码的前提下动态的添加和删除校验。




先简单介绍一下Spring AOP和redis百度百科的AOPAOP为Aspect Oriented Programming的缩写,意为:面向切面编程
<https://baike.baidu.com/item/%E9%9D%A2%E5%90%91%E5%88%87%E9%9D%A2%E7%BC%96%E7%A8%8B>
,通过预编译 <https://baike.baidu.com/item/%E9%A2%84%E7%BC%96%E8%AF%91>
方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP <https://baike.baidu.com/item/OOP>
的延续,是软件开发中的一个热点,也是Spring <https://baike.baidu.com/item/Spring>框架中的一个重要内容,是函数式编程
<https://baike.baidu.com/item/%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B>
的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度
<https://baike.baidu.com/item/%E8%80%A6%E5%90%88%E5%BA%A6>
降低,提高程序的可重用性,同时提高了开发的效率。
我理解的AOP
简单的来讲,AOP其实就是利用动态代理(代理的对象可以是类或者是方法)来对类和方法进行预处理,或者可以当做过滤器,对调用方法或者类之前进行过滤。利用AOP可以不改动业务代码的前提下实现对方法和类的代理。从而降低耦合度,而且AOP可以动态的添加和删除。


AOP的通知类型如下



百度百科的redisRedis是一个开源的使用ANSI C语言
<https://baike.baidu.com/item/C%E8%AF%AD%E8%A8%80>
编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库
<https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E5%BA%93>
,并提供多种语言的API。redis是一个key-value存储系统
<https://baike.baidu.com/item/%E5%AD%98%E5%82%A8%E7%B3%BB%E7%BB%9F>
。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表
<https://baike.baidu.com/item/%E9%93%BE%E8%A1%A8>)、set(集合)、zset(sorted set
--有序集合)和hash(哈希类型)。这些数据类型
<https://baike.baidu.com/item/%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B>
都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。Redis采用的是基于内存的采用的是单进程单线程模型的KV数据库。

查阅资料发现网上有一些做法是,在进入表单页面的时候,给表单页面生成一个token,该token存储在session和request中。token在页面上使用隐藏域存放,并且在提交表单的时候一起提交。后台从request中获取到token之后和session中的token进行比较,如果匹配成功则从session中删除该token。但是这样的做法是只允许提交一次,万一如果是提交之后处理失败了,这样就只能重新进入页面再次进行提交。防止重复提交的意思应该是防止用户在提交一次表单之后,在表单还没有返回处理信息之前再次提交的意思,而不是说只允许用户提交一次表单。
在这里针对了以上的做法进行了优化1、使用了redis的分布式锁,分布式锁部分采纳了
https://www.cnblogs.com/linjiqin/p/8003838.html
<https://www.cnblogs.com/linjiqin/p/8003838.html>
2、使用了AOP的Around环绕通知,访问save方法之前,先判断该请求的token是否已经上锁了(不刷新页面的情况下token不会变化),如果已经上锁了,则返回信息提示重复提交。如果没有上锁,则将token加锁,然后调用save方法,当save方法处理完之后,然后再解锁。



代码如下:1、注解import java.lang.annotation.ElementType;import
java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 防止重复提交注解* @author zzp 2018.03.11*
@version 1.0*/@Retention(RetentionPolicy.RUNTIME) // 在运行时可以获取@Target(value =
{ElementType.METHOD, ElementType.TYPE})  // 作用到类,方法,接口上等public @interface
PreventRepetitionAnnotation {}
2、AOP代码import java.lang.reflect.Method;import java.util.UUID;import
javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpSession;
import org.aspectj.lang.ProceedingJoinPoint;import
org.aspectj.lang.annotation.Around;import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;import
org.com.rlid.utils.json.JsonBuilder;
import  org.springframework.beans.factory.annotation.Autowired;
import  org.springframework.context.annotation.EnableAspectJAutoProxy;import
org.springframework.stereotype.Component;import
demo.zzp.app.aop.annotation.OperaterAnnotation;import
demo.zzp.app.redis.JedisUtils;/*** 防止重复提交操作AOP类* @author zzp 2018.03.10*
@version 1.0*/@[email protected]@EnableAspectJAutoProxy(proxyTargetClass=true)
public class PreventRepetitionAspect {          @Autowired     private
JedisUtils jedisUtils;     private static final String PARAM_TOKEN = "token";
    private static final String PARAM_TOKEN_FLAG =  "tokenFlag";        /**
      * around      * @throws Throwable      */     @Around(value
=  "@annotation(demo.zzp.app.aop.annotation.PreventRepetitionAnnotation)")
     public Object excute(ProceedingJoinPoint  joinPoint) throws Throwable{
         try {              Object result = null;              Object[] args =
joinPoint.getArgs();              for(int i = 0;i < args.length;i++){
                  if(args[i] != null && args[i]  instanceof HttpServletRequest){
                       HttpServletRequest request =  (HttpServletRequest)
args[i];//被调用的方法需要加上HttpServletRequest request这个参数
                       HttpSession session =  request.getSession();
                       if(request.getMethod().equalsIgnoreCase("get")){
                            //方法为get                            result
=  generate(joinPoint, request, session,  PARAM_TOKEN_FLAG);
                       }else{                            //方法为post
                            result =  validation(joinPoint, request,
session,  PARAM_TOKEN_FLAG);                       }                  }
              }                            return result;         } catch
(Exception e) {              e.printStackTrace();              return
JsonBuilder.toJson(false, "操作失败!", "执行防止重复提交功能AOP失败,原因:" +  e.getMessage());
         }     }          public Object
generate(ProceedingJoinPoint  joinPoint, HttpServletRequest request,
HttpSession  session,String tokenFlag) throws Throwable {        String uuid =
UUID.randomUUID().toString();        request.setAttribute(PARAM_TOKEN, uuid);
        return joinPoint.proceed();    }          public Object
validation(ProceedingJoinPoint  joinPoint, HttpServletRequest request,
HttpSession  session,String tokenFlag) throws Throwable {         String
requestFlag =  request.getParameter(PARAM_TOKEN);         //redis加锁
         boolean lock =  jedisUtils.tryGetDistributedLock(tokenFlag
+  requestFlag, requestFlag, 60000);         if(lock){              //加锁成功
              //执行方法              Object funcResult = joinPoint.proceed();
              //方法执行完之后进行解锁
              jedisUtils.releaseDistributedLock(tokenFlag +  requestFlag,
requestFlag);              return funcResult;         }else{              //锁已存在
              return JsonBuilder.toJson(false, "不能重复提交!",  null);         }    }
     }
3、Controller代码@RequestMapping(value = "/index",method =  RequestMethod.GET)
@PreventRepetitionAnnotation public String
toIndex(HttpServletRequest  request,Map<String, Object> map){         return
"form";  }       @RequestMapping(value = "/add",method =  RequestMethod.POST) 
@ResponseBody  @PreventRepetitionAnnotation   public String
add(HttpServletRequest request){         try {              Thread.sleep(5000);
         } catch (InterruptedException e) {              e.printStackTrace();
         }         return JsonBuilder.toJson(true, "保存成功!",null);     }




源码路径:
https://github.com/karyzeng/examples/tree/master/demo.zzp.prevent.repetition
<https://github.com/karyzeng/examples/tree/master/demo.zzp.prevent.repetition>
(备注:此项目使用了springboot和maven,从github下载了源码之后,eclipse导入maven项目,然后运行demo.zzp.app.application.java即可,不过还需要自行去下载配置redis)



运行效果主要是为了体现防止重复提交,所以页面比较简单,效果如下


第一次点击提交表单,判断到当前的token还没有上锁,即给该token上锁。如果连续点击提交,则提示不能重复提交,当上锁的那次操作执行完,redis释放了锁之后才能继续提交。