表单重复提交是在web中存在的一个很常见,会带来很多麻烦的一个问题。尤其是在表单新增的时候,如果重复提交了多条一样的数据,带来的麻烦更大。
实现防止表单重复提交的方法有前端限制和后台限制
1、前端限制就是当点击了提交按钮之后,就给按钮添加属性disabled,然后等后台返回提交信息之后再将disabled移除掉
2、后台实现是否重复提交的判断
前端限制按钮的方法比较简单,这里就不再介绍,这里主要介绍的是后台实现防止重复提交,利用Spring AOP的面向切面编程的特点,可以实现不修改原代码的前提下动态的添加和删除校验。
先简单介绍一下Spring AOP和redis
百度百科的AOP
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
我理解的AOP
简单的来讲,AOP其实就是利用动态代理(代理的对象可以是类或者是方法)来对类和方法进行预处理,或者可以当做过滤器,对调用方法或者类之前进行过滤。利用AOP可以不改动业务代码的前提下实现对方法和类的代理。从而降低耦合度,而且AOP可以动态的添加和删除。
AOP的通知类型如下

百度百科的redis
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。redis是一个key-value存储系统。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持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
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
*/
@Aspect
@Component
@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 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
(备注:此项目使用了springboot和maven,从github下载了源码之后,eclipse导入maven项目,然后运行demo.zzp.app.application.java即可,不过还需要自行去下载配置redis)
运行效果
主要是为了体现防止重复提交,所以页面比较简单,效果如下
第一次点击提交表单,判断到当前的token还没有上锁,即给该token上锁。如果连续点击提交,则提示不能重复提交,当上锁的那次操作执行完,redis释放了锁之后才能继续提交。