1. 程式人生 > >springboot實現防重複提交和防重複點選

springboot實現防重複提交和防重複點選

## 背景 同一條資料被使用者點選了多次,導致資料冗餘,需要防止弱網路等環境下的重複點選 ## 目標 通過在指定的介面處添加註解,實現根據指定的介面引數來防重複點選 ## 說明 這裡的重複點選是指在指定的時間段內多次點選按鈕 ## 技術方案 springboot + redis鎖 + 註解 使用 feign client 進行請求測試 ## 最終的使用例項 1、根據介面收到 PathVariable 引數判斷唯一 ``` /** * 根據請求引數裡的 PathVariable 裡獲取的變數進行介面級別防重複點選 * * @param testId 測試id * @param requestVo 請求引數 * @return * @author daleyzou */ @PostMapping("/test/{testId}") @NoRepeatSubmit(location = "thisIsTestLocation", seconds = 6) public RsVo thisIsTestLocation(@PathVariable Integer testId, @RequestBody RequestVo requestVo) throws Throwable { // 睡眠 5 秒,模擬業務邏輯 Thread.sleep(5); return RsVo.success("test is return success"); } ``` 2、根據介面收到的 RequestBody 中指定變數名的值判斷唯一 ``` /** * 根據請求引數裡的 RequestBody 裡獲取指定名稱的變數param5的值進行介面級別防重複點選 * * @param testId 測試id * @param requestVo 請求引數 * @return * @author daleyzou */ @PostMapping("/test/{testId}") @NoRepeatSubmit(location = "thisIsTestBody", seconds = 6, argIndex = 1, name = "param5") public RsVo thisIsTestBody(@PathVariable Integer testId, @RequestBody RequestVo requestVo) throws Throwable { // 睡眠 5 秒,模擬業務邏輯 Thread.sleep(5); return RsVo.success("test is return success"); } ``` ps: jedis 2.9 和 springboot有各種相容問題,無奈只有降低springboot的版本了 ## 執行結果 ``` 收到響應:{"succeeded":true,"code":500,"msg":"操作過於頻繁,請稍後重試","data":null} 收到響應:{"succeeded":true,"code":500,"msg":"操作過於頻繁,請稍後重試","data":null} 收到響應:{"succeeded":true,"code":500,"msg":"操作過於頻繁,請稍後重試","data":null} 收到響應:{"succeeded":true,"code":200,"msg":"success","data":"test is return success"} ``` ## 測試用例 ``` package com.dalelyzou.preventrepeatsubmit.controller; import com.dalelyzou.preventrepeatsubmit.PreventrepeatsubmitApplicationTests; import com.dalelyzou.preventrepeatsubmit.service.AsyncFeginService; import com.dalelyzou.preventrepeatsubmit.vo.RequestVo; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import java.io.IOException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * TestControllerTest * @description 防重複點選測試類 * @author daleyzou * @date 2020年09月28日 17:13 * @version 1.3.1 */ class TestControllerTest extends PreventrepeatsubmitApplicationTests { @Autowired AsyncFeginService asyncFeginService; @Test public void thisIsTestLocation() throws IOException { RequestVo requestVo = new RequestVo(); requestVo.setParam5("random"); ExecutorService executorService = Executors.newFixedThreadPool(4); for (int i = 0; i <= 3; i++) { executorService.execute(() -> { String kl = asyncFeginService.thisIsTestLocation(requestVo); System.err.println("收到響應:" + kl); }); } System.in.read(); } @Test public void thisIsTestBody() throws IOException { RequestVo requestVo = new RequestVo(); requestVo.setParam5("special"); ExecutorService executorService = Executors.newFixedThreadPool(4); for (int i = 0; i <= 3; i++) { executorService.execute(() -> { String kl = asyncFeginService.thisIsTestBody(requestVo); System.err.println("收到響應:" + kl); }); } System.in.read(); } } ``` ## 定義一個註解 ``` package com.dalelyzou.preventrepeatsubmit.aspect; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * NoRepeatSubmit * @description 重複點選的切面 * @author daleyzou * @date 2020年09月23日 14:35 * @version 1.4.8 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoRepeatSubmit { /** * 鎖過期的時間 * */ int seconds() default 5; /** * 鎖的位置 * */ String location() default "NoRepeatSubmit"; /** * 要掃描的引數位置 * */ int argIndex() default 0; /** * 引數名稱 * */ String name() default ""; } ``` ## 根據指定的註解定義一個切面,根據引數中的指定值來判斷請求是否重複 ``` package com.dalelyzou.preventrepeatsubmit.aspect; import com.dalelyzou.preventrepeatsubmit.constant.RedisKey; import com.dalelyzou.preventrepeatsubmit.service.LockService; import com.dalelyzou.preventrepeatsubmit.vo.RsVo; import com.google.common.collect.Maps; import com.google.gson.Gson; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.lang.reflect.Field; import java.util.Map; @Aspect @Component public class NoRepeatSubmitAspect { private static final Logger logger = LoggerFactory.getLogger(NoRepeatSubmitAspect.class); private static Gson gson = new Gson(); private static final String SUFFIX = "SUFFIX"; @Autowired LockService lockService; /** * 橫切點 */ @Pointcut("@annotation(noRepeatSubmit)") public void repeatPoint(NoRepeatSubmit noRepeatSubmit) { } /** * 接收請求,並記錄資料 */ @Around(value = "repeatPoint(noRepeatSubmit)") public Object doBefore(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) { String key = RedisKey.NO_REPEAT_LOCK_PREFIX + noRepeatSubmit.location(); Object[] args = joinPoint.getArgs(); String name = noRepeatSubmit.name(); int argIndex = noRepeatSubmit.argIndex(); String suffix; if (StringUtils.isEmpty(name)) { suffix = String.valueOf(args[argIndex]); } else { Map keyAndValue = getKeyAndValue(args[argIndex]); Object valueObj = keyAndValue.get(name); if (valueObj == null) { suffix = SUFFIX; } else { suffix = String.valueOf(valueObj); } } key = key + ":" + suffix; logger.info("=================================================="); for (Object arg : args) { logger.info(gson.toJson(arg)); } logger.info("=================================================="); int seconds = noRepeatSubmit.seconds(); logger.info("lock key : " + key); if (!lockService.isLock(key, seconds)) { return RsVo.fail("操作過於頻繁,請稍後重試"); } try { Object proceed = joinPoint.proceed(); return proceed; } catch (Throwable throwable) { logger.error("執行業務程式碼出錯", throwable); throw new RuntimeException(throwable.getMessage()); } finally { lockService.unLock(key); } } public static Map getKeyAndValue(Object obj) { Map map = Maps.newHashMap(); // 得到類物件 Class userCla = (Class) obj.getClass(); /* 得到類中的所有屬性集合 */ Field[] fs = userCla.getDeclaredFields(); for (int i = 0; i < fs.length; i++) { Field f = fs[i]; // 設定些屬性是可以訪問的 f.setAccessible(true); Object val = new Object(); try { val = f.get(obj); // 得到此屬性的值 // 設定鍵值 map.put(f.getName(), val); } catch (IllegalArgumentException e) { logger.error("getKeyAndValue IllegalArgumentException", e); } catch (IllegalAccessException e) { logger.error("getKeyAndValue IllegalAccessException", e); } } logger.info("掃描結果:" + gson.toJson(map)); return map; } } ``` ## 專案完整程式碼 https://github.com/daleyzou/PreventRepe