自用记录,欢迎大佬指正!
环境:
jdk1.8
spring boot 2.7.11
redis
话不多说,上代码。
pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- aop-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<!-- 过滤lettuce,使用jedis作为redis客户端 lettuce会自动断开,不知道怎么处理-->
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 解决Springboot Redis command timed out 问题-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- 数字签名算法SHA1-->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!--json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
application.yml
spring:
redis:
database: 9
host: 127.0.0.1
password: password
port: 6379
timeout: 5000
jedis:
pool:
max-active: 8
max-idle: 8
max-wait: -1
min-idle: 0
全局异常处理处理器:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 全局异常处理
*/
@RestControllerAdvice(annotations = {RestController.class, Controller.class})
public class GlobalExceptionHandler {
/**
* 指定拦截那一中类型
* @param ex 类型
* @return m
*/
@ExceptionHandler(RuntimeException.class)
public String exceptionHandler(RuntimeException ex){
return new Exception().getStackTrace()[1].getMethodName()+"方法出错,"+ex.getMessage();
}
}
自定义注解,加上这个注解的接口,才会限制重复访问。
import java.lang.annotation.*;
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 防重复操作限时标记数值(存储redis限时标记数值)
*/
String value() default "value" ;
/**
* 防重复操作过期时间(借助redis实现限时控制)
*/
long expireSeconds() default 10;
}
自定义拦截器
根据redisKey进行判断,是否拦截。
redisKey组成:
自己随便定义的前缀:PREVENT_DUPLICATION_PREFIX
请求的URL:request.getRequestURI()
接口请求的所有参数:request.getParameterMap()
SHA1加密的方法信息:getMethodSign()
重复提交判断依据:
通过redisTemplate.opsForValue().set()添加请求的信息到redis。设置key的失效时间,失效时间在注解上设置。annotation.expireSeconds()就是获取的@RepeatSubmit(expireSeconds = 2)这里,代表这个key2秒后自动删除。2秒后就可以访问了。
import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.digest.DigestUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
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.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
@Component
@Aspect
public class NoRepeatSubmitAspect {
@Resource
private RedisTemplate<String,Object> redisTemplate;
/**
* 定义切点
*/
@Pointcut("@annotation(com.example.repeat1.repeatSub.RepeatSubmit)")
public void preventDuplication() {}
@Around("preventDuplication()")
public Object around(ProceedingJoinPoint joinPoint){
/*
* 获取请求信息
*/
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
// 获取执行方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取防重复提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 获取token以及方法标记,生成redisKey和redisValue
String token = JSONObject.toJSONString(request.getParameterMap());
String url = request.getRequestURI();
// redisKey的前缀
String PREVENT_DUPLICATION_PREFIX = "PREVENT_DUPLICATION_PREFIX:";
/*
* 通过前缀 + url + token + 函数参数签名 来生成redis上的 key
*/
String redisKey = PREVENT_DUPLICATION_PREFIX
.concat(url)
.concat(token)
.concat(getMethodSign(method, joinPoint.getArgs()));
// 这个值只是为了标记,不重要
String redisValue = redisKey.concat(annotation.value()).concat("submit duplication");
if (Boolean.FALSE.equals(redisTemplate.hasKey(redisKey))) {
// 设置防重复操作限时标记(前置通知)
redisTemplate.opsForValue().set(redisKey, redisValue, annotation.expireSeconds(), TimeUnit.SECONDS);
try {
//正常执行方法并返回
//ProceedingJoinPoint类型参数可以决定是否执行目标方法,
// 且环绕通知必须要有返回值,返回值即为目标方法的返回值
return joinPoint.proceed();
} catch (Throwable throwable) {
//确保方法执行异常实时释放限时标记(异常后置通知)
redisTemplate.delete(redisKey);
throw new RuntimeException(throwable);
}
} else {
// 重复提交了抛出异常,如果是在项目中,根据具体情况处理。
throw new RuntimeException("请勿重复提交");
}
}
/**
* 生成方法标记:采用数字签名算法SHA1对方法签名字符串加签
*
* @param method 1
* @param args 1
* @return 1
*/
private String getMethodSign(Method method, Object... args) {
StringBuilder sb = new StringBuilder(method.toString());
for (Object arg : args) {
sb.append(JSONObject.toJSONString(arg));
}
return DigestUtils.sha1Hex(sb.toString());
}
}
TestController
import com.example.repeat1.repeatSub.RepeatSubmit;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {
@GetMapping
@RepeatSubmit(expireSeconds = 2)
public String get1(){
return "get1";
}
}
最后附上目录:
请求效果:
正常访问:
2秒内再次访问:
上面说的aop需要用到包有点多,现在更新一个依赖少的,功能差不多,可能效率比redis要低点:
package com.sky.common.submit;
import com.alibaba.fastjson.JSONObject;
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.stereotype.Component;
import org.springframework.util.DigestUtils;
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.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Aspect
public class NoRepeatSubmitAspect {
// 本地缓存,用于存储请求防重复提交的标记
private final Map<String, String> preventDuplicationCache = new ConcurrentHashMap<>();
// 这个用于记录过期时间的
private final Map<String, Long> expirationMap = new ConcurrentHashMap<>();
@Around("@annotation(repeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit repeatSubmit) {
/*
* 获取请求信息
*/
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
assert attributes != null;
HttpServletRequest request = attributes.getRequest();
// 获取执行方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
// 获取防重复提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
// 获取token以及方法标记,生成缓存的 key
String token = JSONObject.toJSONString(request.getParameterMap());
String url = request.getRequestURI();
/*
* 通过前缀 + url + token + 函数参数签名来生成缓存上的 key
*/
String cacheKey = getCacheKey(url, token, method, joinPoint.getArgs());
// 这个值只是为了标记,不重要
String cacheValue = cacheKey.concat(annotation.value()).concat("submit duplication");
synchronized (preventDuplicationCache) {
// 判断过期就删除key 如果使用redis就不需要这步,直接set的时候就可以设置过期时间。
if ((expirationMap.containsKey(cacheKey)
&& expirationMap.get(cacheKey) <= System.currentTimeMillis())) {
preventDuplicationCache.remove(cacheKey);
}
if (!preventDuplicationCache.containsKey(cacheKey)) {
// 设置防重复操作限时标记和超时时间
putWithExpiration(cacheKey, cacheValue, Long.parseLong(annotation.value()));
try {
// 正常执行方法并返回
// ProceedingJoinPoint类型参数可以决定是否执行目标方法,
// 且环绕通知必须要有返回值,返回值即为目标方法的返回值
return joinPoint.proceed();
} catch (Throwable throwable) {
// 确保方法执行异常实时释放限时标记(异常后置通知)
preventDuplicationCache.remove(cacheKey);
throw new RuntimeException(throwable);
}
}
throw new RuntimeException("请勿重复提交");
}
}
/**
* 生成缓存的 key:采用数字签名算法SHA1对方法签名字符串加签
*
* @param url 请求的 URL
* @param token 请求的 Token
* @param method 方法
* @param args 方法参数
* @return 缓存的 key
*/
private String getCacheKey(String url, String token, Method method, Object... args) {
StringBuilder sb = new StringBuilder(url).append(token).append(method.toString());
for (Object arg : args) {
sb.append(JSONObject.toJSONString(arg));
}
return DigestUtils.md5DigestAsHex(sb.toString().getBytes());
}
/**
* 设置判断重复访问的key和过期时间
*
* @param key 1
* @param value 1
* @param expiration 过期时间 秒
*/
public void putWithExpiration(String key, String value, long expiration) {
preventDuplicationCache.put(key, value);
long expireTime = System.currentTimeMillis() + expiration * 1000;
expirationMap.put(key, expireTime);
}
}
本人是面向百度编程,有什么不足欢迎大佬指正。
最后如果对你有一点点帮助,麻烦支持一下。
全国寄快递5元起,电影票8.8折。更多优惠微信关注公众号:【折价寄件】
感谢阅读!!!!