如何写一个防止表单重复提交的注解?

wen java案例 51

本文目录导读:

如何写一个防止表单重复提交的注解?

  1. 定义注解
  2. 创建切面实现
  3. 使用示例
  4. 优化版本(支持配置和自定义处理)
  5. 前端配合方案

我来帮你设计一个防止表单重复提交的注解方案,包含完整的实现。

定义注解

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PreventDuplicateSubmit {
    /**
     * 防重复提交的key前缀
     */
    String prefix() default "prevent_duplicate:";
    /**
     * 锁的超时时间,默认5秒
     */
    int timeout() default 5;
    /**
     * 时间单位,默认秒
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    /**
     * 请求频率限制,0表示不限制
     */
    int interval() default 0;
}

创建切面实现

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.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
public class PreventDuplicateSubmitAspect {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Around("@annotation(preventDuplicateSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable {
        // 获取请求信息
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        // 生成唯一key
        String key = generateKey(joinPoint, request, preventDuplicateSubmit.prefix());
        // 检查是否重复提交
        if (isDuplicateSubmit(key, preventDuplicateSubmit.timeout(), preventDuplicateSubmit.timeUnit())) {
            throw new RuntimeException("请勿重复提交请求");
        }
        // 执行原方法
        return joinPoint.proceed();
    }
    private String generateKey(ProceedingJoinPoint joinPoint, HttpServletRequest request, String prefix) {
        StringBuilder sb = new StringBuilder(prefix);
        // 获取类名和方法名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        sb.append(method.getDeclaringClass().getName())
          .append(".")
          .append(method.getName());
        // 获取用户标识(可以是用户ID、IP等)
        String userId = getUserId(request);
        sb.append(":").append(userId);
        // 获取请求参数
        Object[] args = joinPoint.getArgs();
        if (args != null && args.length > 0) {
            sb.append(":").append(generateParamKey(args));
        }
        return sb.toString();
    }
    private String getUserId(HttpServletRequest request) {
        // 从请求中获取用户标识,这里需要根据实际业务实现
        // 从token、session等中获取
        String token = request.getHeader("Authorization");
        if (token != null) {
            // 解析token获取用户ID
        }
        return request.getRemoteAddr(); // 默认使用IP
    }
    private String generateParamKey(Object[] args) {
        // 生成参数key,可以根据实际需要序列化
        StringBuilder sb = new StringBuilder();
        for (Object arg : args) {
            if (arg != null) {
                sb.append(arg.hashCode()).append("_");
            }
        }
        return sb.toString();
    }
    private boolean isDuplicateSubmit(String key, int timeout, TimeUnit timeUnit) {
        // 使用Redis的SETNX命令实现分布式锁
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(key, "1", timeout, timeUnit);
        return Boolean.FALSE.equals(success);
    }
}

使用示例

@RestController
@RequestMapping("/api/order")
public class OrderController {
    @PostMapping("/submit")
    @PreventDuplicateSubmit(prefix = "order_submit:", timeout = 5)
    public Result submitOrder(@RequestBody OrderDTO orderDTO) {
        // 处理订单逻辑
        return Result.success();
    }
    @PostMapping("/pay")
    @PreventDuplicateSubmit(prefix = "order_pay:", timeout = 10, interval = 3)
    public Result payOrder(@RequestBody PayDTO payDTO) {
        // 处理支付逻辑
        return Result.success();
    }
}

优化版本(支持配置和自定义处理)

import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PreventDuplicateSubmit {
    String prefix() default "prevent_duplicate:";
    int timeout() default 5;
    TimeUnit timeUnit() default TimeUnit.SECONDS;
    int interval() default 0;
    // 自定义处理器
    Class<? extends DuplicateSubmitHandler> handler() default DefaultDuplicateSubmitHandler.class;
    // 是否启用
    boolean enabled() default true;
}
// 处理器接口
public interface DuplicateSubmitHandler {
    /**
     * 处理重复提交
     * @param key 唯一标识
     * @param timeout 超时时间
     * @param timeUnit 时间单位
     * @return true表示是重复提交
     */
    boolean handleDuplicateSubmit(String key, int timeout, TimeUnit timeUnit);
    /**
     * 生成唯一key
     */
    String generateKey(ProceedingJoinPoint joinPoint, HttpServletRequest request);
    /**
     * 处理重复提交后的返回
     */
    Object handleDuplicateResponse(ProceedingJoinPoint joinPoint);
}
// 默认实现
@Component
public class DefaultDuplicateSubmitHandler implements DuplicateSubmitHandler {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Override
    public boolean handleDuplicateSubmit(String key, int timeout, TimeUnit timeUnit) {
        Boolean success = redisTemplate.opsForValue()
            .setIfAbsent(key, "1", timeout, timeUnit);
        return Boolean.FALSE.equals(success);
    }
    @Override
    public String generateKey(ProceedingJoinPoint joinPoint, HttpServletRequest request) {
        // 默认key生成逻辑
        return null;
    }
    @Override
    public Object handleDuplicateResponse(ProceedingJoinPoint joinPoint) {
        throw new BusinessException("请勿重复提交");
    }
}
// 增强的切面
@Aspect
@Component
public class EnhancedPreventDuplicateSubmitAspect {
    @Autowired
    private ApplicationContext applicationContext;
    @Around("@annotation(preventDuplicateSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, PreventDuplicateSubmit preventDuplicateSubmit) throws Throwable {
        if (!preventDuplicateSubmit.enabled()) {
            return joinPoint.proceed();
        }
        // 获取自定义处理器
        DuplicateSubmitHandler handler = getHandler(preventDuplicateSubmit.handler());
        // 生成key
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String key = handler.generateKey(joinPoint, request);
        // 检查重复提交
        if (handler.handleDuplicateSubmit(key, preventDuplicateSubmit.timeout(), preventDuplicateSubmit.timeUnit())) {
            return handler.handleDuplicateResponse(joinPoint);
        }
        return joinPoint.proceed();
    }
    private DuplicateSubmitHandler getHandler(Class<? extends DuplicateSubmitHandler> handlerClass) {
        if (handlerClass == DefaultDuplicateSubmitHandler.class) {
            return new DefaultDuplicateSubmitHandler();
        }
        return applicationContext.getBean(handlerClass);
    }
}

前端配合方案

// 前端防重复提交工具
class PreventDuplicateSubmit {
  constructor() {
    this.submitting = false;
    this.submitMap = new Map();
  }
  /**
   * 检查是否可以提交
   * @param {string} key 提交标识
   * @param {number} interval 间隔时间(毫秒)
   */
  canSubmit(key, interval = 1000) {
    const now = Date.now();
    const lastSubmit = this.submitMap.get(key);
    if (lastSubmit && (now - lastSubmit) < interval) {
      console.warn('提交过于频繁');
      return false;
    }
    this.submitMap.set(key, now);
    return true;
  }
  /**
   * 清除指定key的提交记录
   */
  clearSubmit(key) {
    this.submitMap.delete(key);
  }
  /**
   * 清除所有提交记录
   */
  clearAll() {
    this.submitMap.clear();
  }
}
// 使用示例
const preventDuplicate = new PreventDuplicateSubmit();
async function submitForm() {
  const submitKey = 'submit_order_' + JSON.stringify(formData);
  if (!preventDuplicate.canSubmit(submitKey, 2000)) {
    showToast('请勿重复提交');
    return;
  }
  try {
    const loading = showLoading();
    await api.submitOrder(formData);
    hideLoading(loading);
    showSuccess('提交成功');
  } catch (error) {
    preventDuplicate.clearSubmit(submitKey);
    showError(error.message);
  }
}

这个方案包含了完整的后端注解实现和前端配合方案,可以根据实际需求进行调整和扩展。

抱歉,评论功能暂时关闭!