Vue3+Spring boot 前后端防抖增强方案

发布于:2025-06-25 ⋅ 阅读:(24) ⋅ 点赞:(0)

我将基于您提供的防抖切面代码进行改进,主要优化点包括使用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);
      }
    }
    // ... 其他错误处理
  }
);

改进亮点

  1. 分布式支持

    • 使用Redis实现分布式防抖锁

    • 原子操作(SETNX + EXPIRE)确保并发安全

  2. 灵活的键策略

    • 支持方法、用户、IP、参数、自定义五种键类型

    • 集成SpEL表达式引擎实现动态键生成

  3. 增强的异常处理

    • 自定义防抖异常类型(DebounceException)

    • 统一使用429状态码(Too Many Requests)

    • 可自定义错误消息

  4. 注解增强

    • 支持方法继承的注解查找

    • 可配置防抖时间和错误消息

    • 支持SpEL表达式自定义键

  5. 集成Spring生态

    • 自动获取请求上下文(IP、用户信息)

    • 通过全局异常处理器统一处理

  6. 更精确的防抖

    • 精确到毫秒级别的防抖控制

    • 避免本地内存防抖的分布式问题

这个改进方案保持了原有代码的结构和风格,同时增加了企业级应用所需的分布式支持和灵活性,特别适合在微服务架构中使用。


网站公告

今日签到

点亮在社区的每一天
去签到