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;
    }

}
相关推荐
xing-xing1 小时前
Spring Data Elasticsearch
后端·spring·elasticsearch
今天你TLE了吗1 小时前
JVM学习笔记:第五章——堆内存
java·jvm·笔记·后端·学习
竟未曾年少轻狂1 小时前
JavaScript 对象与数组
java·前端·javascript·数组·对象
摸鱼的春哥2 小时前
春哥的Agent通关秘籍10:本地RAG实战(上)
前端·javascript·后端
Hx_Ma162 小时前
回显逻辑详解
java
彭于晏Yan2 小时前
LangChain4j实战二:集成到Springboot
java·spring boot·后端·langchain
fengtangjiang2 小时前
nacos服务之间相互调用
android·java·开发语言
石牌桥网管2 小时前
正则表达式:匹配不包含指定字符串的文本
java·javascript·python·正则表达式·go·php
独隅2 小时前
macOS 查看与安装 Java JDK 全面指南(2026年版)
java·开发语言·macos