利用Spring AOP和redis的鎖來實現防止表單重複提交
阿新 • • 發佈:2019-01-28
表單重複提交是在web中存在的一個很常見,會帶來很多麻煩的一個問題。尤其是在表單新增的時候,如果重複提交了多條一樣的資料,帶來的麻煩更大。 實現防止表單重複提交的方法有前端限制和後臺限制1、前端限制就是當點選了提交按鈕之後,就給按鈕新增屬性disabled,然後等後臺返回提交資訊之後再將disabled移除掉2、後臺實現是否重複提交的判斷前端限制按鈕的方法比較簡單,這裡就不再介紹,這裡主要介紹的是後臺實現防止重複提交,利用Spring AOP的面向切面程式設計的特點,可以實現不修改原始碼的前提下動態的新增和刪除校驗。先簡單介紹一下Spring AOP和redis百度百科的AOPAOP為Aspect Oriented Programming的縮寫,意為: 面向切面程式設計,通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。AOP是OOP的延續,是軟體開發中的一個熱點,也是Spring框架中的一個重要內容,是函數語言程式設計的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。我理解的AOP簡單的來講,AOP其實就是利用動態代理(代理的物件可以是類或者是方法)來對類和方法進行預處理,或者可以當做過濾器,對呼叫方法或者類之前進行過濾。利用AOP可以不改動業務程式碼的前提下實現對方法和類的代理。從而降低耦合度,而且AOP可以動態的新增和刪除。AOP的通知型別如下 百度百科的redisRedis是一個開源的使用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。但是這樣的做法是隻允許提交一次,萬一如果是提交之後處理失敗了,這樣就只能重新進入頁面再次進行提交。防止重複提交的意思應該是防止使用者在提交一次表單之後,在表單還沒有返回處理資訊之前再次提交的意思,而不是說只允許使用者提交一次表單。 在這裡針對了以上的做法進行了優化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<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); }(備註:此專案使用了springboot和maven,從github下載了原始碼之後,eclipse匯入maven專案,然後執行demo.zzp.app.application.java即可,不過還需要自行去下載配置redis)執行效果主要是為了體現防止重複提交,所以頁面比較簡單,效果如下第一次點選提交表單,判斷到當前的token還沒有上鎖,即給該token上鎖。如果連續點選提交,則提示不能重複提交,當上鎖的那次操作執行完,redis釋放了鎖之後才能繼續提交。