最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot使用AOP實(shí)現(xiàn)防重復(fù)提交功能

 更新時(shí)間:2024年03月08日 15:04:05   作者:AjaxZhan  
這篇文章主要為大家詳細(xì)介紹了SpringBoot如何使用AOP實(shí)現(xiàn)防重復(fù)提交功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

防重冪等的概念

防重冪等指的是我們的業(yè)務(wù)需要防止兩條相同的數(shù)據(jù)重復(fù)提交導(dǎo)致臟數(shù)據(jù)或業(yè)務(wù)錯(cuò)亂。需要注意的是,重復(fù)提交屬于小概率事件,這和并發(fā)壓測(cè)不是同一個(gè)概念。

我們的目標(biāo)是通過防重冪等的設(shè)計(jì),讓系統(tǒng)支持業(yè)務(wù)失敗或異常快速釋放限制。業(yè)務(wù)處理成功后,會(huì)在指定時(shí)間限定內(nèi)限制同一條數(shù)據(jù)的提交。本文將介紹如何在SpringBoot開發(fā)中,使用AOP+Redis實(shí)現(xiàn)一個(gè)防重冪等功能。

防重冪等設(shè)計(jì)思路

目標(biāo):防止同一個(gè)用戶在同一個(gè)業(yè)務(wù)下提交同一個(gè)數(shù)據(jù)。

策略:將用戶路徑+請(qǐng)求參數(shù)+Token生成唯一ID,存入Redis。具體流程如下:

  • 用戶從前端發(fā)送請(qǐng)求,我們通過切面攔截,拿到請(qǐng)求地址、請(qǐng)求參數(shù)和token,生成一個(gè)唯一ID。
  • 判斷Redis中是否已存在數(shù)據(jù)以及數(shù)據(jù)是否有效
  • 如果不存在:正常執(zhí)行業(yè)務(wù)。如果存在:拋出異常,提示重復(fù)提交。
  • 通過AOP攔截方法執(zhí)行結(jié)果,如果結(jié)果正常,就放行;否則就刪掉存入Redis的Key,說明本次業(yè)務(wù)異常,下次提交可以放行。

自定義注解@RepeatSubmit

首先我們定義一個(gè)注解@RepeatSubmit,作用于方法上,設(shè)置如下參數(shù),用于設(shè)置AOP切點(diǎn)。

  • interval:間隔時(shí)間
  • timeUnit:時(shí)間單位,ms
  • message:支持國(guó)際化的提示消息
@Inherited  
@Target(ElementType.METHOD)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface RepeatSubmit {  
  
    /**  
     * 間隔時(shí)間(ms),小于此時(shí)間視為重復(fù)提交  
     */  
    int interval() default 5000;  
  
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;  
  
    /**  
     * 提示消息 支持國(guó)際化 格式為 [code]  
     */  
     String message() default "{repeat.submit.message}";  
  
}

自定義切面@RepeatSubmitAspect

定義一個(gè)切面@RepeatSubmitAspect,作為防重冪等的模塊化,用于橫切標(biāo)記上@RepeatSubmit注解的方法。

我們需要定義三個(gè)通知:前置通知、后置通知、拋出異常時(shí)的通知,他們執(zhí)行的業(yè)務(wù)如下,基本上是按照上述防重冪等設(shè)計(jì)的策略來寫的。

doBefore:使用@Before("@annotation(repeatSubmit)")定義前置通知,切點(diǎn)是加了注解的方法。

  • 從注解拿到間隔時(shí)間
  • 從切點(diǎn)拿到請(qǐng)求參數(shù),從ServletRequest拿到請(qǐng)求地址和請(qǐng)求頭的用戶token。
  • 拼接SubmitKey:對(duì)token:請(qǐng)求參數(shù)做MD5加密。
  • 拼接CacheKey(存到ThreadLocal里面):將緩存常量SUBMIT_KEY,拼接URL,SubmitKey三者拼接作為Cachekey。(這意味著,如果請(qǐng)求的地址相同、參數(shù)相同、token相同,就認(rèn)為是相同提交。)
  • 判斷Redis中是否已存在Key,如果存在,拋出異常。否則將CacheKey設(shè)置到Redis。

doAfterReturning:使用@AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")定義后置通知并拿到j(luò)sonResult。

  • 拿到j(luò)sonResult,轉(zhuǎn)為R類型
  • 判斷code是否為成功,如果是就返回,如果不是就代表業(yè)務(wù)失敗,于是我們就刪掉CacheKey,因?yàn)檫@次業(yè)務(wù)并未處理成功,下一次請(qǐng)求是可以接納的。
  • 刪除ThreadLocal本地變量

doAfterThrowing

刪除key,移除ThreadLocal本地變量

PS:這里用到了ThreadLocal,ThreadLocal是一個(gè) Java 類,可以用來定義只由創(chuàng)建它們的線程訪問的變量,常用于我們需要存儲(chǔ)不在線程之間共享的數(shù)據(jù)。

@Aspect  
@Component  
public class RepeatSubmitAspect {  
  
    private static final ThreadLocal<String> KEY_CACHE = new ThreadLocal<>();  
  
    @Before("@annotation(repeatSubmit)")  
    public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {  
        // 如果注解不為0 則使用注解數(shù)值  
        long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());  
  
        if (interval < 1000) {  
            throw new ServiceException("重復(fù)提交間隔時(shí)間不能小于'1'秒");  
        }  
        HttpServletRequest request = ServletUtils.getRequest();  
        String nowParams = argsArrayToString(point.getArgs());  
  
        // 請(qǐng)求地址(作為存放cache的key值)  
        String url = request.getRequestURI();  
  
        // 唯一值(沒有消息頭則使用請(qǐng)求地址)  
        String submitKey = StringUtils.trimToEmpty(request.getHeader(SaManager.getConfig().getTokenName()));  
  
        submitKey = SecureUtil.md5(submitKey + ":" + nowParams);  
        // 唯一標(biāo)識(shí)(指定key + url + 消息頭)  
        String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY + url + submitKey;  
        if (RedisUtils.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {  
            KEY_CACHE.set(cacheRepeatKey);  
        } else {  
            String message = repeatSubmit.message();  
            if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {  
                message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));  
            }  
            throw new ServiceException(message);  
        }  
    }  
  
    /**  
     * 處理完請(qǐng)求后執(zhí)行  
     *  
     * @param joinPoint 切點(diǎn)  
     */  
    @AfterReturning(pointcut = "@annotation(repeatSubmit)", returning = "jsonResult")  
    public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {  
        if (jsonResult instanceof R) {  
            try {  
                R<?> r = (R<?>) jsonResult;  
                // 成功則不刪除redis數(shù)據(jù) 保證在有效時(shí)間內(nèi)無法重復(fù)提交  
                if (r.getCode() == R.SUCCESS) {  
                    return;  
                }  
                RedisUtils.deleteObject(KEY_CACHE.get());  
            } finally {  
                KEY_CACHE.remove();  
            }  
        }  
    }  
  
    /**  
     * 攔截異常操作  
     *  
     * @param joinPoint 切點(diǎn)  
     * @param e         異常  
     */  
    @AfterThrowing(value = "@annotation(repeatSubmit)", throwing = "e")  
    public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception e) {  
        RedisUtils.deleteObject(KEY_CACHE.get());  
        KEY_CACHE.remove();  
    }  
  
    /**  
     * 參數(shù)拼裝  
     */  
    private String argsArrayToString(Object[] paramsArray) {  
        StringJoiner params = new StringJoiner(" ");  
        if (ArrayUtil.isEmpty(paramsArray)) {  
            return params.toString();  
        }  
        for (Object o : paramsArray) {  
            if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {  
                params.add(JsonUtils.toJsonString(o));  
            }  
        }  
        return params.toString();  
    }  
  
    /**  
     * 判斷是否需要過濾的對(duì)象。  
     *  
     * @param o 對(duì)象信息。  
     * @return 如果是需要過濾的對(duì)象,則返回true;否則返回false。  
     */  
    @SuppressWarnings("rawtypes")  
    public boolean isFilterObject(final Object o) {  
        Class<?> clazz = o.getClass();  
        if (clazz.isArray()) {  
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);  
        } else if (Collection.class.isAssignableFrom(clazz)) {  
            Collection collection = (Collection) o;  
            for (Object value : collection) {  
                return value instanceof MultipartFile;  
            }  
        } else if (Map.class.isAssignableFrom(clazz)) {  
            Map map = (Map) o;  
            for (Object value : map.values()) {  
                return value instanceof MultipartFile;  
            }  
        }  
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse  
            || o instanceof BindingResult;  
    }  
  
}

簡(jiǎn)單測(cè)試

我們創(chuàng)建一個(gè)接口用于測(cè)試防重冪等,我的系統(tǒng)中使用Sa-Token權(quán)限框架,為了方便,我們通過@SaIngore放行接口。

/**  
 * @author AjaxZhan  
 */@RestController  
@RequestMapping("/repeat")  
@Slf4j  
@SaIgnore  
public class RepeatController {  
  
    @PostMapping  
    @RepeatSubmit(interval = 2000)  
    public R<Void> repeat1(String info){  
        log.info("請(qǐng)求成功,信息" + info);  
        return R.ok("請(qǐng)求成功");  
    }  
}

使用Apifox測(cè)試結(jié)果如下:

當(dāng)我們?cè)?s內(nèi)連續(xù)提交就會(huì)觸發(fā)異常:

至此,我們就成功地使用AOP+Redis的方式設(shè)計(jì)了一個(gè)防重冪等功能。

到此這篇關(guān)于SpringBoot使用AOP實(shí)現(xiàn)防重復(fù)提交功能的文章就介紹到這了,更多相關(guān)SpringBoot AOP防重復(fù)提交內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Spring Boot 整合 Mybatis Annotation 注解的完整 Web 案例

    Spring Boot 整合 Mybatis Annotation 注解的完整 Web 案例

    這篇文章主要介紹了Spring Boot 整合 Mybatis Annotation 注解的完整 Web 案例,需要的朋友可以參考下
    2017-05-05
  • Java中LocalDate的詳細(xì)方法舉例總結(jié)

    Java中LocalDate的詳細(xì)方法舉例總結(jié)

    這篇文章主要給大家介紹了關(guān)于Java中LocalDate詳細(xì)方法舉例的相關(guān)資料,LocalDate主要是用來處理日期的類,文中通過代碼示例介紹的非常詳細(xì),需要的朋友可以參考下
    2023-09-09
  • 使用Spring Cache和Redis實(shí)現(xiàn)查詢數(shù)據(jù)緩存

    使用Spring Cache和Redis實(shí)現(xiàn)查詢數(shù)據(jù)緩存

    在現(xiàn)代應(yīng)用程序中,查詢緩存的使用已經(jīng)變得越來越普遍,它不僅能夠顯著提高系統(tǒng)的性能,還能提升用戶體驗(yàn),在這篇文章中,我們將探討緩存的基本概念、重要性以及如何使用Spring Cache和Redis實(shí)現(xiàn)查詢數(shù)據(jù)緩存,需要的朋友可以參考下
    2024-07-07
  • Java導(dǎo)入導(dǎo)出csv格式文件完整版詳解(附代碼)

    Java導(dǎo)入導(dǎo)出csv格式文件完整版詳解(附代碼)

    在Java中你可以使用不同的庫來導(dǎo)出CSV格式的文件,這篇文章主要給大家介紹了關(guān)于Java導(dǎo)入導(dǎo)出csv格式文件的相關(guān)資料,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2024-07-07
  • 在實(shí)踐中了解Java反射機(jī)制應(yīng)用

    在實(shí)踐中了解Java反射機(jī)制應(yīng)用

    當(dāng)程序運(yùn)行時(shí),允許改變程序結(jié)構(gòu)或變量類型,這種語言稱為動(dòng)態(tài)語言。我們認(rèn)為java并不是動(dòng)態(tài)語言,但是它卻有一個(gè)非常突出的動(dòng)態(tài)相關(guān)機(jī)制,俗稱:反射。下面我們來簡(jiǎn)單學(xué)習(xí)一下吧
    2019-05-05
  • Spring中字段格式化的使用小結(jié)

    Spring中字段格式化的使用小結(jié)

    Spring提供的一個(gè)core.convert包?是一個(gè)通用類型轉(zhuǎn)換系統(tǒng)。它提供了統(tǒng)一的?ConversionService??API和強(qiáng)類型的Converter SPI,用于實(shí)現(xiàn)從一種類型到另一種類型的轉(zhuǎn)換邏輯,這篇文章主要介紹了Spring中字段格式化的使用詳解,需要的朋友可以參考下
    2022-06-06
  • Java如何通過屬性名獲取Object對(duì)象屬性值

    Java如何通過屬性名獲取Object對(duì)象屬性值

    這篇文章主要介紹了Java如何通過屬性名獲取Object對(duì)象屬性值問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • spring mvc中的@PathVariable動(dòng)態(tài)參數(shù)詳解

    spring mvc中的@PathVariable動(dòng)態(tài)參數(shù)詳解

    這篇文章主要介紹了spring mvc中的@PathVariable動(dòng)態(tài)參數(shù)詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2021-11-11
  • 一篇文章帶你了解Java泛型的super和extends

    一篇文章帶你了解Java泛型的super和extends

    這篇文章主要介紹了Java泛型extends及super區(qū)別實(shí)例解析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2021-08-08
  • SpringSecurity+jwt+captcha登錄認(rèn)證授權(quán)流程總結(jié)

    SpringSecurity+jwt+captcha登錄認(rèn)證授權(quán)流程總結(jié)

    本文介紹了SpringSecurity、JWT和驗(yàn)證碼在Spring Boot 3.2.0中的應(yīng)用,包括登錄認(rèn)證和授權(quán)流程的詳細(xì)步驟,本文給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧
    2024-11-11

最新評(píng)論

刚察县| 紫阳县| 山丹县| 龙门县| 黎平县| 黎平县| 保康县| 滦南县| 阿城市| 志丹县| 五家渠市| 金阳县| 勃利县| 察雅县| 澄江县| 新营市| 太仆寺旗| 紫金县| 忻城县| 钟山县| 柘城县| 南召县| 天台县| 铜陵市| 沭阳县| 望奎县| 岢岚县| 井研县| 湛江市| 宁武县| 普兰县| 全州县| 涿鹿县| 榆林市| 孝义市| 高州市| 张北县| 桃园市| 上蔡县| 剑阁县| 库伦旗|