一、前言
有时在项目开发中某些接口逻辑比较复杂,响应时间长,那么可能导致重复提交问题。
二、如何解决
1.先定义一个防重复提交的注解。
import java.lang.annotation.*;
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 防重复操作限时标记数值(存储redis限时标记数值)
*/
String value() default "value" ;
/**
* 防重复操作过期时间(借助redis实现限时控制)
*/
int expireSeconds() default 10;
}
2.编写防重复操作的AOP
import lombok.extern.slf4j.Slf4j;
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.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
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.Objects;
@Slf4j
@Component
@Aspect
@Order(0)
public class NoRepeatSubmitAspect {
private static final String TOKENAuthorization = "Authorization";
private static final String TOKENUSERNAME = "api-userName";
private static final String PREVENT_DUPLICATION_PREFIX = "PREVENT_DUPLICATION_PREFIX:";
@Autowired
private RedisService redisService;
@Pointcut("@annotation(com.dp.aop.annotation.RepeatSubmit)")
public void preventDuplication() {}
@Around("preventDuplication()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
if (Objects.isNull(request)) {
return joinPoint.proceed();
}
//获取执行方法
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
//获取防重复提交注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
//获取token以及方法标记,生成redisKey
String header = request.getHeader(TOKENAuthorization);
String token = header == null ? "" : header;
String requestHeader = request.getHeader(TOKENUSERNAME);
String headerToken = requestHeader == null ? "" : requestHeader;
token = token + headerToken;
String url = request.getRequestURI();
// 通过前缀 + url + token + 函数参数签名 来生成redis上的 key
String redisKey = PREVENT_DUPLICATION_PREFIX
.concat(url)
.concat(token)
.concat(getMethodSign(method, joinPoint.getArgs()));
RedisLock redisLock = null;
try {
try {
redisLock = redisService.tryLock(redisKey, annotation.expireSeconds());
} catch (Exception e) {
log.error("tryLock error ", e);
throw new BizException(CommonMsgConstants.NoRepeatSubmitMsg);
}
return joinPoint.proceed();
} catch (Throwable throwable) {
log.error("throwable trace is ", throwable);
throw new RuntimeException(throwable);
} finally {
if (Objects.nonNull(redisLock)) {
redisLock.unlock();
}
}
}
/**
* 生成方法标记:采用数字签名算法SHA1对方法签名字符串加签
*
* @param method
* @param args
* @return
*/
private String getMethodSign(Method method, Object... args) {
StringBuilder sb = new StringBuilder(method.toString());
for (Object arg : args) {
sb.append(toString(arg));
}
return DigestUtil.sha1Hex(sb.toString());
}
private String toString(Object arg) {
if (Objects.isNull(arg)) {
return "null";
}
if (arg instanceof Number) {
return arg.toString();
}
return JSONObject.toJSONString(arg);
}
}
3.接下来定义redisService类文章来源:https://www.toymoban.com/news/detail-669062.html
@Component
public class RedisService {
public RedisLock tryLock(String lockKey, int expireTime) {
String lockValue = UUID.randomUUID().toString();
Boolean hasLock = (Boolean)this.redisTemplate.execute((connection) -> {
Object nativeConnection = connection.getNativeConnection();
String status = null;
if (nativeConnection instanceof Jedis) {
Jedis jedis = (Jedis)nativeConnection;
status = jedis.set(lockKey, lockValue, "nx", "ex", expireTime);
} else {
JedisCluster jedisx = (JedisCluster)nativeConnection;
status = jedisx.set(lockKey, lockValue, "nx", "ex", (long)expireTime);
}
return "OK".equals(status);
});
if (hasLock) {
return new RedisService.RedisLockInner(this.redisTemplate, lockKey, lockValue);
} else {
throw new RuntimeException("获取锁失败,lockKey:" + lockKey);
}
}
private class RedisLockInner implements RedisLock {
private RedisTemplate redisTemplate;
private String key;
private String expectedValue;
protected RedisLockInner(RedisTemplate redisTemplate, String key, String expectedValue) {
this.redisTemplate = redisTemplate;
this.key = key;
this.expectedValue = expectedValue;
}
public Object unlock() {
final List<String> keys = new ArrayList();
keys.add(this.key);
final List<String> values = new ArrayList();
values.add(this.expectedValue);
Object result = this.redisTemplate.execute(new RedisCallback<Long>() {
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
return nativeConnection instanceof JedisCluster ? (Long)((JedisCluster)nativeConnection).eval("if redis.call('get',KEYS[1])==ARGV[1]\n then\n return redis.call('del',KEYS[1])\n else\n return 0\n end", keys, values) : (Long)((Jedis)nativeConnection).eval("if redis.call('get',KEYS[1])==ARGV[1]\n then\n return redis.call('del',KEYS[1])\n else\n return 0\n end", keys, values);
}
});
return result;
}
public void close() throws Exception {
this.unlock();
}
}
}
4.最后在Controller接口加上注解就行了。文章来源地址https://www.toymoban.com/news/detail-669062.html
到了这里,关于springboot aop实现接口防重复操作的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!