在平時開發中,如果網速比較慢的情況下,用戶提交表單后,發現服務器半天都沒有響應,那么用戶可能會以為是自己沒有提交表單,就會再點擊提交按鈕重復提交表單,我們在開發中必須防止表單重復提交….
下面我們利用自定義注解
、Spring Aop
、Guava Cache
實現表單防重復提交
一、導入依賴
創建springboot項目,在pom.xml文件中加入以下內容
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>21.0</version> </dependency> </dependencies>
二、Lock注解
創建一個 LocalLock
注解,就一個 key
可以了
1 package com.carry.annotation; 2 3 import java.lang.annotation.Documented; 4 import java.lang.annotation.ElementType; 5 import java.lang.annotation.Inherited; 6 import java.lang.annotation.Retention; 7 import java.lang.annotation.RetentionPolicy; 8 import java.lang.annotation.Target; 9 10 /** 11 * 鎖的注解 12 * 13 */ 14 @Target(ElementType.METHOD) 15 @Retention(RetentionPolicy.RUNTIME) 16 @Documented 17 @Inherited 18 public @interface LocalLock { 19 20 String key() default ""; 21 }
三、Lock攔截器(AOP)
首先通過 CacheBuilder.newBuilder()
構建出緩存對象,設置好過期時間;其目的就是為了防止因程序崩潰鎖得不到釋放,然后在具體的 interceptor()
方法上采用的是 Around(環繞增強)
,所有帶 LocalLock
注解的都將被切面處理
具體代碼
1 package com.carry.interceptor; 2 3 import java.lang.reflect.Method; 4 import java.util.concurrent.TimeUnit; 5 import org.aspectj.lang.ProceedingJoinPoint; 6 import org.aspectj.lang.annotation.Around; 7 import org.aspectj.lang.annotation.Aspect; 8 import org.aspectj.lang.reflect.MethodSignature; 9 import org.springframework.context.annotation.Configuration; 10 import org.springframework.util.StringUtils; 11 import com.carry.annotation.LocalLock; 12 import com.google.common.cache.Cache; 13 import com.google.common.cache.CacheBuilder; 14 15 @Aspect 16 @Configuration 17 public class LockMethodInterceptor { 18 19 private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder() 20 // 最大緩存 100 個 21 .maximumSize(100) 22 // 設置寫緩存后 5 秒鍾過期 23 .expireAfterWrite(5, TimeUnit.SECONDS).build(); 24 25 @Around("execution(public * *(..)) && @annotation(com.carry.annotation.LocalLock)") 26 public Object interceptor(ProceedingJoinPoint pjp) { 27 MethodSignature signature = (MethodSignature) pjp.getSignature(); 28 Method method = signature.getMethod(); 29 LocalLock localLock = method.getAnnotation(LocalLock.class); 30 String key = getKey(localLock.key(), pjp.getArgs()); 31 if (!StringUtils.isEmpty(key)) { 32 if (CACHES.getIfPresent(key) != null) { 33 throw new RuntimeException("請勿重復請求"); 34 } 35 // 如果是第一次請求,就將 key 當前對象壓入緩存中 36 CACHES.put(key, key); 37 } 38 try { 39 return pjp.proceed(); 40 } catch (Throwable throwable) { 41 throw new RuntimeException("服務器異常"); 42 } finally { 43 // TODO 44 } 45 } 46 47 /** 48 * key 的生成策略,如果想靈活可以寫成接口與實現類的方式 49 * 50 * @param keyExpress 51 * 表達式 52 * @param args 53 * 參數 54 * @return 生成的key 55 */ 56 private String getKey(String keyExpress, Object[] args) { 57 for (int i = 0; i < args.length; i++) { 58 keyExpress = keyExpress.replace("arg[" + i + "]", args[i].toString()); 59 } 60 return keyExpress; 61 } 62 }
四、控制層
在接口方法上添加 @LocalLock(key = "book:arg[0]")
;意味着會將 arg[0]
替換成第一個參數的值,生成后的新 key 將被緩存起來
具體代碼
1 package com.carry.controller; 2 3 import org.springframework.web.bind.annotation.GetMapping; 4 import org.springframework.web.bind.annotation.RequestMapping; 5 import org.springframework.web.bind.annotation.RequestParam; 6 import org.springframework.web.bind.annotation.RestController; 7 8 import com.carry.annotation.LocalLock; 9 10 @RestController 11 @RequestMapping("/test") 12 public class LocalLockController { 13 14 @LocalLock(key = "key:arg[0]") 15 @GetMapping 16 public String query(@RequestParam String token) { 17 return "success - " + token; 18 } 19 }
五、測試
啟動項目,在postman中輸入url:localhost:8080/test?token=1
第一次請求結果:
第二次請求結果: