需排查分析的防抖切面类 AOP代码:
package com.weiyu.aop;
import com.weiyu.anno.Debounce;
import com.weiyu.utils.DebounceUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* 防抖切面类 AOP
*/
@Component
@Aspect
public class DebounceAspect {
@Autowired
private DebounceUtil debounceUtil;
/**
* 切面:防抖
*/
@Around("@annotation(com.weiyu.anno.Debounce)")
public Object debounce(ProceedingJoinPoint joinPoint, Debounce debounceAnnotation) throws Throwable {
System.out.println("=== 防抖切面进入 ===");
// 从注解中获取 key
String key = debounceAnnotation.key();
// 从注解中获取防抖时间
long delay = debounceAnnotation.value();
// 防抖
if (!debounceUtil.checkAndSet(key, delay)) {
throw new RuntimeException("操作过于频繁,请稍后再试");
}
return joinPoint.proceed();
}
}
您提供的代码存在一个关键问题,会导致防抖功能失效。以下是详细的问题排查和修复方案:
java
复制
下载
@Around("@annotation(com.weiyu.anno.Debounce)") public Object debounce(ProceedingJoinPoint joinPoint, Debounce debounceAnnotation) throws Throwable { // ... }
问题出在 切点表达式与参数绑定的不匹配:
切点表达式:@annotation(com.weiyu.anno.Debounce)
这种写法只匹配带有 @Debounce
注解的方法
但不会将注解实例绑定到切面方法的参数
方法参数:Debounce debounceAnnotation
期望接收注解实例
但由于切点表达式没有指定绑定参数,所以 debounceAnnotation
始终为 null
java
复制
下载
String key = debounceAnnotation.key(); // debounceAnnotation 为 null long delay = debounceAnnotation.value(); // 抛出 NullPointerException
当代码执行到 debounceAnnotation.key()
时
因为 debounceAnnotation
是 null
抛出 NullPointerException
异常可能被全局异常处理器捕获或日志忽略
最终表现:防抖功能未生效,没有看到预期的防抖行为
java
复制
下载
@Around("@annotation(debounceAnnotation)") public Object debounce(ProceedingJoinPoint joinPoint, Debounce debounceAnnotation) throws Throwable { // 修复:表达式中的标识符与参数名一致 System.out.println("=== 防抖切面进入 ==="); // 添加空检查更安全 if (debounceAnnotation == null) { System.err.println("警告:防抖注解未获取到,直接执行方法"); return joinPoint.proceed(); } String key = debounceAnnotation.key(); long delay = debounceAnnotation.value(); // ... 其余逻辑不变 }
为什么有效:
@annotation(debounceAnnotation)
中的 debounceAnnotation
与参数名匹配
AspectJ 会自动将注解实例注入到同名参数
参数 debounceAnnotation
不再为 null
java
复制
下载
@Around("@annotation(com.weiyu.anno.Debounce)") public Object debounce(ProceedingJoinPoint joinPoint) throws Throwable { // 移除参数 System.out.println("=== 防抖切面进入 ==="); // 通过方法签名获取注解 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); Debounce debounceAnnotation = method.getAnnotation(Debounce.class); if (debounceAnnotation == null) { System.err.println("警告:防抖注解未获取到,直接执行方法"); return joinPoint.proceed(); } String key = debounceAnnotation.key(); long delay = debounceAnnotation.value(); // ... 其余逻辑不变 }
java
复制
下载
package com.weiyu.aop; import com.weiyu.anno.Debounce; 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.stereotype.Component; import java.lang.reflect.Method; /** * 防抖切面类 AOP */ @Component @Aspect public class DebounceAspect { @Autowired private DebounceUtil debounceUtil; /** * 修复后的防抖切面 */ @Around("@annotation(debounceAnnotation)") // 关键修改:匹配参数名 public Object debounce(ProceedingJoinPoint joinPoint, Debounce debounceAnnotation) throws Throwable { System.out.println("=== 防抖切面进入 ==="); // 安全保护:注解为空时直接放行 if (debounceAnnotation == null) { System.err.println("⚠️ 防抖注解实例未获取到,直接执行方法"); return joinPoint.proceed(); } // 从注解中获取 key String key = debounceAnnotation.key(); // 从注解中获取防抖时间 long delay = debounceAnnotation.value(); // 处理空key情况(重要!) if (key == null || key.isEmpty()) { // 生成默认key:类名+方法名 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); key = signature.getDeclaringTypeName() + "#" + signature.getName(); } System.out.println("防抖参数: key=" + key + ", delay=" + delay + "ms"); // 防抖检查 if (!debounceUtil.checkAndSet(key, delay)) { System.out.println("防抖触发: 拒绝请求"); throw new RuntimeException("操作过于频繁,请稍后再试"); } System.out.println("防抖检查通过,执行方法"); return joinPoint.proceed(); } }
切点表达式修正:
使用 @annotation(debounceAnnotation)
确保参数绑定
空注解保护:
添加 if (debounceAnnotation == null)
检查
防止空指针异常导致整个请求失败
空key处理:
当注解的 key()
为空时自动生成唯一key
使用 类名#方法名
作为默认标识符
避免所有方法共享同一个防抖锁
增强日志:
添加详细的执行过程日志
方便调试和问题排查
在 Controller 中添加测试方法:
java
复制
下载
@RestController public class TestController { @Debounce(key = "testKey", value = 3000) @GetMapping("/test") public String test() { System.out.println("业务方法执行"); return "success"; } }
测试步骤:
快速连续调用两次 /test
接口
第一次应该成功
第二次应该收到 "操作过于频繁" 错误
观察控制台日志输出
预期日志:
text
复制
下载
=== 防抖切面进入 === 防抖参数: key=testKey, delay=3000ms 防抖检查通过,执行方法 业务方法执行 === 防抖切面进入 === 防抖参数: key=testKey, delay=3000ms 防抖触发: 拒绝请求
在您原始的代码中,这行日志可能掩盖了问题:
java
复制
下载
System.out.println("key = " + key + ", delay = " + delay);
当 debounceAnnotation
为 null 时:
java
复制
下载
String key = debounceAnnotation.key(); // 抛出 NullPointerException
这个异常可能是:
被全局异常处理器捕获了
被框架吞掉了
在日志中被忽略
您可以添加空检查来验证:
java
复制
下载
@Around("@annotation(com.weiyu.anno.Debounce)") public Object debounce(ProceedingJoinPoint joinPoint, Debounce debounceAnnotation) throws Throwable { if (debounceAnnotation == null) { System.err.println("⚠️ 注解参数为null!"); return joinPoint.proceed(); } // ... }
您遇到的问题是由于 AspectJ 的参数绑定规则导致的:
@annotation(全限定类名)
只匹配注解类型,不绑定实例
@annotation(参数名)
同时匹配注解类型并将实例绑定到同名参数
参数名必须完全匹配(包括大小写)
这个看似微小的语法差异(com.weiyu.anno.Debounce
vs debounceAnnotation
)实际上完全改变了切面的行为方式。这也是为什么 AspectJ 切面开发时需要特别注意参数绑定的原因。
全局异常处理:
java
复制
下载
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public ResponseEntityhandleRuntimeException(RuntimeException e) { if ("操作过于频繁,请稍后再试".equals(e.getMessage())) { return ResponseEntity.status(429).body(e.getMessage()); // HTTP 429 Too Many Requests } return ResponseEntity.status(500).body("服务器错误"); } }
SpEL表达式支持(可选增强):
java
复制
下载
// 在获取key后添加 if (key.contains("#")) { key = parseSpelExpression(joinPoint, key); } // SpEL解析方法 private String parseSpelExpression(ProceedingJoinPoint joinPoint, String expression) { // 实现SpEL解析逻辑 }
防抖工具类检查:
确保 DebounceUtil.checkAndSet()
是线程安全的
推荐使用 ConcurrentHashMap
存储最后一次执行时间
这个修复方案解决了切面参数绑定的核心问题,同时增强了代码的健壮性和可维护性。