深圳做網(wǎng)站 信科網(wǎng)絡(luò)seo研究中心學(xué)員案例
一、啥是防抖
?
所謂防抖,一是防用戶手抖,二是防網(wǎng)絡(luò)抖動。在Web系統(tǒng)中,表單提交是一個非常常見的功能,如果不加控制,容易因為用戶的誤操作或網(wǎng)絡(luò)延遲導(dǎo)致同一請求被發(fā)送多次,進(jìn)而生成重復(fù)的數(shù)據(jù)記錄。要針對用戶的誤操作,前端通常會實現(xiàn)按鈕的loading狀態(tài),阻止用戶進(jìn)行多次點(diǎn)擊。而對于網(wǎng)絡(luò)波動造成的請求重發(fā)問題,僅靠前端是不行的。為此,后端也應(yīng)實施相應(yīng)的防抖邏輯,確保在網(wǎng)絡(luò)波動的情況下不會接收并處理同一請求多次。
一個理想的防抖組件或機(jī)制,我覺得應(yīng)該具備以下特點(diǎn):
邏輯正確,也就是不能誤判;
響應(yīng)迅速,不能太慢;
易于集成,邏輯與業(yè)務(wù)解耦;
良好的用戶反饋機(jī)制,比如提示“您點(diǎn)擊的太快了”
二、思路解析
前面講了那么多,我們已經(jīng)知道接口的防抖是很有必要的了,但是在開發(fā)之前,我們需要捋清楚幾個問題。
2.1.哪一類接口需要防抖?
接口防抖也不是每個接口都需要加,一般需要加防抖的接口有這幾類:
用戶輸入類接口:比如搜索框輸入、表單輸入等,用戶輸入往往會頻繁觸發(fā)接口請求,但是每次觸發(fā)并不一定需要立即發(fā)送請求,可以等待用戶完成輸入一段時間后再發(fā)送請求。
按鈕點(diǎn)擊類接口:比如提交表單、保存設(shè)置等,用戶可能會頻繁點(diǎn)擊按鈕,但是每次點(diǎn)擊并不一定需要立即發(fā)送請求,可以等待用戶停止點(diǎn)擊一段時間后再發(fā)送請求。
滾動加載類接口:比如下拉刷新、上拉加載更多等,用戶可能在滾動過程中頻繁觸發(fā)接口請求,但是每次觸發(fā)并不一定需要立即發(fā)送請求,可以等待用戶停止?jié)L動一段時間后再發(fā)送請求。
2.2.如何確定接口是重復(fù)的?
防抖也即防重復(fù)提交,那么如何確定兩次接口就是重復(fù)的呢?首先,我們需要給這兩次接口的調(diào)用加一個時間間隔,大于這個時間間隔的一定不是重復(fù)提交;其次,兩次請求提交的參數(shù)比對,不一定要全部參數(shù),選擇標(biāo)識性強(qiáng)的參數(shù)即可;最后,如果想做的更好一點(diǎn),還可以加一個請求地址的對比。
- 定義一個RequestLock,配置超時時間、異常消息、分組標(biāo)識(用戶標(biāo)識)
/*** 請求鎖,防止重復(fù)提交** @author xt*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLock {/*** 過期時間** @return*/long expire() default 3;/*** 異常提示** @return*/String message() default "您的操作太快了,請稍后重試";/*** 參數(shù)分隔符** @return*/String delimiter() default "|";/*** 時間單位** @return*/TimeUnit timeUnit() default TimeUnit.SECONDS;/*** 前綴(從請求header key)** @return*/String group() default "loginuserid";
}
- 定義一個aspect 實現(xiàn)對注解RequestLock的endpoint進(jìn)行攔截
@EnableAspectJAutoProxy
@Aspect
@Configuration
@Order
public class RequestLockAspect {@Resourceprivate RedisTemplate redisTemplate;@Pointcut("execution(public * * (..)) && @annotation(org.xt.shisui.redis.duplicate.RequestLock)")public void endpointPointcut() {}@Around("endpointPointcut()")public Object interceptor(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();Method method = methodSignature.getMethod();RequestLock requestLock = method.getAnnotation(RequestLock.class);if (redisTemplate != null) {String key = RequestLockKeyGenerator.getLockKey(joinPoint);Boolean success = redisTemplate.opsForValue().setIfAbsent(key, new byte[0], requestLock.expire(), requestLock.timeUnit());if (Boolean.FALSE.equals(success)) {return Response.no(requestLock.message());}}return joinPoint.proceed();}
}
- 根據(jù)請求參數(shù)構(gòu)建RequestLock鎖的key,即Redis存儲的key
/*** 根據(jù)請求參數(shù)構(gòu)建鎖的key** @author xt* @date 2022-07-15 14:21*/
public class RequestLockKeyGenerator {public static String getLockKey(ProceedingJoinPoint joinPoint) {String ipAddress = null, group = null;ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();//方法名稱String methodName = signature.getName();//類路徑String declaringTypeName = signature.getDeclaringTypeName();RequestLock requestLock = method.getAnnotation(RequestLock.class);if (attributes != null) {//加上請求中的ip和分組標(biāo)識,防止錯誤攔截HttpServletRequest request = attributes.getRequest();ipAddress = request.getRemoteAddr();group = request.getHeader(requestLock.group());}final Object[] args = joinPoint.getArgs();final Parameter[] parameters = method.getParameters();StringBuilder params = new StringBuilder();String delimiter = requestLock.delimiter();for (int i = 0; i < parameters.length; i++) {//忽略特殊參數(shù),如圖片、大文本等,如果是存hashcode 可以不需要這個注解final RequestLockKeyIgnore keyIgnore = parameters[i].getAnnotation(RequestLockKeyIgnore.class);if (keyIgnore != null) {continue;}Object arg = args[i];if (arg != null) {params.append(delimiter).append(arg);}}StringBuilder result = new StringBuilder();result.append(declaringTypeName).append(delimiter).append(methodName).append(delimiter).append(ipAddress).append(delimiter).append(delimiter).append(group).append(params.hashCode());return result.toString();}
}
- 如果Redis存儲請求參數(shù)字符串,可以增加特殊參數(shù)忽略注解,如圖片等屬性,建議用hashcode
/*** 忽略該參數(shù),防止一些base64字符串被當(dāng)做主鍵** @author xt* @date 2022-01-05 14:37*/
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestLockKeyIgnore {
}
- 具體使用demo
@RequestLock(expire = 5)@ApiOperation("新增")@RequestMapping(value = "/create", method = RequestMethod.POST)public Response<ChatSpeechcraftCategoryCreateResp> create(@RequestBody @Validated ChatSpeechcraftCategoryCreateReq req, final HttpServletRequest request) throws SimpleException {return chatSpeechcraftCategoryApiService.create(req);}