一般遇见这种需求,大体思路思路我想基本是这样的, 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 * * @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 * * @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 * * @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 * * @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