主頁 > 後端開發 > 介面防刷處理方案,太優雅了!

介面防刷處理方案,太優雅了!

2023-06-10 07:53:30 後端開發

來源:juejin.cn/post/7200366809407750181


前言

本文為描述通過Interceptor以及Redis實作介面訪問防刷Demo

這里會通過逐步找問題,逐步去完善的形式展示

原理

  • 通過ip地址+uri拼接用以作為訪問者訪問介面區分
  • 通過在Interceptor中攔截請求,從Redis中統計用戶訪問介面次數從而達到介面防刷目的

如下圖所示

工程

推薦一個開源免費的 Spring Boot 實戰專案:

https://github.com/javastacks/spring-boot-best-practice

其中,Interceptor處代碼處理邏輯最為重要

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 介面防刷攔截處理
 */
@Slf4j
public class AccessLimintInterceptor  implements HandlerInterceptor {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 多長時間內
     */
    @Value("${interfaceAccess.second}")
    private Long second = 10L;

    /**
     * 訪問次數
     */
    @Value("${interfaceAccess.time}")
    private Long time = 3L;

    /**
     * 禁用時長--單位/秒
     */
    @Value("${interfaceAccess.lockTime}")
    private Long lockTime = 60L;

    /**
     * 鎖住時的key前綴
     */
    public static final String LOCK_PREFIX = "LOCK";

    /**
     * 統計次數時的key前綴
     */
    public static final String COUNT_PREFIX = "COUNT";

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String uri = request.getRequestURI();
        String ip = request.getRemoteAddr(); // 這里忽略代理軟體方式訪問,默認直接訪問,也就是獲取得到的就是訪問者真實ip地址
        String lockKey = LOCK_PREFIX + ip + uri;
        Object isLock = redisTemplate.opsForValue().get(lockKey);
        if(Objects.isNull(isLock)){
            // 還未被禁用
            String countKey = COUNT_PREFIX + ip + uri;
            Object count = redisTemplate.opsForValue().get(countKey);
            if(Objects.isNull(count)){
                // 首次訪問
                log.info("首次訪問");
                redisTemplate.opsForValue().set(countKey,1,second, TimeUnit.SECONDS);
            }else{
                // 此用戶前一點時間就訪問過該介面
                if((Integer)count < time){
                    // 放行,訪問次數 + 1
                    redisTemplate.opsForValue().increment(countKey);
                }else{
                    log.info("{}禁用訪問{}",ip, uri);
                    // 禁用
                    redisTemplate.opsForValue().set(lockKey, 1,lockTime, TimeUnit.SECONDS);
                    // 洗掉統計
                    redisTemplate.delete(countKey);
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                }
            }
        }else{
            // 此用戶訪問此介面已被禁用
            throw new CommonException(ResultCode.ACCESS_FREQUENT);
        }
        return true;
    }
}

在多長時間內訪問介面多少次,以及禁用的時長,則是通過與組態檔配合動態設定

當處于禁用時直接拋例外則是通過在ControllerAdvice處統一處理 (這里代碼寫的有點丑陋)

下面是一些測驗(可以把專案通過Git還原到“【初始化】”狀態進行測驗)

  • 正常訪問時

  • 訪問次數過于頻繁時

自我提問

上述實作就好像就已經達到了我們的介面防刷目的了

但是,還不夠

為方便后續描述,專案中新增補充Controller,如下所示

簡單來說就是

  • PassCotrollerRefuseController
  • 每個Controller分別有對應的get,post,put,delete型別的方法,其映射路徑與方法名稱一致

介面自由

  • 對于上述實作,不知道你們有沒有發現一個問題
  • 就是現在我們的介面防刷處理,針對是所有的介面(專案案例中我只是寫的介面比較少)
  • 而在實際開發中,說對于所有的介面都要做防刷處理,感覺上也不太可能(寫此文時目前大四,實際作業經驗較少,這里不敢肯定)
  • 那么問題有了,該如何解決呢?目前來說想到兩個解決方案
攔截器映射規則

專案通過Git還原到"【Interceptor設定映射規則實作介面自由】"版本即可得到此案例實作

我們都知道攔截器是可以設定攔截規則的,從而達到攔截處理目的

1.這個AccessInterfaceInterceptor是專門用來進行防刷處理的,那么實際上我們可以通過設定它的映射規則去匹配需要進行【介面防刷】的介面即可

2.比如說下面的映射配置

3.這樣就初步達到了我們的目的,通過映射規則的配置,只針對那些需要進行【介面防刷】的介面才會進行處理

4.至于為啥說是初步呢?下面我就說說目前我想到的使用這種方式進行【介面防刷】的不足點:

所有要進行防刷處理的介面統一都是配置成了 x 秒內 y 次訪問次數,禁用時長為 z 秒

  • 要知道就是要進行防刷處理的介面,其 x, y, z的值也是并不一定會統一的
  • 某些防刷介面處理比較消耗性能的,我就把x, y, z設定的緊一點
  • 而某些防刷介面處理相對來說比較快,我就把x, y, z 設定的松一點
  • 這沒問題吧
  • 但是現在呢?x, y, z值全都一致了,這就不行了
  • 這就是其中一個不足點
  • 當然,其實針對當前這種情況也有解決方案
  • 那就是弄多個攔截器
  • 每個攔截器的【介面防刷】處理邏輯跟上述一致,并去映射對應要處理的防刷介面
  • 唯一不同的就是在每個攔截器內部,去修改對應防刷介面需要的x, y, z值
  • 這樣就是感徑訓比較麻煩

防刷介面映射路徑修改后維護問題

  • 雖然說防刷介面的映射路徑基本上定下來后就不會改變
  • 但實際上前后端聯調開發專案時,不會有那么嚴謹的Api檔案給我們用(這個在實習中倒是碰到過,公司不是很大,開發起來也就不那么嚴謹,啥都要自己搞,功能能實作就好)
  • 也就是說還是會有那種要修改介面的映射路徑需求
  • 當防刷介面數量特別多,后面的接手人員就很痛苦了
  • 就算是專案是自己從0到1實作的,其實有時候專案開發到后面,自己也會忘記自己前面是如何設計的
  • 而使用當前這種方式的話,誰維護誰蛋疼
自定義注解 + 反射

咋說呢

  • 就是通過自定義注解中定義 x 秒內 y 次訪問次數,禁用時長為 z 秒
  • 自定義注解 + 在需要進行防刷處理的各個介面方法上
  • 在攔截器中通過反射獲取到各個介面中的x, y, z值即可達到我們想要的介面自由目的

下面做個實作

宣告自定義注解

Controlller中方法中使用

Interceptor處邏輯修改(最重要是通過反射判斷此介面是否需要進行防刷處理,以及獲取到x, y, z的值)

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 介面防刷攔截處理
 */
@Slf4j
public class AccessLimintInterceptor  implements HandlerInterceptor {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;
    /**
     * 鎖住時的key前綴
     */
    public static final String LOCK_PREFIX = "LOCK";

    /**
     * 統計次數時的key前綴
     */
    public static final String COUNT_PREFIX = "COUNT";

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//        自定義注解 + 反射 實作
        // 判斷訪問的是否是介面方法
        if(handler instanceof HandlerMethod){
            // 訪問的是介面方法,轉化為待訪問的目標方法物件
            HandlerMethod targetMethod = (HandlerMethod) handler;
            // 取出目標方法中的 AccessLimit 注解
            AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
            // 判斷此方法介面是否要進行防刷處理(方法上沒有對應注解就代表不需要,不需要的話進行放行)
            if(!Objects.isNull(accessLimit)){
                // 需要進行防刷處理,接下來是處理邏輯
                String ip = request.getRemoteAddr();
                String uri = request.getRequestURI();
                String lockKey = LOCK_PREFIX + ip + uri;
                Object isLock = redisTemplate.opsForValue().get(lockKey);
                // 判斷此ip用戶訪問此介面是否已經被禁用
                if (Objects.isNull(isLock)) {
                    // 還未被禁用
                    String countKey = COUNT_PREFIX + ip + uri;
                    Object count = redisTemplate.opsForValue().get(countKey);
                    long second = accessLimit.second();
                    long maxTime = accessLimit.maxTime();

                    if (Objects.isNull(count)) {
                        // 首次訪問
                        log.info("首次訪問");
                        redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
                    } else {
                        // 此用戶前一點時間就訪問過該介面,且頻率沒超過設定
                        if ((Integer) count < maxTime) {
                            redisTemplate.opsForValue().increment(countKey);
                        } else {

                            log.info("{}禁用訪問{}", ip, uri);
                            long forbiddenTime = accessLimit.forbiddenTime();
                            // 禁用
                            redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
                            // 洗掉統計--已經禁用了就沒必要存在了
                            redisTemplate.delete(countKey);
                            throw new CommonException(ResultCode.ACCESS_FREQUENT);
                        }
                    }
                } else {
                    // 此用戶訪問此介面已被禁用
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                }
            }
        }
        return  true;
    }
}

由于不好演示效果,這里就不貼測驗結果圖片了

專案通過Git還原到"【自定義主鍵+反射實作介面自由"版本即可得到此案例實作,后面自己可以針對介面做下測驗看看是否如同我所說的那樣實作自定義x, y, z 的效果

嗯,現在看起來,可以針對每個要進行防刷處理的介面進行針對性自定義多長時間內的最大訪問次數,以及禁用時長,哪個介面需要,就直接+在那個介面方法出即可

感徑訓不錯的樣子,現在網上挺多資料也都是這樣實作的

但是還是可以有改善的地方

先舉一個例子,以我們的PassController為例,如下是其實作

下圖是其映射路徑關系

同一個Controller的所有介面方法映射路徑的前綴都包含了/pass

我們在類上通過注解@ReqeustMapping標記映射路徑/pass,這樣所有的介面方法前綴都包含了/pass,并且以致于后面要修改映射路徑前綴時只需改這一塊地方即可

這也是我們使用SpringMVC最常見的用法

那么,我們的自定義注解也可不可以這樣做呢?先無中生有個需求

假設PassController中所有介面都是要進行防刷處理的,并且他們的x, y, z值就一樣

如果我們的自定義注解還是只能加載方法上的話,一個一個介面加,那么無疑這是一種很呆的做法

要改的話,其實也很簡單,首先是修改自定義注解,讓其可以作用在類上

接著就是修改AccessLimitInterceptor的處理邏輯

AccessLimitInterceptor中代碼修改的有點多,主要邏輯如下

與之前實作比較,不同點在于x, y, z的值要首先嘗試在目標類中獲取

其次,一旦類中標有此注解,即代表此類下所有介面方法都要進行防刷處理

如果其介面方法同樣也標有此注解,根據就近優先原則,以介面方法中的注解標明的值為準

/**
 * @author: Zero
 * @time: 2023/2/14
 * @description: 介面防刷攔截處理
 */
@Slf4j
public class AccessLimintInterceptor implements HandlerInterceptor {
    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 鎖住時的key前綴
     */
    public static final String LOCK_PREFIX = "LOCK";

    /**
     * 統計次數時的key前綴
     */
    public static final String COUNT_PREFIX = "COUNT";

    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

//      自定義注解 + 反射 實作, 版本 2.0
        if (handler instanceof HandlerMethod) {
            // 訪問的是介面方法,轉化為待訪問的目標方法物件
            HandlerMethod targetMethod = (HandlerMethod) handler;
            // 獲取目標介面方法所在類的注解@AccessLimit
            AccessLimit targetClassAnnotation = targetMethod.getMethod().getDeclaringClass().getAnnotation(AccessLimit.class);
            // 特別注意不能采用下面這條陳述句來獲取,因為 Spring 采用的代理方式來代理目標方法
            //  也就是說targetMethod.getClass()獲得是class org.springframework.web.method.HandlerMethod ,而不知我們真正想要的 Controller
//            AccessLimit targetClassAnnotation = targetMethod.getClass().getAnnotation(AccessLimit.class);
            // 定義標記位,標記此類是否加了@AccessLimit注解
            boolean isBrushForAllInterface = false;
            String ip = request.getRemoteAddr();
            String uri = request.getRequestURI();
            long second = 0L;
            long maxTime = 0L;
            long forbiddenTime = 0L;
            if (!Objects.isNull(targetClassAnnotation)) {
                log.info("目標介面方法所在類上有@AccessLimit注解");
                isBrushForAllInterface = true;
                second = targetClassAnnotation.second();
                maxTime = targetClassAnnotation.maxTime();
                forbiddenTime = targetClassAnnotation.forbiddenTime();
            }
            // 取出目標方法中的 AccessLimit 注解
            AccessLimit accessLimit = targetMethod.getMethodAnnotation(AccessLimit.class);
            // 判斷此方法介面是否要進行防刷處理
            if (!Objects.isNull(accessLimit)) {
                // 需要進行防刷處理,接下來是處理邏輯
                second = accessLimit.second();
                maxTime = accessLimit.maxTime();
                forbiddenTime = accessLimit.forbiddenTime();
                if (isForbindden(second, maxTime, forbiddenTime, ip, uri)) {
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                }
            } else {
                // 目標介面方法處無@AccessLimit注解,但還要看看其類上是否加了(類上有加,代表針對此類下所有介面方法都要進行防刷處理)
                if (isBrushForAllInterface && isForbindden(second, maxTime, forbiddenTime, ip, uri)) {
                    throw new CommonException(ResultCode.ACCESS_FREQUENT);
                }
            }
        }
        return true;
    }

    /**
     * 判斷某用戶訪問某介面是否已經被禁用/是否需要禁用
     *
     * @param second        多長時間  單位/秒
     * @param maxTime       最大訪問次數
     * @param forbiddenTime 禁用時長 單位/秒
     * @param ip            訪問者ip地址
     * @param uri           訪問的uri
     * @return ture為需要禁用
     */
    private boolean isForbindden(long second, long maxTime, long forbiddenTime, String ip, String uri) {
        String lockKey = LOCK_PREFIX + ip + uri; //如果此ip訪問此uri被禁用時的存在Redis中的 key
        Object isLock = redisTemplate.opsForValue().get(lockKey);
        // 判斷此ip用戶訪問此介面是否已經被禁用
        if (Objects.isNull(isLock)) {
            // 還未被禁用
            String countKey = COUNT_PREFIX + ip + uri;
            Object count = redisTemplate.opsForValue().get(countKey);
            if (Objects.isNull(count)) {
                // 首次訪問
                log.info("首次訪問");
                redisTemplate.opsForValue().set(countKey, 1, second, TimeUnit.SECONDS);
            } else {
                // 此用戶前一點時間就訪問過該介面,且頻率沒超過設定
                if ((Integer) count < maxTime) {
                    redisTemplate.opsForValue().increment(countKey);
                } else {
                    log.info("{}禁用訪問{}", ip, uri);
                    // 禁用
                    redisTemplate.opsForValue().set(lockKey, 1, forbiddenTime, TimeUnit.SECONDS);
                    // 洗掉統計--已經禁用了就沒必要存在了
                    redisTemplate.delete(countKey);
                    return true;
                }
            }
        } else {
            // 此用戶訪問此介面已被禁用
            return true;
        }
        return false;
    }
}

好了,這樣就達到我們想要的效果了

專案通過Git還原到"【自定義注解+反射實作介面自由-版本2.0】"版本即可得到此案例實作,自己可以測驗萬一下

這是目前來說比較理想的做法,至于其他做法,暫時沒啥了解到

時間邏輯漏洞

這是我一開始都有留意到的問題

也是一直搞不懂,就是我們現在的所有做法其實感覺都不是嚴格意義上的x秒內y次訪問次數

特別注意這個x秒,它是連續,任意的(代表這個x秒時間片段其實是可以發生在任意一個時間軸上)

我下面嘗試表達我的意思,但是我不知道能不能表達清楚

假設我們固定某個介面5秒內只能訪問3次,以下面例子為例

底下的小圓圈代表此刻請求訪問介面

按照我們之前所有做法的邏輯走

  1. 第2秒請求到,為首次訪問,Redis中統計次數為1(過期時間為5秒)
  2. 第7秒,此時有兩個動作,一是請求到,二是剛剛第二秒Redis存的值現在過期
  3. 我們先假設這一刻,請求處理完后,Redis存的值才過期
  4. 按照這樣的邏輯走
  5. 第七秒請求到,Redis存在對應key,且不大于3, 次數+1
  6. 接著這個key立馬過期
  7. 再繼續往后走,第8秒又當做新的一個起始,就不往下說了,反正就是不會出現禁用的情況

按照上述邏輯走,實際上也就是說當出現首次訪問時,當做這5秒時間片段的起始

第2秒是,第8秒也是

但是有沒有想過,實際上這個5秒時間片段實際上是可以放置在時間軸上任意區域的

上述情況我們是根據請求的到來情況人為的把它放在【2-7】,【8-13】上

而實際上這5秒時間片段是可以放在任意區域的

那么,這樣的話,【7-12】也可以放置

而【7-12】這段時間有4次請求,就達到了我們禁用的條件了

是不是感覺怪怪的

想過其他做法,但是好像嚴格意義上真的做不到我所說的那樣(至少目前來說想不到)

之前我們的做法,正常來說也夠用,至少說有達到防刷的作用

后面有機會的話再看看,不知道我是不是鉆牛角尖了

路徑引數問題

假設現在PassController中有如下介面方法

也就是我們在介面方法中常用的在請求路徑中獲取引數的套路

但是使用路徑引數的話,就會發生問題

那就是同一個ip地址訪問此介面時,我攜帶的引數值不同

按照我們之前那種前綴+ip+uri拼接的形式作為key的話,其實是區分不了的

下圖是訪問此介面,攜帶不同引數值時獲取的uri狀況

這樣的話在我們之前攔截器的處理邏輯中,會認為是此ip用戶訪問的是不同的介面方法,而實際上訪問的是同一個介面方法

也就導致了【介面防刷】失效

接下來就是解決它,目前來說有兩種

  1. 不要使用路徑引數

這算是比較理想的做法,相當于沒這個問題

但有一定局限性,有時候接手別的專案,或者自己根本沒這個權限說不能使用路徑引數

  1. 替換uri
  • 我們獲取uri的目的,其實就是為了區別訪問介面
  • 而把uri替換成另一種可以區分訪問介面方法的標識即可
  • 最容易想到的就是通過反射獲取到介面方法名稱,使用介面方法名稱替換成uri即可
  • 當然,其實不同的Controller中,其介面方法名稱也有可能是相同的
  • 實際上可以再獲取介面方法所在類類名,使用類名 + 方法名稱替換uri即可
  • 實際解決方案有很多,看個人需求吧

真實ip獲取

在之前的代碼中,我們獲取代碼都是通過request.getRemoteAddr()獲取的

但是后續有了解到,如果說通過代理軟體方式訪問的話,這樣是獲取不到來訪者的真實ip的

至于如何獲取,后續我再研究下http再說,這里先提個醒

總結

說實話,挺有意思的,一開始自己想【介面防刷】的時候,感覺也就是轉化成統計下訪問次數的問題擺了,后面到網上看別人的寫法,又再自己給自己找點問題出來,后面會衍生出來一推東西出來,諸如自定義注解+反射這種實作方式,

以前其實對注解 + 反射其實有點不太懂干嘛用的,而從之前的資料報表匯出,再到基本權限控制實作,最后到今天的【介面防刷】一點點來進步去補充自己的知識點,而且,感覺寫博客真的是件挺有意義的事情,它會讓你去更深入的了解某個點,并且知識是相關聯的,探索的程序中會牽扯到其他別的知識點,就像之前的寫的【單例模式】實作,一開始就了解到懶漢式,餓漢式

后面深入的話就知道其實會還有序列化/反序列化,反射呼叫生成實體,物件克隆這幾種方式回去破壞單例模式,又是如何解決的,這也是一個進步的點,后續為了保證執行緒安全問題,牽扯到的synchronized,voliate關鍵字,繼而又關聯到JVM,JUC,作業系統的東西,

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了,,,

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554748.html

標籤:其他

上一篇:websocket

下一篇:返回列表

標籤雲
其他(160666) Python(38219) JavaScript(25489) Java(18216) C(15237) 區塊鏈(8270) C#(7972) AI(7469) 爪哇(7425) MySQL(7241) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5873) 数组(5741) R(5409) Linux(5347) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4589) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2435) ASP.NET(2404) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) .NET技术(1984) 功能(1967) HtmlCss(1956) Web開發(1951) C++(1933) python-3.x(1918) 弹簧靴(1913) xml(1889) PostgreSQL(1880) .NETCore(1863) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • 介面防刷處理方案,太優雅了!

    來源:juejin.cn/post/7200366809407750181 ## **前言** 本文為描述通過Interceptor以及Redis實作介面訪問防刷Demo 這里會通過逐步找問題,逐步去完善的形式展示 ## **原理** - 通過ip地址+uri拼接用以作為訪問者訪問介面區分 - 通過 ......

    uj5u.com 2023-06-10 07:53:30 more
  • websocket

    # 1. WebSocket介紹 * WebSocket 是一種網路通信協議。RFC6455 定義了它的通信標準。 * WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。 * HTTP 協議是一種無狀態的、無連接的、單向的應用層協議。它采用了請求/回應模型 ......

    uj5u.com 2023-06-10 07:53:08 more
  • 【K哥爬蟲普法】一個人、一年半、掙了2000萬!

    ![01](https://img2023.cnblogs.com/other/2501174/202306/2501174-20230609142240935-1220768091.png) > 我國目前并未出臺專門針對網路爬蟲技術的法律規范,但在司法實踐中,相關判決已屢見不鮮,K 哥特設了“K哥 ......

    uj5u.com 2023-06-10 07:52:20 more
  • ImageIO的應用 (AWT和Swing初接觸)

    # ImageIO的應用 # 一、關于IO流 在講imageio之前,我們先來復習一下IO流的使用。 這里我建立一個Java類,用來實作讀取檔案中的內容,并且能夠識別換行,話不多說,上代碼: ```java package com.Evan.demo; import java.io.Buffered ......

    uj5u.com 2023-06-10 07:51:44 more
  • JavaWeb準備作業和部分環境配置

    # JavaWeb ## 基本概念 web開發: - web:網頁的意思 - 靜態web:提供的資料資源不會發生改變 - 動態web:資料是不斷變化的,現代網站幾乎都是動態網站技術堆疊: Servlet/JSP:基于java,主推B/S架構,可以 ? 承載高并發,語法類似ASP ? ASP:微軟,國內 ......

    uj5u.com 2023-06-10 07:51:03 more
  • java集成chatGpt完整案例代碼(效果和官網一樣逐字輸出)

    背景 要集成chatGpt參考我上一篇文章即可。但是,如果要實作官網一樣的效果,逐字輸出,難度就提升了不少了。經過在官網的研究發現它應該是采用了SSE技術,這是一種最新的HTTP互動技術。SSE(Server-Sent Events):通俗解釋起來就是一種基于HTTP的,以流的形式由服務端持續向客戶 ......

    uj5u.com 2023-06-10 07:50:36 more
  • Java物件中轉換空值的欄位

    在后端回傳資料給前端時,公司的框架會把值為null的資料去掉,方便前端取值。 如:Person物件為:{name:"浩二", age:24, weight:null, height:114},那回傳給前端的就為{name:"浩二", age:24, height:114}。 如果這個時候有個需求: ......

    uj5u.com 2023-06-10 07:50:12 more
  • 構建高可用、高效、安全和可靠的分布式系統:Spring Cloud為開發人

    Spring Cloud是一個基于Spring框架的云應用開發工具箱,可以快速構建分布式應用程式。它為開發人員提供了許多有用的功能,例如服務發現、負載均衡、配置管理和應用監控等,這些功能可以使分布式系統更加穩定和可靠。 如果您還沒有了解過Spring Cloud,我強烈建議您觀看以下這個視頻鏈接: ......

    uj5u.com 2023-06-10 07:50:05 more
  • Java 集合框架體系簡介

    陣列可以通過索引快速訪問和操作元素,在許多場景下仍然是非常有用的,但如果需要動態調整大小或保存不同型別的元素,則可以考慮使用集合類來代替陣列。集合類還提供了一系列增加、洗掉、修改和查找元素的方法。集合框架中還提供了多種優化和封裝好的實作類,通過使用合適的集合類可以更高效地組織和操作資料。 ......

    uj5u.com 2023-06-10 07:44:50 more
  • websocket

    # 1. WebSocket介紹 * WebSocket 是一種網路通信協議。RFC6455 定義了它的通信標準。 * WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。 * HTTP 協議是一種無狀態的、無連接的、單向的應用層協議。它采用了請求/回應模型 ......

    uj5u.com 2023-06-10 07:25:34 more