我将基于您提供的防抖切面代码进行改进,主要优化点包括使用Redis实现分布式防抖、增强键生成策略、改进异常处理等。以下是完整的改进方案:
1. 改进的防抖注解 (Debounce.java
)
java
复制
下载
package com.weiyu.anno; import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Debounce { /** * 防抖时间(毫秒),默认5秒 */ long value() default 5000; /** * 自定义键值(支持SpEL表达式) */ String key() default ""; /** * 防抖键类型 */ KeyType keyType() default KeyType.METHOD; /** * 错误提示信息 */ String message() default "操作过于频繁,请稍后再试"; enum KeyType { METHOD, // 方法级别防抖(默认) USER, // 用户级别防抖(需要用户上下文) IP, // IP级别防抖 PARAM, // 参数级别防抖(基于参数值) CUSTOM // 完全自定义键 } }
2. 改进的防抖工具类 (DebounceUtil.java
)
java
复制
下载
package com.weiyu.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import java.util.concurrent.TimeUnit; @Component public class DebounceUtil { @Autowired private StringRedisTemplate redisTemplate; private static final ExpressionParser parser = new SpelExpressionParser(); /** * 检查并设置防抖锁 * @param key 防抖键 * @param debounceTime 防抖时间(毫秒) * @return 是否允许操作(true:允许,false:防抖中) */ public boolean checkAndSet(String key, long debounceTime) { // 使用SETNX+EXPIRE原子操作 Boolean result = redisTemplate.opsForValue().setIfAbsent( key, "1", debounceTime, TimeUnit.MILLISECONDS ); return result != null && result; } /** * 生成防抖键 * @param joinPoint 切点 * @param keyType 键类型 * @param customKey 自定义键表达式 * @return 生成的防抖键 */ public String generateKey(ProceedingJoinPoint joinPoint, Debounce.KeyType keyType, String customKey) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); String methodName = signature.getDeclaringTypeName() + "#" + signature.getName(); // 1. 处理自定义SpEL表达式 if (keyType == Debounce.KeyType.CUSTOM && StringUtils.hasText(customKey)) { return evaluateSpEL(joinPoint, customKey); } // 2. 处理其他键类型 switch (keyType) { case USER: return "DEBOUNCE:USER:" + getCurrentUserId() + ":" + methodName; case IP: return "DEBOUNCE:IP:" + getClientIp() + ":" + methodName; case PARAM: return "DEBOUNCE:PARAM:" + methodName + ":" + Arrays.hashCode(joinPoint.getArgs()); case CUSTOM: // 已处理,此处为fallback case METHOD: default: return "DEBOUNCE:METHOD:" + methodName; } } /** * 解析SpEL表达式 */ private String evaluateSpEL(ProceedingJoinPoint joinPoint, String expression) { try { StandardEvaluationContext context = new StandardEvaluationContext(); // 设置方法参数 context.setVariable("args", joinPoint.getArgs()); // 设置方法签名 context.setVariable("method", joinPoint.getSignature()); // 设置目标对象 context.setVariable("target", joinPoint.getTarget()); Expression exp = parser.parseExpression(expression); return "DEBOUNCE:CUSTOM:" + exp.getValue(context, String.class); } catch (Exception e) { throw new IllegalArgumentException("Invalid SpEL expression: " + expression, e); } } /** * 获取当前用户ID(需要根据实际认证系统实现) */ private String getCurrentUserId() { // 示例:从Spring Security上下文中获取 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication != null && authentication.isAuthenticated()) { return authentication.getName(); } return "ANONYMOUS"; } /** * 获取客户端IP */ private String getClientIp() { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } return ip; } }
3. 改进的防抖切面 (DebounceAspect.java
)
java
复制
下载
package com.weiyu.aop; import com.weiyu.anno.Debounce; import com.weiyu.exception.DebounceException; import com.weiyu.utils.DebounceUtil; 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.core.annotation.AnnotationUtils; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import java.lang.reflect.Method; /** * 改进的防抖切面(支持Redis分布式锁) */ @Aspect @Component public class DebounceAspect { @Autowired private DebounceUtil debounceUtil; @Around("@annotation(debounceAnnotation)") public Object debounce(ProceedingJoinPoint joinPoint, Debounce debounceAnnotation) throws Throwable { // 获取方法上的注解(处理继承情况) if (debounceAnnotation == null) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); debounceAnnotation = AnnotationUtils.findAnnotation(method, Debounce.class); if (debounceAnnotation == null) { return joinPoint.proceed(); } } // 生成防抖键 String key = debounceUtil.generateKey( joinPoint, debounceAnnotation.keyType(), debounceAnnotation.key() ); // 获取防抖时间 long debounceTime = debounceAnnotation.value(); // 检查并设置防抖锁 if (!debounceUtil.checkAndSet(key, debounceTime)) { throw new DebounceException(debounceAnnotation.message()); } // 执行目标方法 try { return joinPoint.proceed(); } catch (Throwable throwable) { // 异常时移除防抖锁(可选) // debounceUtil.removeLock(key); throw throwable; } } }
4. 自定义防抖异常 (DebounceException.java
)
java
复制
下载
package com.weiyu.exception; /** * 防抖业务异常 */ public class DebounceException extends RuntimeException { private int code = 429; // 使用429状态码表示操作过于频繁 public DebounceException(String message) { super(message); } public int getCode() { return code; } }
5. 全局异常处理器 (GlobalExceptionHandler.java
)
java
复制
下载
package com.weiyu.exception; import com.weiyu.common.Result; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { /** * 处理防抖异常 */ @ExceptionHandler(DebounceException.class) public Result handleDebounceException(DebounceException e) { return Result.error(e.getCode(), e.getMessage()); } }
6. 统一响应对象 (Result.java
)
java
复制
下载
package com.weiyu.common; public class Result<T> { private int code; private String msg; private T data; // 成功静态方法 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } // 错误静态方法 public static <T> Result<T> error(int code, String msg) { Result<T> result = new Result<>(); result.setCode(code); result.setMsg(msg); return result; } // Getters and Setters }
7. 使用示例
java
复制
下载
@RestController @RequestMapping("/api/download") public class DownloadController { // 基本方法级别防抖(5秒内只能调用一次) @Debounce @GetMapping("/basic") public Result<String> basicDownload() { // ... 下载逻辑 return Result.success("下载成功"); } // 用户级别防抖(每个用户5秒内只能下载一次) @Debounce(keyType = Debounce.KeyType.USER, value = 5000) @GetMapping("/user") public Result<String> userSpecificDownload() { // ... 下载逻辑 return Result.success("下载成功"); } // 自定义键防抖(基于文件ID) @Debounce( keyType = Debounce.KeyType.CUSTOM, key = "'FILE:' + #fileId", // SpEL表达式 message = "该文件正在下载中,请稍后再试" ) @GetMapping("/file/{fileId}") public Result<String> fileDownload(@PathVariable String fileId) { // ... 下载逻辑 return Result.success("下载成功"); } // IP级别防抖(每个IP 10秒内只能下载一次) @Debounce(keyType = Debounce.KeyType.IP, value = 10000) @GetMapping("/ip-based") public Result<String> ipBasedDownload() { // ... 下载逻辑 return Result.success("下载成功"); } }
8. 前端适配方案
javascript
复制
下载
// 在响应拦截器中处理防抖异常 instance.interceptors.response.use( response => { // ... 正常处理 }, error => { if (error.response) { // 识别防抖异常(HTTP 429状态码) if (error.response.status === 429) { ElMessage.error(error.response.data?.msg || "操作过于频繁,请稍后再试"); return Promise.reject(error); } // 识别业务层防抖异常(code=429) const res = error.response.data; if (res && res.code === 429) { ElMessage.error(res.msg || "操作过于频繁,请稍后再试"); return Promise.reject(error); } } // ... 其他错误处理 } );
改进亮点
分布式支持:
使用Redis实现分布式防抖锁
原子操作(SETNX + EXPIRE)确保并发安全
灵活的键策略:
支持方法、用户、IP、参数、自定义五种键类型
集成SpEL表达式引擎实现动态键生成
增强的异常处理:
自定义防抖异常类型(DebounceException)
统一使用429状态码(Too Many Requests)
可自定义错误消息
注解增强:
支持方法继承的注解查找
可配置防抖时间和错误消息
支持SpEL表达式自定义键
集成Spring生态:
自动获取请求上下文(IP、用户信息)
通过全局异常处理器统一处理
更精确的防抖:
精确到毫秒级别的防抖控制
避免本地内存防抖的分布式问题
这个改进方案保持了原有代码的结构和风格,同时增加了企业级应用所需的分布式支持和灵活性,特别适合在微服务架构中使用。