一般遇见这种需求,大体思路思路我想基本是这样的,
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 哨兵配置代码参考一下)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.goswan</groupId>
<artifactId>orient-common</artifactId>
<version>3.9.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>basal-common-idempotent</artifactId>
<dependencies>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.goswan</groupId>
<artifactId>orient-common-data</artifactId>
</dependency>
</dependencies>
</project>
Idempotent.java
package com.basal.common.idempotent.annotation;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @Author alan.wang
* @date: 2021-12-30 17:54
* @desc: 定义注解
*/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
/**
* 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数
* @return Spring-EL expression
*/
String key() default "";
// /**
// * 是否作用域是所有请求(根据请求ip)
// * 默认:false
// * false:只做用在当前请求人(限定同意时间段只对当前访问ip拦截)
// * ture: 作用在所有人(同一时间对所有ip进行拦截)
// *
// * @return isWorkOnAll
// **/
// boolean isWorkOnAll() default false;
/**
* 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来
* @return expireTime
*/
int expireTime() default 1;
/**
* 时间单位 默认:s
* @return TimeUnit
*/
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;
/**
* @Author alan.wang
* @date: 2021-12-30 17:56
* @desc:
* 防止重复提交注解拦截器,具体流程就是拦截带@Idempotent的方法,然后从redis取出key
* 如果key 已经存在:抛出自定义异常
* 如果key不存在:则存入
*/
@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;
/**
* @Author alan.wang
* @date: 2022-01-03 11:35
* @desc: 将IdempotentAspect 拦截器注入到spring 容器中
*/
@Configuration
public class IdempotentConfig {
@Bean
public IdempotentAspect IdempotentAspect(){
IdempotentAspect idempotentAspect = new IdempotentAspect();
return idempotentAspect;
}
}
IdempotentException.java
package com.basal.common.idempotent.exception;
/**
* @Author alan.wang
* @date: 2022-01-04 15:26
* @desc: Idempotent 重复提交异常
*/
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