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

代码
注解
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;
}
}