springboot防抖 限流 幂等实现 AOP注解实现

概要

在实际应用中,我们会经常用到限流,防抖,幂等等操作,在服务端后台也经常用到

效果图

代码

注解

java 复制代码
import java.lang.annotation.*;

/**
 * 防重复提交注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
    /**
     * 有效期(毫秒), 默认5000秒
     */
    long expire() default 5000;
}

切面(前置通知)

java 复制代码
import com.cc672cc.aop.annotation.NoRepeatSubmit;
import com.cc672cc.common.constants.RedisPreKey;
import com.cc672cc.common.utils.ShiroUtils;
import com.cc672cc.enums.ExceptionEnum;
import com.cc672cc.exceptions.BusinessException;
import com.cc672cc.service.IRedisService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;

import static com.cc672cc.enums.ExceptionEnum.RATE_LIMIT_EXCEPTION;
import static com.cc672cc.enums.ExceptionEnum.SYSTEM_ERROE;

/**
 * 幂等拦截切面(重复提交校验)
 * 核心:基于Redis实现,通过用户ID+请求URI+参数MD5生成唯一标识,防止重复提交
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {

    @Autowired
    private IRedisService redisService;

    private static final String MD5 = "MD5";

    @Before("@annotation(noRepeatSubmit)")
    public void before(JoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) {
        // 1. 获取当前请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            throw new BusinessException(SYSTEM_ERROE.getCode(), "无法获取当前请求上下文");
        }
        HttpServletRequest request = attributes.getRequest();

        // 2. 获取当前登录用户ID(未登录则拒绝)
        Long userId = ShiroUtils.getCurLoginUserId();
        if (userId == null) {
            throw new BusinessException(ExceptionEnum.UNAUTHENTICATED);
        }
        // 3. 构建请求唯一标识(URI + 参数)
        String requestUri = request.getRequestURI();
        String args = Arrays.toString(joinPoint.getArgs());

        // 4. 生成参数MD5哈希(保证唯一且长度固定)
        String paramHash = generateMD5Hash(requestUri + args);
        if (StringUtils.isBlank(paramHash)) {
            throw new BusinessException(SYSTEM_ERROE.getCode(), "参数哈希生成失败");
        }

        // 5. 构建Redis幂等Key
        String redisKey = RedisPreKey.CAHCE_REPEAT_SUBMIT + userId + ":" + paramHash;

        // 6. 幂等校验核心逻辑(SETNX:不存在则设置,存在则抛出异常)
        Boolean exists = redisService.hasKey(redisKey);
        if (Boolean.TRUE.equals(exists)) {
            // 2. 获取剩余过期时间(毫秒),处理null值
            Long expireMillis = redisService.getExpire(redisKey, TimeUnit.MILLISECONDS);
            // 兜底:若返回null/负数,默认0毫秒
            expireMillis = (expireMillis == null || expireMillis < 0) ? 0L : expireMillis;

            // 3. 毫秒转秒,保留1位小数(四舍五入)
            double expireSeconds = expireMillis / 1000.0;
            // 四舍五入保留1位小数(解决原代码Math.round直接取整的问题)
            expireSeconds = Math.round(expireSeconds * 10) / 10.0;

            // 4. 抛限流异常,格式化显示1位小数
            throw new BusinessException(RATE_LIMIT_EXCEPTION.getCode(),
                    String.format("触发限流,请%.1fs后再试", expireSeconds));
        }

        // 7. 设置Redis锁(过期时间 = 注解配置的expire值)
        redisService.set(redisKey, "1", noRepeatSubmit.expire(), TimeUnit.MILLISECONDS);
        log.info("幂等拦截成功,用户ID:{},请求URI:{},RedisKey:{}", userId, requestUri, redisKey);
    }


    /**
     * 生成MD5哈希值
     */
    private String generateMD5Hash(String content) {
        if (StringUtils.isBlank(content)) {
            return "";
        }
        try {
            MessageDigest md = MessageDigest.getInstance(MD5);
            byte[] bytes = md.digest(content.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder();
            for (byte b : bytes) {
                sb.append(String.format("%02x", b));
            }
            return sb.toString();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("MD5算法不存在", e);
        }
    }
}

测试接口层

java 复制代码
public class PortalTestController {

    @GetMapping("/auth-hello")
    @Operation(summary = "认证hello")
    @NoRepeatSubmit(expire = 5000)
    public String authHello(@RequestParam(value = "word", required = false) String word) {
        return "authHello" + ",word=" + word;
    }

}
相关推荐
Java开发的小李几秒前
SpringBoot + Redis 实现分布式 Session 共享(解决多实例登录状态丢失问题)
spring boot·redis·分布式
Old Uncle Tom18 分钟前
OpenClaw 记忆系统 -- 记忆预加载
java·数据结构·算法·agent
小小小米粒30 分钟前
Collection单列集合、Map(Key - Value)双列集合,多继承实现。
java·开发语言·windows
前端一小卒1 小时前
我用 Claude Code 的 Superpowers 技能链写了个服务,部署前差点把服务器搞炸
前端·javascript·后端
摇滚侠1 小时前
expdp 查看帮助
java·数据库·oracle
:1212 小时前
java基础
java·开发语言
曹牧2 小时前
Spring:@RequestMapping注解,匹配的顺序与上下文无关
java·后端·spring
daixin88482 小时前
cursor无法正常使用gpt5.5等模型解决方案
java·redis·cursor
韦禾水3 小时前
记录一次项目部署到tomcat的异常
java·tomcat
曦月合一3 小时前
树莓派安装jdk、tomcat、vnc、谷歌浏览器开机自启等环境配置
java·tomcat·树莓派