一般遇见这种需求,大体思路思路我想基本是这样的,1.自定义一个spring-boot-starter2.启动一个拦截器实现拦截自定义注解3.根据注解的一些属性进行拼接一个key4.判断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.goswanorient-common3.9.04.0.0basal-common-idempotentorg.redissonredisson-spring-boot-startercn.goswanorient-common-data
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";@Autowiredprivate 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 }
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 {@Beanpublic 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