SpringBoot+Redis?BitMap實現(xiàn)簽到與統(tǒng)計的項目實踐
最近項目里需要集成簽到和統(tǒng)計功能,連續(xù)簽到后會給用戶發(fā)放一些優(yōu)惠券和獎品,以此來吸引用戶持續(xù)在該品臺進(jìn)行活躍。下面我們一些來聊一聊目前主流的實現(xiàn)方案。
因為簽到和統(tǒng)計的功能涉及的數(shù)據(jù)量比較大,所以在如此大的數(shù)據(jù)下利用傳統(tǒng)的關(guān)系型數(shù)據(jù)庫進(jìn)行計算和統(tǒng)計是非常耗費(fèi)性能的,所以目前市面上主要依賴于高性能緩存RedisBitMap 功能來實現(xiàn)。
先看看利用Mysql實現(xiàn)以上功能會有哪些缺陷和短板。
1.使用Mysql實現(xiàn)簽到功能
首先我們需要一個簽到表
DROP TABLE IF EXISTS `tb_sign`; CREATE TABLE `tb_sign` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵', `user_id` int(11) NOT NULL COMMENT '用戶Id', `year` year(4) NOT NULL COMMENT '簽到的年', `month` tinyint(2) NOT NULL COMMENT '簽到的月', `date` date NOT NULL COMMENT '簽到日期', `is_backup` tinyint(1) NOT NULL COMMENT '是否補(bǔ)簽', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
用戶一次簽到,就是一條記錄,假如有1000萬用戶,平均每人每年簽到次數(shù)為10次,則這張表一年的數(shù)據(jù)量為 1億條
每簽到一次需要使用(8 + 8 + 1 + 1 + 3 + 1)共22 字節(jié)的內(nèi)存,一個月則最多需要600多字節(jié)
這樣的壞處,占用內(nèi)存太大了,極大的消耗內(nèi)存空間!
我們可以根據(jù) Redis中 提供的 BitMap 位圖功能來實現(xiàn),每次簽到與未簽到用0 或1 來標(biāo)識 ,一次存31個數(shù)字,只用了2字節(jié) 這樣我們就用極小的空間實現(xiàn)了簽到功能
2.Redis BitMap
2.1 BitMap 的操作指令
- SETBIT :向指定位置(offset)存入一個0或1
- GETBIT :獲取指定位置(offset)的bit值
- BITCOUNT :統(tǒng)計BitMap中值為1的bit位的數(shù)量
- BITFIELD :操作(查詢、修改、自增)BitMap中bit數(shù)組中的指定位置(offset)的值
- BITFIELD_RO :獲取BitMap中bit數(shù)組,并以十進(jìn)制形式返回
- BITOP :將多個BitMap的結(jié)果做位運(yùn)算(與 、或、異或)
- BITPOS :查找bit數(shù)組中指定范圍內(nèi)第一個0或1出現(xiàn)的位置
2.2 使用 BitMap 完成功能實現(xiàn)
利用SETBIT新增key 進(jìn)行存儲
SETBIT bm1 0 1
看不懂上面的指令?沒關(guān)系,我們可以通過help指令查看提示
help SETBIT
通過這個指令可以看出Redis SETBIT 命令用于對 key 所儲存的字符串值,設(shè)置或清除指定偏移量上的位(bit)。位的設(shè)置或清除取決于 value,可以是 0 或者是 1 。
當(dāng) key 不存在時,自動生成一個新的字符串值。字符串會進(jìn)行伸展以確保它可以將 value 保存在指定的偏移量上。當(dāng)字符串值進(jìn)行伸展時,空白位置以 0 填充。 offset 參數(shù)必須大于或等于 0 ,小于 2^32 (bit 被限制在 512 MB 之內(nèi))。
提示:如果 offset 偏移量的值較大,計算機(jī)進(jìn)行內(nèi)存分配時可能會造成 Redis 服務(wù)器被阻塞。
這樣的話我們就可以通過偏移量設(shè)置每一天的簽到情況:
- 偏移量:表示天
- val值:表示是否簽到
- 已簽到設(shè)置為1
- 未簽到設(shè)置0
下面我們只需要通過GETBIT命令就可以查看每一天的簽到情況
GETBIT bm1 2
表示查看bm1用戶第二天的簽到情況
同樣,我們可以通過BITCOUNT可以統(tǒng)計出該用戶簽到了多少天
BITCOUNT bm1
BITFIELD
通過BITFIELD以原子方式操作(查詢、修改、自增)BitMap中bit數(shù)組中的指定位置(offset)的值
BITFIELD復(fù)雜度是O(n),其中n是訪問的計數(shù)器數(shù)。
語法:
BITFIELD key [GET type offset] [SET type offset value] [INCRBY type offset increment] [OVERFLOW WRAP|SAT|FAIL]
參數(shù)說明:
- key :需要操作的鍵名。
- GET type offset :獲取指定位數(shù)的值
- type表示數(shù)據(jù)類型,可以是無符號整數(shù)(u)或有符號整數(shù)(i)
- offset表示偏移量,如果位不存在,返回0。
- SET type offset value :設(shè)置指定位數(shù)的值,type和offset同上,value表示需要設(shè)置的值,因為此命令只支持設(shè)置8位或更少的位,所以value不能超過8個二進(jìn)制位。
- INCRBY type offset increment :將指定位的值增加指定的增量,type和offset同上,increment表示需要增加的值,也必須不超過8個二進(jìn)制位。
- OVERFLOW :做溢出的處理,默認(rèn)是WRAP(循環(huán)),其他兩個選項是SAT(飽和,超出范圍的值都設(shè)置成最大或最小值)和FAIL(不允許溢出,會返回一個錯誤)。
使用方法
- 獲取一個整數(shù)的二進(jìn)制位
舉個例子,我們有一個整數(shù)值10,它的二進(jìn)制位是00001010,我們想獲取它的第3位到第5位的值,即001。那么可以這樣使用:
127.0.0.1:6379> BITFIELD myint GET u3 3 u1 6 1) (integer) 1 2) (integer) 0
u3:表示3位無符號整數(shù)
指定myint鍵值,通過GET命令獲取它的第3位到第5位,結(jié)果返回一個二進(jìn)制數(shù)001,也就是十進(jìn)制的1。第二個返回值是0,表示其他未指定的位都是0。
- 設(shè)置一個整數(shù)的二進(jìn)制位
現(xiàn)在我們想把剛才的整數(shù)值10的第3位到第5位修改為101,即值為5??梢赃@樣設(shè)置:
127.0.0.1:6379> BITFIELD myint SET u3 3 5 (integer) 10
執(zhí)行成功后,該鍵值指定的整數(shù)值變?yōu)?3(二進(jìn)制為00001101)。
所以統(tǒng)計每個用戶的簽到情況和積分,可以使用 Redis BITFIELD 命令記錄每個用戶每天的簽到情況。
每個用戶一年365天,需要使用整數(shù)類型的BITFIELD信息記錄,每個用戶需要365個二進(jìn)制位來表示簽到情況(已簽到為1,未簽到為0),再需要一個整數(shù)位去表示用戶的總積分,就可以方便地統(tǒng)計用戶簽到情況并進(jìn)行排名。
當(dāng)然該命令的功能遠(yuǎn)不止于此,在某些系統(tǒng)中,需要對某些數(shù)據(jù)做計數(shù),比如對每個IP地址訪問次數(shù)的計數(shù)??梢允褂米址愋偷挠嫈?shù)器來達(dá)到這個目的,首先在Redis中創(chuàng)建一個字符串類型的計數(shù)器(值為0),通過INCRBY命令執(zhí)行增減操作,每次給IP地址所代表的計數(shù)器加1,最后獲取到的長度即為IP地址對應(yīng)的訪問次數(shù)。如果訪問量過大,可以使用整數(shù)類型的BITFIELD存儲計數(shù)器,通過INCRBY命令執(zhí)行增減操作。這樣可以優(yōu)化性能并減少內(nèi)存占用。
BITPOS 查詢1 和 0 第一次出現(xiàn)的坐標(biāo)
3.SpringBoot整合Redis實現(xiàn)簽到功能
3.1 思路分析
考慮到每月初需要重置連續(xù)簽到次數(shù),我們可以把 年和月 作為BitMap的key,然后保存到一個BitMap中,每次簽到就到對應(yīng)的位上把數(shù)字從0 變?yōu)?,只要是1,就代表是這一天簽到了,反之咋沒有簽到。
key的格式為:“USER_SIGN_KEY:” + userId + keySuffix
- USER_SIGN_KEY:前綴
- userId:用戶id
- keySuffix:yyyyMM
Value則采用長度為4個字節(jié)(32位)的位圖(最大月份只有31天)。位圖的每一位代表一天的簽到,1表示已簽,0表示未簽。
例如:USER_SIGN_KEY:122101:202309:表示ID=122101的用戶在2023年9月的簽到記錄
# 用戶9月17號簽到 SETBIT USER_SIGN_KEY:122101:202309 16 1 # 偏移量是從0開始,所以要把17減1 # 檢查9月17號是否簽到 GETBIT USER_SIGN_KEY:122101:202309 16 # 偏移量是從0開始,所以要把17減1 # 統(tǒng)計9月份的簽到次數(shù) BITCOUNT USER_SIGN_KEY:122101:202309 # 獲取9月份前30天的簽到數(shù)據(jù),u30表示取0-29位的數(shù)據(jù) BITFIELD USER_SIGN_KEY:122101:202309 get u30 0 # 獲取9月份首次簽到的日期 BITPOS USER_SIGN_KEY:122101:202309 1 # 返回的首次簽到的偏移量,加上1即為當(dāng)月的某一天
有了上面的理論支撐,下面我開始在項目里去集成,為了節(jié)約篇幅,只展示核心代碼,項目地址在文章末尾,完整代碼可下載之后自行觀看
首先需要我們項目里集成redis,可以參考:《springBoot集成redis(jedis)詳解》
3.2 簽到功能
簽到功能實現(xiàn)方式如下:
@GetMapping("sign") public Object sign() { //1. 獲取登錄用戶 Long userId = 122101L; //2.獲取簽到使用的key String key = RedisKeyUtils.createSignKey(userId); //3. 獲取今天是本月的第幾天設(shè)置偏移量 LocalDateTime now = LocalDateTime.now(); int offset = now.getDayOfMonth() - 1; System.out.println(String.format("入?yún)ⅲ簁ey:%s,offset:%s", JSON.toJSONString(key), JSON.toJSONString(offset))); //5. 寫入redis setbit key offset 1 Boolean res = jedis.setbit(key, offset, true); System.out.println(String.format("出參:%s", JSON.toJSONString(res))); return String.format("%s簽到成功,簽到結(jié)果:%s", JSON.toJSONString(now.format(DateTimeFormatter.ofPattern("yyyyMM"))), JSON.toJSONString(res)); }
測試:
簽到功能我們已經(jīng)實現(xiàn),只需要將對應(yīng)該月的某天設(shè)置為1則表示簽到成功
檢查用戶是否簽到
@GetMapping("checkSign") public Object checkSign(@RequestParam(value = "sigDate") String sigDate) { //1. 獲取登錄用戶 Long userId = 122101L; //2.獲取簽到使用的key LocalDateTime time = TimeUtils.str2LocalDateTime(TimeUtils.PATTERN.YYYYMMDD, sigDate); if (Objects.isNull(time)) { return false; } String key = RedisKeyUtils.createSignKey(time, userId); //3. 獲取今天是本月的第幾天設(shè)置偏移量 int offset = time.getDayOfMonth() - 1; System.out.println(String.format("入?yún)ⅲ簁ey:%s,offset:%s", JSON.toJSONString(key), JSON.toJSONString(offset))); Boolean res = jedis.getbit(key, offset); System.out.println(String.format("出參:%s", JSON.toJSONString(res))); return res; }
訪問:http://127.0.0.1:8080/checkSign?sigDate=2023-09-07
下面我們繼續(xù)看看簽到統(tǒng)計功能
3.3 簽到統(tǒng)計功能
Q1:什么叫連續(xù)簽到天數(shù)?
從最后一次簽到開始向前統(tǒng)計,直到遇到第一次未簽到為止,計算總的簽到次數(shù),就是連續(xù)簽到天數(shù)。
所以我們統(tǒng)計的方式很簡單:
?獲得當(dāng)前這個月的最后一次簽到數(shù)據(jù),定義一個計數(shù)器,然后不停的向前統(tǒng)計,直到獲得第一個非0的數(shù)字即可,每得到一個非0的數(shù)字計數(shù)器+1,直到遍歷完所有的數(shù)據(jù),就可以獲得當(dāng)前月的簽到總天數(shù)了?
Q2:如何得到本月到今天為止的所有簽到數(shù)據(jù)?
那我們則需要借助BITFIELD指令來進(jìn)行實現(xiàn)
BITFIELD key GET u[dayOfMonth-1] 0
假設(shè)今天是5號,那么我們就可以從當(dāng)前月的第一天開始,獲得到當(dāng)前這一天的位數(shù),是5號,那么就是5位,去拿這段時間的數(shù)據(jù),就能拿到所有的數(shù)據(jù)了,那么這5天里邊簽到了多少次呢?統(tǒng)計有多少個1即可。
Q3:如何從后向前遍歷每個Bit位?
值得我們注意的是:?bitMap返回的數(shù)據(jù)是10進(jìn)制,哪假如說返回一個數(shù)字8,那么我哪兒知道到底哪些是0,哪些是1呢??
這是一道很簡單的位運(yùn)算算法題
我們只需要讓得到的10進(jìn)制數(shù)字和1做與運(yùn)算就可以了,因為1只有遇見1 才是1,其他數(shù)字都是0 ,我們把簽到結(jié)果和1進(jìn)行與操作,每與一次,就把簽到結(jié)果向右移動一位,依次類推,我們就能完成逐個遍歷的效果了。
通過上面的幾個方法就可以很容易統(tǒng)計用戶簽到的情況。
下面看看代碼的具體實現(xiàn)
- 獲取用戶連續(xù)簽到的天數(shù)
為了方便測試,我的當(dāng)前時間為:2023-09-07,所以我對1,3,5,6,7五天的簽到設(shè)置為1
那么當(dāng)前緩存里的值則為:1010111,對應(yīng)的10進(jìn)制數(shù)為:87,最大連續(xù)1出現(xiàn)的個數(shù)為3,所以簽到的天數(shù)為3
代碼如下:
@GetMapping("signCount") public Object signCount() { //1. 獲取登錄用戶 Long userId = 122101L; //2. 獲取日期 LocalDateTime now = LocalDateTime.now(); //3. 拼接key String key = RedisKeyUtils.createSignKey(now, userId); //4. 獲取今天是本月的第幾天 int offset = now.getDayOfMonth(); //5. 獲取本月截至今天為止的所有的簽到記錄,返回的是一個十進(jìn)制的數(shù)字 BITFIELD USER_SIGN_KEY1:5:202309 GET u5 0 String type = String.format("u%d", offset); System.out.println(String.format("入?yún)ⅲ簁ey:%s,operationType:%s,type:%s", JSON.toJSONString(key), JSON.toJSONString(RedisHelper.OPERATION_TYPE.GET.name()), JSON.toJSONString(type))); List<Long> result = RedisHelper.bitField(key, RedisHelper.OPERATION_TYPE.GET, type, "0"); //沒有任務(wù)簽到結(jié)果 if (result == null || result.isEmpty()) { return 0; } Long num = result.get(0); if (num == null || num == 0) { return 0; } //6. 循環(huán)遍歷 int count = 0; while (true) { //6.1 讓這個數(shù)字與1 做與運(yùn)算,得到數(shù)字的最后一個bit位 判斷這個數(shù)字是否為0 if ((num & 1) == 0) { //如果為0,簽到結(jié)束 break; } else { count++; } num >>>= 1; } System.out.println(String.format("緩存值:%s,簽到天數(shù):%d", JSON.toJSONString(result), count)); return count; }
測試:
3.4 獲取當(dāng)月首次簽到日期
利用Bitpos可以幫助我們輕松實現(xiàn)該功能
- bitpos USER_SIGN_KEY:122101:202309 1:表示1首次出現(xiàn)的位置
- bitpos USER_SIGN_KEY:122101:202309 0:表示0首次出現(xiàn)的位置
@GetMapping("getFirstSignDate") public Object getFirstSignDate(@RequestParam(value = "time") String time) { //1. 獲取登錄用戶 Long userId = 122101L; //2. 獲取日期 LocalDateTime dateTime = TimeUtils.str2LocalDateTime(TimeUtils.PATTERN.YYYYMMDD, time); //3. 拼接key String key = RedisKeyUtils.createSignKey(dateTime, userId); //4. 獲取首次出現(xiàn)的位置索引 long pos = jedis.bitpos(key, true); //5. 轉(zhuǎn)換為日期 String res = pos < 0 ? "無簽到記錄" : TimeUtils.localDateTime2String(TimeUtils.PATTERN.YYYYMMDD, dateTime.withDayOfMonth((int) (pos + 1))); return res; }
3.5 獲取當(dāng)月的簽到情況
@GetMapping("getSignInfo") public Object getSignInfo(@RequestParam(value = "time") String time) { //1. 獲取登錄用戶 Long userId = 122101L; //2. 獲取日期 LocalDateTime dateTime = TimeUtils.str2LocalDateTime(TimeUtils.PATTERN.YYYYMMDD, time); //當(dāng)月天數(shù) int monthOfDays = TimeUtils.getDayNumsOfMonth(dateTime); Map<String, Boolean> signMap = new HashMap<>(dateTime.getDayOfMonth()); //3. 拼接key String key = RedisKeyUtils.createSignKey(dateTime, userId); //4. 設(shè)置位數(shù) String type = String.format("u%d", monthOfDays); System.out.println(String.format("入?yún)ⅲ簁ey:%s,operationType:%s,type:%s", JSON.toJSONString(key), JSON.toJSONString(RedisHelper.OPERATION_TYPE.GET.name()), JSON.toJSONString(type))); List<Long> list = RedisHelper.bitField(key, RedisHelper.OPERATION_TYPE.GET, type, "0"); if (!CollectionUtils.isEmpty(list)) { Long num = list.get(0); int i = monthOfDays; while (i > 0) { String d = TimeUtils.localDateTime2String(TimeUtils.PATTERN.YYYYMMDD, dateTime.withDayOfMonth(i--)); if ((num & 1) == 0) { signMap.put(d, false); } else { signMap.put(d, true); } num >>>= 1; } } //按照日期排序 TreeMap sortedMap = new TreeMap(signMap); System.out.println("出參:" + JSON.toJSONString(sortedMap)); return sortedMap; }
至此簽到所需要的功能我們就全部實現(xiàn)了
本文的核心在于bitMap的使用,但是任何技術(shù)都需要有具體的落地,所以借著簽到的場景帶著大家具體使用一下。
當(dāng)然bitmap的落地場景遠(yuǎn)不止我上文中介紹的簽到統(tǒng)計等業(yè)務(wù)層面的方案,針對緩存穿透的場景,bitMap也是一個極佳的選擇。
4.利用BitMap解決緩存穿透
描述: 緩存穿透是指緩存和數(shù)據(jù)庫中都沒有的數(shù)據(jù),而用戶不斷發(fā)起請求,如發(fā)起為id為“-1”的數(shù)據(jù)或id為特別大不存在的數(shù)據(jù)。這時的用戶很可能是攻擊者,攻擊會導(dǎo)致數(shù)據(jù)庫壓力過大。
這種可以認(rèn)為是系統(tǒng)漏洞,一旦發(fā)生這種情況一般都來自于惡意請求。
常見解決方案:
- 接口層增加校驗,如用戶鑒權(quán)校驗,id做基礎(chǔ)校驗,id<=0的直接攔截;
- 從緩存取不到的數(shù)據(jù),在數(shù)據(jù)庫中也沒有取到,這時也可以將key-value對寫為key-null,緩存有效時間可以設(shè)置短點,如30秒(設(shè)置太長會導(dǎo)致正常情況也沒法使用)。這樣可以防止攻擊用戶反復(fù)用同一個id暴力攻擊
第一種解決方案:遇到的問題是如果用戶訪問的是id不存在的數(shù)據(jù),則此時就無法生效,如id遠(yuǎn)大于某個值,雖然不小于0,但是也無法進(jìn)行過濾。
第二種解決方案:遇到的問題是:如果是不同的id那就可以防止下次過來直擊數(shù)據(jù)
所以我們?nèi)绾谓鉀Q呢?
我們可以將數(shù)據(jù)庫的數(shù)據(jù),所對應(yīng)的id寫入到一個list集合中,當(dāng)用戶過來訪問的時候,我們直接去判斷l(xiāng)ist中是否包含當(dāng)前的要查詢的數(shù)據(jù),如果說用戶要查詢的id數(shù)據(jù)并不在list集合中,則直接返回,如果list中包含對應(yīng)查詢的id數(shù)據(jù),則說明不是一次緩存穿透數(shù)據(jù),則直接放行。
現(xiàn)在的問題是這個主鍵其實并沒有那么短,有的可能是通過各種拼接生成的Id,是很長的一個主鍵,所以如果采用以上方案,這個list也會很大,所以我們可以 使用bitmap來減少list的存儲空間
我們可以把list數(shù)據(jù)抽象成一個非常大的bitmap,我們不再使用list,而是將db中的id數(shù)據(jù)利用哈希思想,比如:
id 求余bitmap長度 : index=id%bitmap.size
算出當(dāng)前這個id對應(yīng)應(yīng)該落在bitmap的哪個索引上,然后將這個值從0變成1,然后當(dāng)用戶來查詢數(shù)據(jù)時,此時已經(jīng)沒有了list,讓用戶用他查詢的id去用相同的哈希算法, 算出來當(dāng)前這個id應(yīng)當(dāng)落在bitmap的哪一位,然后判斷這一位是0,還是1,如果是0則表明這一位上的數(shù)據(jù)一定不存在,采用這種方式來處理,需要重點考慮一個事情,就是誤差率, 所謂的誤差率就是指當(dāng)發(fā)生哈希沖突的時候,產(chǎn)生的誤差 。
項目地址:https://gitee.com/ninesuntec/redisBitMapSign
到此這篇關(guān)于SpringBoot+Redis BitMap實現(xiàn)簽到與統(tǒng)計的項目實踐的文章就介紹到這了,更多相關(guān)SpringBoot+Redis BitMap簽到與統(tǒng)計內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- springboot項目Redis統(tǒng)計在線用戶的實現(xiàn)示例
- SpringBoot+Redis Bitmap實現(xiàn)活躍用戶統(tǒng)計
- 微服務(wù)Spring Boot 整合 Redis 實現(xiàn)UV 數(shù)據(jù)統(tǒng)計的詳細(xì)過程
- 微服務(wù)?Spring?Boot?整合?Redis?BitMap?實現(xiàn)?簽到與統(tǒng)計功能
- SpringBoot整合Redis實現(xiàn)訪問量統(tǒng)計的示例代碼
- SpringBoot使用Redis的zset統(tǒng)計在線用戶信息
- SpringBoot運(yùn)用Redis統(tǒng)計用戶在線數(shù)量的兩種方法實現(xiàn)
相關(guān)文章
MyBatis批量插入幾千條數(shù)據(jù)為何慎用foreach
這篇文章主要介紹了MyBatis批量插入幾千條數(shù)據(jù)為何慎用foreach問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10如何使用try-with-resource機(jī)制關(guān)閉連接
這篇文章主要介紹了使用try-with-resource機(jī)制關(guān)閉連接的操作,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-07-07Java中實現(xiàn)定時任務(wù)的兩種方法舉例詳解
這篇文章主要給大家介紹了關(guān)于Java中實現(xiàn)定時任務(wù)的兩種方法,文中總結(jié)了各種實現(xiàn)方式的優(yōu)缺點,并給出了推薦的使用場景,通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-12-12java-SSH2實現(xiàn)數(shù)據(jù)庫和界面的分頁
本文主要是介紹SSH2實現(xiàn)數(shù)據(jù)庫和界面的分頁的代碼,分頁在web應(yīng)用中是經(jīng)常要做的事情,實用性比較大,有需要的朋友可以來了解一下。2016-10-10SpringBoot DBUnit 單元測試(小結(jié))
這篇文章主要介紹了SpringBoot DBUnit 單元測試(小結(jié)),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-09-09前端如何傳遞Array、Map類型數(shù)據(jù)到Java后端
這篇文章主要給大家介紹了關(guān)于前端如何傳遞Array、Map類型數(shù)據(jù)到Java后端的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考借鑒價值,需要的朋友可以參考下2024-01-01SpringBoot同一接口多個實現(xiàn)類配置的實例詳解
這篇文章主要介紹了SpringBoot同一接口多個實現(xiàn)類配置的實例詳解,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-11-11