一般遇见这种需求,大体思路思路我想基本是这样的, 1.自定义一个spring-boot-starter 2.启动一个拦截器实现拦截自定义注解 3.根据注解的一些属性进行拼接一个key 4.判断key是否存在 4.1 不存在 存入redis,然后设置一个过期时间(一般过期时间也是注解的一个属性) 4.2 存在则抛出一个重复提交异常
闲话少说,先来一个使用端代码以及结果
使用方式
key = "T(cn.goswan.orient.common.security.util.SecurityUtils).getUser().getUsername()+#test.id"
这部分 的key就是拦截器里面用到的判断的key,具体可以根据自己业务用el表达式去定义
我用的是class fullpanth+用户名+业务主键 当作判定key
expireTime = 3
设置为了 3
timeUnit = TimeUnit.SECONDS
设置为了秒,即为3秒后这个key从缓存中消失,使用端一定注意这个时常一定要大于自己的业务处理耗时
好了下面上结果,连续发送两次请求(postman 发送)第一次请求并没有报错
第二次请求抛出如下错误(自定义的错误)
exception.IdempotentException: classUrl public cn.goswan.orient.common.core.util.R com..demo.controller.TestController.save(com.demo.entity.Test) not allow repeat submit
好了,说了这么多,下面上源码
目录结构
pom 文件(这里的comm-data实际上内部是对redis 的引用配置可以忽略,大家可以替换成自己的redis 配置即可,如果有不明白的可以看看我之前的文件,redis templete 哨兵配置代码参考一下)
cn.goswan orient-common3.9.0 4.0.0 basal-common-idempotentorg.redisson redisson-spring-boot-startercn.goswan orient-common-data
Idempotent.java
package com.basal.common.idempotent.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String key() default "";
//
// boolean isWorkonAll() default false;
int expireTime() default 1;
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
IdempotentAspect.java
package com.basal.common.idempotent.aspect;
import cn.goswan.orient.common.data.util.StringUtils;
import com.basal.common.idempotent.annotation.Idempotent;
import com.basal.common.idempotent.exception.IdempotentException;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.Redisson;
import org.redisson.api.RMapCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.spel.standard.Spelexpression;
import org.springframework.expression.spel.standard.SpelexpressionParser;
import org.springframework.expression.spel.support.StandardevaluationContext;
import java.lang.reflect.Method;
import java.util.Objects;
@Aspect
public class IdempotentAspect {
final SpelexpressionParser PARSER = new SpelexpressionParser();
final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
private static final String RMAPCACHE_KEY = "idempotent";
@Autowired
private Redisson redisson;
@Pointcut("@annotation(com.basal.common.idempotent.annotation.Idempotent)")
public void pointCut() {
}
@Before("pointCut()")
public void beforeCut(JoinPoint joinPoint) {
//获取切面拦截的方法
Object[] arguments = joinPoint.getArgs();
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
if (!methodSignature.getMethod().isAnnotationPresent(Idempotent.class)) {
return;
}
Method method = ((MethodSignature) signature).getMethod();
if (method.getDeclaringClass().isInterface()) {
try {
method = joinPoint.getTarget().getClass().getDeclaredMethod(joinPoint.getSignature().getName(),
method.getParameterTypes());
} catch (SecurityException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
//获取切面拦截的方法的参数并放入值context中
StandardevaluationContext context = new StandardevaluationContext();
String[] params = DISCOVERER.getParameterNames(method);
if (params != null && params.length > 0) {
for (int len = 0; len < params.length; len++) {
context.setVariable(params[len], arguments[len]);
}
}
//获取类全路径作为根key
String classUrl = method.toString();
Idempotent idempotent = methodSignature.getMethod().getAnnotation(Idempotent.class);
String idKey = "";
if (StringUtils.isEmpty(idempotent.key())) {
idKey = classUrl;
} else {
//将annotation中的key 获取到并通过spelexpression 转为具体值
Spelexpression spelexpression = PARSER.parseRaw(idempotent.key());
String key = spelexpression.getValue(context, String.class);
idKey = classUrl + key;
}
//判断map 中是否已经存在key
RMapCache rMapCache = redisson.getMapCache(RMAPCACHE_KEY);
//存在则抛出重复提交异常
if (rMapCache.containsKey(idKey)) {
throw new IdempotentException("classUrl " + classUrl + " not allow repeat submit ");
} else {
//不存在则存入cache map,如果存入过程中又有操作以至于存在key,则同样抛出异常
Object idObj = rMapCache.putIfAbsent(idKey, System.currentTimeMillis(), idempotent.expireTime(), idempotent.timeUnit());
if (Objects.nonNull(idObj)) {
throw new IdempotentException("classUrl " + classUrl + " not allow repeat submit ");
}
}
}
}
IdempotentConfig.java
package com.basal.common.idempotent.config;
import com.basal.common.idempotent.aspect.IdempotentAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class IdempotentConfig {
@Bean
public IdempotentAspect IdempotentAspect(){
IdempotentAspect idempotentAspect = new IdempotentAspect();
return idempotentAspect;
}
}
IdempotentException.java
package com.basal.common.idempotent.exception;
public class IdempotentException extends RuntimeException {
public IdempotentException() {
super();
}
public IdempotentException(String message) {
super(message);
}
public IdempotentException(String message, Throwable cause) {
super(message, cause);
}
public IdempotentException(Throwable cause) {
super(cause);
}
protected IdempotentException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.basal.common.idempotent.config.IdempotentConfig



