????????防抖也即防重復提交,那么如何確定兩次接口就是重復的呢?首先,我們需要給這兩次接口的調用加一個時間間隔,大于這個時間間隔的一定不是重復提交;其次,兩次請求提交的參數比對,不一定要全部參數,選擇標識性強的參數即可(生產環境還可以加上用戶ID);最后,如果想做的更好一點,還可以加一個請求地址的對比。
????????分布式部署下接口防抖有有很多方法,如:使用共享緩存,使用分布式鎖,在web開發中一般新增后者。
? ? ? ? 思路如下:
1. 自定義注解@RequestLock,用于標記需要防抖的接口方法,記錄鎖的一些信息如:鎖過期時間、時間單位等。
2. 自定義@RequestKeyParam注解用于指定生成唯一鍵的參數(生成鎖名的依據)。
3. 封裝RequestKeyGenerator 類定義鎖的生成邏輯,返回生成的鎖名。
4. 實現一個切入點為添加了@RequestLock注解的方法的環繞通知,通知的內容是:生成鎖名key, 獲取目標對象的的@RequestLock攜帶的鎖過期作為鎖名key存入Redis的TTL。
5. 在需要增強的方法上添加@RequestLock,用于加鎖的參數上添加@RequestKeyParam。
實現過程:
@RequestLock
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLock {String prefix() default "";long expire() default 5; // 鎖過期時間,單位:秒String timeUnit() default "SECONDS";String delimiter() default "&";
}
@RequestKeyParam
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @description 加上這個注解可以將參數設置為key*/
@Target({ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RequestKeyParam {}
RequestKeyGenerator 類(?由于?@RequestKeyParam?
可以放在方法的參數上,也可以放在對象的屬性上,所以這里需要進行兩次判斷,一次是獲取方法上的注解,一次是獲取對象里面屬性上的注解。)
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;public class RequestKeyGenerator {/*** 獲取LockKey** @param joinPoint 切入點* @return*/public static String getLockKey(ProceedingJoinPoint joinPoint) {//獲取連接點的方法簽名對象MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();//Method對象Method method = methodSignature.getMethod();//獲取Method對象上的注解對象RequestLock requestLock = method.getAnnotation(RequestLock.class);//獲取方法參數final Object[] args = joinPoint.getArgs();//獲取Method對象上所有的注解final Parameter[] parameters = method.getParameters();StringBuilder sb = new StringBuilder();for (int i = 0; i < parameters.length; i++) {final RequestKeyParam keyParam = parameters[i].getAnnotation(RequestKeyParam.class);//如果屬性不是RequestKeyParam注解,則不處理if (keyParam == null) {continue;}//如果屬性是RequestKeyParam注解,則拼接 連接符 "& + RequestKeyParam"sb.append(requestLock.delimiter()).append(args[i]);}//如果方法上沒有加RequestKeyParam注解if (StringUtils.isEmpty(sb.toString())) {//獲取方法上的多個注解(為什么是兩層數組:因為第二層數組是只有一個元素的數組)final Annotation[][] parameterAnnotations = method.getParameterAnnotations();//循環注解for (int i = 0; i < parameterAnnotations.length; i++) {final Object object = args[i];//獲取注解類中所有的屬性字段final Field[] fields = object.getClass().getDeclaredFields();for (Field field : fields) {//判斷字段上是否有RequestKeyParam注解final RequestKeyParam annotation = field.getAnnotation(RequestKeyParam.class);//如果沒有,跳過if (annotation == null) {continue;}//如果有,設置Accessible為true(為true時可以使用反射訪問私有變量,否則不能訪問私有變量)field.setAccessible(true);//如果屬性是RequestKeyParam注解,則拼接 連接符" & + RequestKeyParam"sb.append(requestLock.delimiter()).append(ReflectionUtils.getField(field, object));}}}//返回指定前綴的keyreturn requestLock.prefix() + sb;}
}
Redis
import java.lang.reflect.Method;
import com.summo.demo.exception.biz.BizException;
import com.summo.demo.model.response.ResponseCodeEnum;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.util.StringUtils;/*** @description 緩存實現*/
@Aspect
@Configuration
@Order(2)
public class RedisRequestLockAspect {private final StringRedisTemplate stringRedisTemplate;@Autowiredpublic RedisRequestLockAspect(StringRedisTemplate stringRedisTemplate) {this.stringRedisTemplate = stringRedisTemplate;}@Around("execution(public * * (..)) && @annotation(com.summo.demo.config.requestlock.RequestLock)")public Object interceptor(ProceedingJoinPoint joinPoint) {MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();Method method = methodSignature.getMethod();RequestLock requestLock = method.getAnnotation(RequestLock.class);if (StringUtils.isEmpty(requestLock.prefix())) {throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "重復提交前綴不能為空");}//獲取自定義keyfinal String lockKey = RequestKeyGenerator.getLockKey(joinPoint);// 使用RedisCallback接口執行set命令,設置鎖鍵;設置額外選項:過期時間和SET_IF_ABSENT選項final Boolean success = stringRedisTemplate.execute((RedisCallback<Boolean>)connection -> connection.set(lockKey.getBytes(), new byte[0],Expiration.from(requestLock.expire(), requestLock.timeUnit()),RedisStringCommands.SetOption.SET_IF_ABSENT));if (!success) {throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "您的操作太快了,請稍后重試");}try {return joinPoint.proceed();} catch (Throwable throwable) {throw new BizException(ResponseCodeEnum.BIZ_CHECK_FAIL, "系統異常");}}
}
?接口方法和實體類
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;@RestController
public class UserController {@PostMapping("/add")@RequestLock(prefix = "user_add_")public ResponseEntity<String> addUser(@RequestBody AddUserRequest addUserRequest) {// 這里假設調用服務層方法添加用戶,實際需注入 userService 并確保其有 add 方法// 示例中先注釋掉,實際使用要補充服務層調用邏輯// userService.add(addUserRequest);return ResponseEntity.ok("添加用戶成功");}
}import lombok.Data;@Data
public class AddUserRequest {@RequestKeyParamprivate String userName;@RequestKeyParamprivate String userPhone;// 其他字段...
}