防止重複提交解決方案-(基於JAVA註解+AOP切面)

1、前言

  近期在構建項目腳手架時,關於接口冪等性問題,考慮做成獨立模塊工具放進腳手架中進行通用。
  如何保證接口冪等性,換句話說就是如何防止接口重複提交。通常,前後端都需要考慮如何實現相關控制。

  • 前端常用的解決方案是“表單提交完成,按鈕置灰、按鈕不可用或者關閉相關頁面”。
  • 常見的後端解決方案有“基於JAVA註解+AOP切面實現防止重複提交“。

 

2、方案

  基於JAVA註解+AOP切面方式實現防止重複提交,一般需要自定義JAVA註解,採用AOP切面解析註解,實現接口首次請求提交時,將接口請求標記(由接口簽名、請求token、請求客戶端ip等組成)存儲至redis,並設置超時時間T(T時間之後redis清除接口請求標記),接口每次請求都先檢查redis中接口標記,若存在接口請求標記,則判定爲接口重複提交,進行攔截返回處理。

 

3、實現

     本次採用的基礎框架爲SpringBoot,涉及的組件模塊有AOP、WEB、Redis、Lombok、Fastjson。詳細代碼與配置如下文。

  • pom依賴

複製代碼
<properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.28</version>
        </dependency>

    </dependencies>
複製代碼

 

  • 配置文件

複製代碼
server.port=8888

# Redis數據庫索引(默認爲0)
spring.redis.database=0
# Redis服務器地址
spring.redis.host=127.0.0.1
# Redis服務器連接端口
spring.redis.port=6379
# Redis服務器連接密碼(默認爲空)
spring.redis.password=
# 連接池最大連接數(使用負值表示沒有限制)
spring.redis.pool.max-active=8
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
spring.redis.pool.max-wait=-1
# 連接池中的最大空閒連接
spring.redis.pool.max-idle=8
# 連接池中的最小空閒連接
spring.redis.pool.min-idle=0
# 連接超時時間(毫秒)
spring.redis.timeout=5000
複製代碼

 

  • 自定義註解

複製代碼
/**
 * @author :Gavin
 * @see :防止重複操作註解
 */

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PreventDuplication {
    /**
     * 防重複操作限時標記數值(存儲redis限時標記數值)
     */
    String value() default "value" ;

    /**
     * 防重複操作過期時間(藉助redis實現限時控制)
     */
    long expireSeconds() default 10;
}
複製代碼

 

  • 自定義切面(解析註解)

    切面用於處理防重複提交註解,通過redis中接口請求限時標記控制接口的提交請求。

複製代碼
/**
 * @author :Gavin
 * @see :防止重複操作切面(處理切面註解)
 */

@Aspect
@Component
public class PreventDuplicationAspect {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 定義切點
     */
    @Pointcut("@annotation(com.example.idempotent.idempotent.annotation.PreventDuplication)")
    public void preventDuplication() {
    }

    /**
     * 環繞通知 (可以控制目標方法前中後期執行操作,目標方法執行前後分別執行一些代碼)
     *
     * @param joinPoint
     * @return
     */
    @Around("preventDuplication()")
    public Object before(ProceedingJoinPoint joinPoint) throws Exception {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Assert.notNull(request, "request cannot be null.");

        //獲取執行方法
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        //獲取防重複提交註解
        PreventDuplication annotation = method.getAnnotation(PreventDuplication.class);

        // 獲取token以及方法標記,生成redisKey和redisValue
        String token = request.getHeader(IdempotentConstant.TOKEN);
        String redisKey = IdempotentConstant.PREVENT_DUPLICATION_PREFIX
                .concat(token)
                .concat(getMethodSign(method, joinPoint.getArgs()));
        String redisValue = redisKey.concat(annotation.value()).concat("submit duplication");

        if (!redisTemplate.hasKey(redisKey)) {
            //設置防重複操作限時標記(前置通知)
            redisTemplate.opsForValue()
                    .set(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS);
            try {
                //正常執行方法並返回
                //ProceedingJoinPoint類型參數可以決定是否執行目標方法,且環繞通知必須要有返回值,返回值即爲目標方法的返回值
                return joinPoint.proceed();
            } catch (Throwable throwable) {
                //確保方法執行異常實時釋放限時標記(異常後置通知)
                redisTemplate.delete(redisKey);
                throw new RuntimeException(throwable);
            }
        } else {
            throw new RuntimeException("請勿重複提交");
        }
    }

    /**
     * 生成方法標記:採用數字簽名算法SHA1對方法簽名字符串加簽
     *
     * @param method
     * @param args
     * @return
     */
    private String getMethodSign(Method method, Object... args) {
        StringBuilder sb = new StringBuilder(method.toString());
        for (Object arg : args) {
            sb.append(toString(arg));
        }
        return DigestUtils.sha1DigestAsHex(sb.toString());
    }

    private String toString(Object arg) {
        if (Objects.isNull(arg)) {
            return "null";
        }
        if (arg instanceof Number) {
            return arg.toString();
        }
        return JSONObject.toJSONString(arg);
    }
}
複製代碼
複製代碼
public interface IdempotentConstant {

    String TOKEN = "token";

    String PREVENT_DUPLICATION_PREFIX = "PREVENT_DUPLICATION_PREFIX:";
}
複製代碼

 

  • controller實現(使用註解)

複製代碼
@Slf4j
@RestController
@RequestMapping("/web")
public class IdempotentController {

    @PostMapping("/sayNoDuplication")
    @PreventDuplication(expireSeconds = 8)
    public String sayNoDuplication(@RequestParam("requestNum") String requestNum) {
        log.info("sayNoDuplicatin requestNum:{}", requestNum);
        return "sayNoDuplicatin".concat(requestNum);
    }

}
複製代碼

 

4、測試

  • 正常請求(首次)

     首次請求,接口正常返回處理結果。

 

  •  限定時間內重複請求(上文設置8s)

   在限定時間內重複請求,AOP切面攔截處理拋出異常,終止接口處理邏輯,異常返回。

 控制檯報錯:

 

 

5、源代碼

  本文代碼已經上傳託管至GitHub以及Gitee,有需要的讀者請自行下載。

  • GitHub:https://github.com/gavincoder/idempotent.git
  • Gitee:https://gitee.com/gavincoderspace/idempotent.git

 

Java後端接口防止重複提交

最近在開發的過程中遇到前端沒有對提交按鈕做點擊後變灰處理,必須在後端添加防止重複提交的校驗。網上有很多中方案,我這邊採用的是aop+自定義註解方式實現。
  剛開始採用利用自定義註解+aop+redis防止重複提交這篇博客的邏輯去實現,但是後來在測試多線程訪問的時候會出現問題,然後參考網上Redis分佈式鎖的邏輯,多線程情況下測試只有一個可以通過。參考了LockManager中關於加鎖的邏輯。具體的代碼邏輯就不佔了,只是在上面介紹的資料基礎上做了稍微的改造。

參考資料
https://blog.csdn.net/weixin_37505014/article/details/103461741
https://gitee.com/billion/redisLock/

自定義註解解決API接口冪等設計防止表單重複提交(生成token存放到redis中)

寫在後面

  本文重點在於講解如何採用基於JAVA註解+AOP切面快速實現防重複提交功能,該方案實現可以完全勝任非高併發場景下實施應用。但是在高併發場景下仍然有不足之處,存在線程安全問題(可以採用Jemeter復現問題)。那麼,如何實現支持高併發場景防重複提交功能?請讀者查看我的博文《基於Redis實現分佈式鎖》,這篇博客對本文基於JAVA註解+AOP切面實現進行了優化改造,以便應用於高併發場景。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章