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

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM