From e3556e0a873a17fc3975ac0626e09dc5220f123e Mon Sep 17 00:00:00 2001 From: lbw Date: Thu, 30 Dec 2021 14:13:12 +0800 Subject: [PATCH] =?UTF-8?q?:sparkles:=20Introducing=20new=20features.=20cl?= =?UTF-8?q?ose=20#3514=20common-idempotent=20=E5=B9=82=E7=AD=89=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pigx-common/pigx-common-idempotent/pom.xml | 41 ++++++ .../IdempotentAutoConfiguration.java | 41 ++++++ .../idempotent/annotation/Idempotent.java | 44 ++++++ .../idempotent/aspect/IdempotentAspect.java | 135 ++++++++++++++++++ .../exception/IdempotentException.java | 32 +++++ .../expression/ExpressionResolver.java | 66 +++++++++ .../idempotent/expression/KeyResolver.java | 22 +++ .../main/resources/META-INF/spring.factories | 2 + 8 files changed, 383 insertions(+) create mode 100644 pigx-common/pigx-common-idempotent/pom.xml create mode 100644 pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/IdempotentAutoConfiguration.java create mode 100644 pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/annotation/Idempotent.java create mode 100644 pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/aspect/IdempotentAspect.java create mode 100644 pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/exception/IdempotentException.java create mode 100644 pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/expression/ExpressionResolver.java create mode 100644 pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/expression/KeyResolver.java create mode 100644 pigx-common/pigx-common-idempotent/src/main/resources/META-INF/spring.factories diff --git a/pigx-common/pigx-common-idempotent/pom.xml b/pigx-common/pigx-common-idempotent/pom.xml new file mode 100644 index 000000000..138424fe7 --- /dev/null +++ b/pigx-common/pigx-common-idempotent/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + + com.pig4cloud + pigx-common + 4.2.0 + + + pigx-common-idempotent + jar + + pigx 幂等插件 + + + 3.16.7 + + + + + + org.springframework.boot + spring-boot-starter-aop + provided + + + + + org.redisson + redisson-spring-boot-starter + ${redisson.version} + + + + + jakarta.servlet + jakarta.servlet-api + + + diff --git a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/IdempotentAutoConfiguration.java b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/IdempotentAutoConfiguration.java new file mode 100644 index 000000000..451c9f499 --- /dev/null +++ b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/IdempotentAutoConfiguration.java @@ -0,0 +1,41 @@ +package com.pig4cloud.pigx.common.idempotent; + +import com.pig4cloud.pigx.common.idempotent.aspect.IdempotentAspect; +import com.pig4cloud.pigx.common.idempotent.expression.ExpressionResolver; +import com.pig4cloud.pigx.common.idempotent.expression.KeyResolver; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author lengleng + * @date 2020/9/25 + *

+ * 幂等插件初始化 + */ +@Configuration(proxyBeanMethods = false) +@AutoConfigureAfter(RedisAutoConfiguration.class) +public class IdempotentAutoConfiguration { + + /** + * 切面 拦截处理所有 @Idempotent + * @return Aspect + */ + @Bean + public IdempotentAspect idempotentAspect() { + return new IdempotentAspect(); + } + + /** + * key 解析器 + * @return KeyResolver + */ + @Bean + @ConditionalOnMissingBean(KeyResolver.class) + public KeyResolver keyResolver() { + return new ExpressionResolver(); + } + +} diff --git a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/annotation/Idempotent.java b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/annotation/Idempotent.java new file mode 100644 index 000000000..b82cdf7fc --- /dev/null +++ b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/annotation/Idempotent.java @@ -0,0 +1,44 @@ +package com.pig4cloud.pigx.common.idempotent.annotation; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + +/** + * @author ITyunqing + */ +@Inherited +@Target(ElementType.METHOD) +@Retention(value = RetentionPolicy.RUNTIME) +public @interface Idempotent { + + /** + * 幂等操作的唯一标识,使用spring el表达式 用#来引用方法参数 + * @return Spring-EL expression + */ + String key() default ""; + + /** + * 有效期 默认:1 有效期要大于程序执行时间,否则请求还是可能会进来 + * @return expireTime + */ + int expireTime() default 1; + + /** + * 时间单位 默认:s + * @return TimeUnit + */ + TimeUnit timeUnit() default TimeUnit.SECONDS; + + /** + * 提示信息,可自定义 + * @return String + */ + String info() default "重复请求,请稍后重试"; + + /** + * 是否在业务完成后删除key true:删除 false:不删除 + * @return boolean + */ + boolean delKey() default false; + +} diff --git a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/aspect/IdempotentAspect.java b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/aspect/IdempotentAspect.java new file mode 100644 index 000000000..ab1b4b423 --- /dev/null +++ b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/aspect/IdempotentAspect.java @@ -0,0 +1,135 @@ +package com.pig4cloud.pigx.common.idempotent.aspect; + +import com.pig4cloud.pigx.common.idempotent.annotation.Idempotent; +import com.pig4cloud.pigx.common.idempotent.expression.KeyResolver; +import com.pig4cloud.pigx.common.idempotent.exception.IdempotentException; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.After; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +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.time.LocalDateTime; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * The Idempotent Aspect + * + * @author ITyunqing + */ +@Aspect +public class IdempotentAspect { + + private static final Logger LOGGER = LoggerFactory.getLogger(IdempotentAspect.class); + + private static final ThreadLocal> THREAD_CACHE = ThreadLocal.withInitial(HashMap::new); + + private static final String RMAPCACHE_KEY = "idempotent"; + + private static final String KEY = "key"; + + private static final String DELKEY = "delKey"; + + @Autowired + private Redisson redisson; + + @Autowired + private KeyResolver keyResolver; + + @Pointcut("@annotation(com.pig4cloud.pigx.common.idempotent.annotation.Idempotent)") + public void pointCut() { + } + + @Before("pointCut()") + public void beforePointCut(JoinPoint joinPoint) { + ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder + .getRequestAttributes(); + HttpServletRequest request = requestAttributes.getRequest(); + + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + if (!method.isAnnotationPresent(Idempotent.class)) { + return; + } + Idempotent idempotent = method.getAnnotation(Idempotent.class); + + String key; + + // 若没有配置 幂等 标识编号,则使用 url + 参数列表作为区分 + if (!StringUtils.hasLength(idempotent.key())) { + String url = request.getRequestURL().toString(); + String argString = Arrays.asList(joinPoint.getArgs()).toString(); + key = url + argString; + } + else { + // 使用jstl 规则区分 + key = keyResolver.resolver(idempotent, joinPoint); + } + + long expireTime = idempotent.expireTime(); + String info = idempotent.info(); + TimeUnit timeUnit = idempotent.timeUnit(); + boolean delKey = idempotent.delKey(); + + // do not need check null + RMapCache rMapCache = redisson.getMapCache(RMAPCACHE_KEY); + String value = LocalDateTime.now().toString().replace("T", " "); + Object v1; + if (null != rMapCache.get(key)) { + // had stored + throw new IdempotentException("[idempotent]:" + info); + } + synchronized (this) { + v1 = rMapCache.putIfAbsent(key, value, expireTime, timeUnit); + if (null != v1) { + throw new IdempotentException("[idempotent]:" + info); + } + else { + LOGGER.info("[idempotent]:has stored key={},value={},expireTime={}{},now={}", key, value, expireTime, + timeUnit, LocalDateTime.now().toString()); + } + } + + Map map = THREAD_CACHE.get(); + map.put(KEY, key); + map.put(DELKEY, delKey); + } + + @After("pointCut()") + public void afterPointCut(JoinPoint joinPoint) { + Map map = THREAD_CACHE.get(); + if (CollectionUtils.isEmpty(map)) { + return; + } + + RMapCache mapCache = redisson.getMapCache(RMAPCACHE_KEY); + if (mapCache.size() == 0) { + return; + } + + String key = map.get(KEY).toString(); + boolean delKey = (boolean) map.get(DELKEY); + + if (delKey) { + mapCache.fastRemove(key); + LOGGER.info("[idempotent]:has removed key={}", key); + } + THREAD_CACHE.remove(); + } + +} diff --git a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/exception/IdempotentException.java b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/exception/IdempotentException.java new file mode 100644 index 000000000..8ed04dee8 --- /dev/null +++ b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/exception/IdempotentException.java @@ -0,0 +1,32 @@ +package com.pig4cloud.pigx.common.idempotent.exception; + +/** + * Idempotent Exception If there is a custom global exception, you need to inherit the + * custom global exception. + * + * @author ITyunqing + */ +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); + } + +} diff --git a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/expression/ExpressionResolver.java b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/expression/ExpressionResolver.java new file mode 100644 index 000000000..bcbff8566 --- /dev/null +++ b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/expression/ExpressionResolver.java @@ -0,0 +1,66 @@ +package com.pig4cloud.pigx.common.idempotent.expression; + +/** + * @author lengleng + * @date 2020/9/25 + */ + +import com.pig4cloud.pigx.common.idempotent.annotation.Idempotent; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; + +/** + * @author lengleng + *

+ * 默认key 抽取, 优先根据 spel 处理 + * @date 2020-09-25 + */ +public class ExpressionResolver implements KeyResolver { + + private static final SpelExpressionParser PARSER = new SpelExpressionParser(); + + private static final LocalVariableTableParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer(); + + @Override + public String resolver(Idempotent idempotent, JoinPoint point) { + Object[] arguments = point.getArgs(); + String[] params = DISCOVERER.getParameterNames(getMethod(point)); + StandardEvaluationContext context = new StandardEvaluationContext(); + + if (params != null && params.length > 0) { + for (int len = 0; len < params.length; len++) { + context.setVariable(params[len], arguments[len]); + } + } + + Expression expression = PARSER.parseExpression(idempotent.key()); + return expression.getValue(context, String.class); + } + + /** + * 根据切点解析方法信息 + * @param joinPoint 切点信息 + * @return Method 原信息 + */ + private Method getMethod(JoinPoint joinPoint) { + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = 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); + } + } + return method; + } + +} diff --git a/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/expression/KeyResolver.java b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/expression/KeyResolver.java new file mode 100644 index 000000000..715b9ea00 --- /dev/null +++ b/pigx-common/pigx-common-idempotent/src/main/java/com/pig4cloud/pigx/common/idempotent/expression/KeyResolver.java @@ -0,0 +1,22 @@ +package com.pig4cloud.pigx.common.idempotent.expression; + +import com.pig4cloud.pigx.common.idempotent.annotation.Idempotent; +import org.aspectj.lang.JoinPoint; + +/** + * @author lengleng + * @date 2020/9/25 + *

+ * 唯一标志处理器 + */ +public interface KeyResolver { + + /** + * 解析处理 key + * @param idempotent 接口注解标识 + * @param point 接口切点信息 + * @return 处理结果 + */ + String resolver(Idempotent idempotent, JoinPoint point); + +} diff --git a/pigx-common/pigx-common-idempotent/src/main/resources/META-INF/spring.factories b/pigx-common/pigx-common-idempotent/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..980edcd10 --- /dev/null +++ b/pigx-common/pigx-common-idempotent/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.pig4cloud.pigx.common.idempotent.IdempotentAutoConfiguration