日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網(wǎng)為廣大站長提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請做好本站友鏈:【 網(wǎng)站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

正如本文標(biāo)題所言,今天我們來聊一聊在JAVA應(yīng)用系統(tǒng)中如何防止接口重復(fù)提交;簡單地講,這其實(shí)就是“重復(fù)提交”的話題,本文將從以下幾個(gè)部分展開介紹:

1.“重復(fù)提交”簡介與造成的后果

2.“防止接口重復(fù)提交”的實(shí)現(xiàn)思路

3.“防止接口重復(fù)提交”的代碼實(shí)戰(zhàn)

1、“重復(fù)提交”簡介與造成的后果

對于“重復(fù)提交”,想必各位小伙伴都知曉它的意思,簡單的理解,它指的是前端用戶在間隔很短的時(shí)間周期內(nèi)對同一個(gè)請求URL發(fā)起請求,導(dǎo)致前端開發(fā)者在很短的時(shí)間周期內(nèi)將同一份數(shù)據(jù)(請求體)提交到后端相同的接口 多次,最終數(shù)據(jù)庫出現(xiàn)多條主鍵ID不一樣而其他業(yè)務(wù)數(shù)據(jù)幾乎一毛一樣的記錄;

仔細(xì)研究上述整個(gè)過程,會(huì)發(fā)現(xiàn)如果發(fā)起的多次請求的時(shí)間間隔足夠短,即時(shí)間趨向于無窮小 時(shí),其過程可以歸為“多線程并發(fā)導(dǎo)致并發(fā)安全”的問題范疇;而對于“并發(fā)安全”的話題,debug早在此前自己錄制的課程以及之前的文章中介紹過多次了,在此不再贅述;

上述在對“重復(fù)提交”的介紹中隱約也提及它所帶來的的后果:

(1)數(shù)據(jù)庫DB出現(xiàn)多條一毛一樣的數(shù)據(jù)記錄;

(2)如果重復(fù)發(fā)起的請求足夠多、請求體容量足夠大,很可能會(huì)給系統(tǒng)接口帶來極大的壓力,導(dǎo)致其出現(xiàn)“接口不穩(wěn)定”、“DB負(fù)載過高”,嚴(yán)重點(diǎn)甚至可能會(huì)出現(xiàn)“系統(tǒng)宕機(jī)”的情況;

因此,我們需要在一些很可能會(huì)出現(xiàn)“重復(fù)提交”的后端接口中加入一些處理機(jī)制(附注:前端其實(shí)也需要配合一同處理的,其處理方式在本文就不做介紹了~);

2、“防止接口重復(fù)提交”的實(shí)現(xiàn)思路

值得一提的是,絕大部分情況下,只有POST/PUT/DELETE的請求方式才會(huì)出現(xiàn)“重復(fù)提交”的情況,而對于GET請求方式,只要不是出現(xiàn)人為的意外情況,那么它就具有“冪等性”,談不上“重復(fù)提交”現(xiàn)象的出現(xiàn),因此,在實(shí)際項(xiàng)目中,出現(xiàn)“重復(fù)提交”現(xiàn)象比較多的一般是POST請求方式;

而在實(shí)際項(xiàng)目開發(fā)中,“防止接口重復(fù)提交”的實(shí)現(xiàn)方式有兩類,一類是純粹的針對請求鏈接URL的,即防止對同一個(gè)URL發(fā)起多次請求:此種方式明顯粒度過大,容易誤傷友軍;另一類是針對請求鏈接URL + 請求體 的,這種方式可以說是比較人性化而且也是比較合理的,而我們在后面要介紹的實(shí)現(xiàn)方式正是基于此進(jìn)行實(shí)戰(zhàn)的;

為了便于小伙伴理解,接下來我們以“用戶在前端提交注冊信息”為例,介紹“如何防止接口重復(fù)提交”的實(shí)現(xiàn)思路,如下圖所示為整體的實(shí)現(xiàn)思路:

干貨實(shí)戰(zhàn)~Java如何防止接口重復(fù)提交

 

從該圖中可以得知,如果當(dāng)前提交的請求URL已經(jīng)存在于緩存中,且 當(dāng)前提交的請求體 跟 緩存中該URL對應(yīng)的請求體一毛一樣 且 當(dāng)前請求URL的時(shí)間戳跟上次相同請求URL的時(shí)間戳 間隔在8s 內(nèi),即代表當(dāng)前請求屬于 “重復(fù)提交”;如果這其中有一個(gè)條件不成立,則意味著當(dāng)前請求很有可能是第一次請求,或者已經(jīng)過了8s時(shí)間間隔的 第N次請求了,不屬于“重復(fù)提交”了。

3、“防止接口重復(fù)提交”的代碼實(shí)戰(zhàn)

照著這個(gè)思路,接下來我們將采用實(shí)際的代碼進(jìn)行實(shí)戰(zhàn),其中涉及到的技術(shù):Spring Boot2.0 + 自定義注解 + 攔截器 + 本地緩存(也可以分布式緩存);

(1)首先,需要自定義一個(gè)用于加在需要“防止重復(fù)提交”的請求方法上 的注解RepeatSubmit,該注解的定義代碼很簡單,就是一個(gè)常規(guī)的注解定義,如下代碼所示:


@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
}

之后,是直接創(chuàng)建一個(gè)新的控制器SubmitController,并在其中創(chuàng)建一請求方法,用于處理前端用戶提交的注冊信息 請求,如下代碼所示:

@RestController
@RequestMApping("submit")
public class SubmitController extends BaseController{
    //用戶注冊
    @RepeatSubmit
    @PostMapping("register")
    public BaseResponse register(@RequestBody RegisterDto dto) throws Exception{
        BaseResponse response=new BaseResponse(StatusCode.Success);
        //log.info("用戶注冊,提交上來的請求信息為:{}",dto);

        //將用戶信息插入到db
        response.setData(dto);
        return response;
    }
}

其中,RegisterDto 為自定義的實(shí)體類,代碼定義如下所示:

@Data
public class RegisterDto implements Serializable{
    private String userName;
    private String nickName;
    private Integer age;
}

(2)將注解加上去之后,接下來需要自定義一個(gè)攔截器RepeatSubmitInterceptor,用于攔截并獲取 加了上述這個(gè)注解的所有請求方法的相關(guān)信息,包括其請求URL和請求體數(shù)據(jù),其核心代碼如下所示:

@Component
public abstract class RepeatSubmitInterceptor extends HandlerInterceptorAdapter{
    //開始攔截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (handler instanceof HandlerMethod){
            HandlerMethod handlerMethod= (HandlerMethod) handler;
            Method method=handlerMethod.getMethod();
            RepeatSubmit submitAnnotation=method.getAnnotation(RepeatSubmit.class);
            if (submitAnnotation!=null){
                //如果是重復(fù)提交,則進(jìn)行攔截,拒絕請求
                if (this.isRepeatSubmit(request)){
                    BaseResponse subResponse=new BaseResponse(StatusCode.CanNotRepeatSubmit);
                    CommonUtil.renderString(response,new Gson().toJson(subResponse));
                    return false;
                }
            }
            return true;
        }else{
            return super.preHandle(request, response, handler);
        }
    }
    //自定義方法邏輯-判定是否重復(fù)提交
    public abstract boolean isRepeatSubmit(HttpServletRequest request);
}

在這里我們將其定義為抽象類,并自定義一個(gè)抽象方法:“判斷當(dāng)前請求是否為重復(fù)提交isRepeatSubmit()”,之所以這樣做,是因?yàn)?ldquo;判斷是否重復(fù)提交”可以有多種實(shí)現(xiàn)方式,而每種實(shí)現(xiàn)方式可以通過繼承該抽象類 并 實(shí)現(xiàn)該抽象方法 從而將其區(qū)分開來,某種程度降低了耦合性(面向接口/抽象類編程);如下代碼所示為該抽象類的其中一種實(shí)現(xiàn)方式:


/**
 * 判斷是否重復(fù)提交,整體的思路:
 * 獲取當(dāng)前請求的URL作為鍵Key,暫且標(biāo)記為:A1,其取值為映射Map(Map里面的元素由:請求的鏈接url 和 請求體的數(shù)據(jù)組成) 暫且標(biāo)記為V1;
 * 從緩存中(本地緩存或者分布式緩存)查找Key=A1的值V2,如果V2和V1的值一樣,即代表當(dāng)前請求是重復(fù)提交的,拒絕執(zhí)行后續(xù)的請求,否則可以繼續(xù)往后面執(zhí)行
 * 其中,設(shè)定重復(fù)提交的請求的間隔有效時(shí)間為8秒
 *
 * 注意點(diǎn):如果在有效時(shí)間內(nèi),如8秒內(nèi),一直發(fā)起同個(gè)請求url、同個(gè)請求體,那么重復(fù)提交的有效時(shí)間將會(huì)自動(dòng)延長
 * @author 修羅debug
 * @date 2020/10/21 8:12
 * @link 微信:debug0868  QQ:1948831260
 * @blog fightjava.com
 */
@Component
public class SameUrlDataRepeatInterceptor extends RepeatSubmitInterceptor{
    private static final String REPEAT_PARAMS = "RepeatParams";
    private static final String REPEAT_TIME = "RepeatTime";
    
//防重提交key
    public static final String REPEAT_SUBMIT_KEY = "Repeat_Submit:";
    private static final int IntervalTime = 8;

    //構(gòu)建本地緩存,有效時(shí)間為8秒鐘
    private final Cache<String,String> cache= CacheBuilder.newBuilder().expireAfterWrite(IntervalTime, TimeUnit.SECONDS).build();

    //真正實(shí)現(xiàn)“是否重復(fù)提交的邏輯”
    @Override
    public boolean isRepeatSubmit(HttpServletRequest request) {
        String currParams=HttpHelper.getBodyString(request);

        if (StringUtils.isBlank(currParams)){
            currParams=new Gson().toJson(request.getParameterMap());
        }
        //獲取請求地址,充當(dāng)A1
        String url=request.getRequestURI();
        //充當(dāng)B1
        RepeatSubmitCacheDto currCacheData=new RepeatSubmitCacheDto(currParams,System.currentTimeMillis(),url);
        //充當(dāng)鍵A1
        String cacheRepeatKey=REPEAT_SUBMIT_KEY+url;

        String cacheValue=cache.getIfPresent(cacheRepeatKey);
        //從緩存中查找A1對應(yīng)的值,如果存在,說明當(dāng)前請求不是第一次了.
        if (StringUtils.isNotBlank(cacheValue)){
            //充當(dāng)B2
            RepeatSubmitCacheDto preCacheData=new Gson().fromJson(cacheValue,RepeatSubmitCacheDto.class);
            if (this.compareParams(currCacheData,preCacheData) && this.compareTime(currCacheData,preCacheData)){
                return true;
            }
        }

        //否則,就是第一次請求
        Map<String, Object> cacheMap = new HashMap<>();
        cacheMap.put(url, currCacheData);
        cache.put(cacheRepeatKey,new Gson().toJson(currCacheData));

        return false;
    }

    //比較參數(shù)
    private boolean compareParams(RepeatSubmitCacheDto currCacheData, RepeatSubmitCacheDto preCacheData){
        Boolean res=currCacheData.getRequestData().equals(preCacheData.getRequestData());
        return res;
    }
    //判斷兩次間隔時(shí)間
    private boolean compareTime(RepeatSubmitCacheDto currCacheData, RepeatSubmitCacheDto preCacheData){
        Boolean res=( (currCacheData.getCurrTime() - preCacheData.getCurrTime()) < (IntervalTime * 1000) );
        return res;
    }
}

該代碼雖然看起來有點(diǎn)多,但是仔細(xì)研讀,會(huì)發(fā)現(xiàn)其實(shí)這些代碼 就是筆者在上文中貼出的實(shí)現(xiàn)流程圖 的具體實(shí)現(xiàn),可以說是將理論知識(shí)進(jìn)行真正的落地實(shí)現(xiàn);

在這里再重復(fù)贅述一下,其整體的實(shí)現(xiàn)思路為:獲取當(dāng)前請求的URL作為鍵Key,暫且標(biāo)記為:A1,其取值為映射Map(Map里面的元素由:請求的鏈接url 、 請求體的數(shù)據(jù)、和 請求時(shí)的時(shí)間戳 三部分組成) 暫且標(biāo)記為V1;從緩存中(本地緩存或者分布式緩存)查找Key=A1的值V2,如果V2和V1里的請求體數(shù)據(jù)一樣 且 兩次請求是在8s內(nèi),即代表當(dāng)前請求是重復(fù)提交的,系統(tǒng)將拒絕執(zhí)行后續(xù)的業(yè)務(wù)邏輯;否則可以繼續(xù)往后面執(zhí)行 “將用戶信息插入到數(shù)據(jù)庫中” 的業(yè)務(wù)邏輯;

(3)最后,需要將上述自定義的攔截器加入中系統(tǒng)全局配置中,如下所示:

@Component
public class CustomWebConfig implements WebMvcConfigurer{
    @Autowired
    private RepeatSubmitInterceptor submitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(submitInterceptor);
    }
}

運(yùn)行項(xiàng)目,打開Postman,連續(xù)多番進(jìn)行測試,如下幾張圖所示:

干貨實(shí)戰(zhàn)~Java如何防止接口重復(fù)提交

 


干貨實(shí)戰(zhàn)~Java如何防止接口重復(fù)提交

 


干貨實(shí)戰(zhàn)~Java如何防止接口重復(fù)提交

 

至此,我們已經(jīng)采用實(shí)際的代碼實(shí)戰(zhàn)實(shí)現(xiàn)了“如何防止接口重復(fù)提交”的功能,值得一提的是,上述代碼在實(shí)現(xiàn)過程中,其核心在于緩存組件的搭建;在“重復(fù)提交”這一業(yè)務(wù)場景中,它需要滿足兩個(gè)條件方可發(fā)揮作用:一個(gè)是可以用于緩存信息,即具有Key - Value的特性;另一個(gè)是可以對存儲(chǔ)的數(shù)據(jù)設(shè)置過期時(shí)間;

在這里筆者采用的是google開發(fā)工具類中的CacheBuilder構(gòu)建本地緩存組件的,感興趣的小伙伴可以自行搜索相關(guān)資料;然而這種實(shí)現(xiàn)方式在集群多實(shí)例部署的情況下是有問題的,因?yàn)镃acheBuilder只適用于單一架構(gòu)體系,所以如果是多實(shí)例集群部署的情況,最好用redis。

(1)文中涉及到的代碼已經(jīng)放在gitee上了,訪問鏈接如下所示,別忘了給個(gè)star哦:https://gitee.com/steadyjack/SpringBootTechnologyA。

(2)期間如何有任何問題都可以私信debug。

(3)請繼續(xù)關(guān)注“程序員實(shí)戰(zhàn)基地”,您的關(guān)注和轉(zhuǎn)發(fā) 就是 debug勤勞寫技術(shù)文的動(dòng)力!!!

分享到:
標(biāo)簽:Java
用戶無頭像

網(wǎng)友整理

注冊時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績評定2018-06-03

通用課目體育訓(xùn)練成績評定