Эх сурвалжийг харах

基于 Redis 实现幂等性操作

YunaiV 4 жил өмнө
parent
commit
1c1f1c49fa

+ 1 - 0
README.md

@@ -45,6 +45,7 @@
 1. 代码生成:前后端代码的生成(Java、Vue、SQL),支持 CRUD 下载
 1. 系统接口:基于 Swagger 自动生成相关的 RESTful API 接口文档
 1. 数据库文档:基于 Screw 自动生成数据库文档
+1. 幂等组件:基于 Redis 实现幂等组件,解决重复请求问题
 
 ## 在线体验
 

+ 2 - 2
src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java

@@ -1,8 +1,8 @@
 package cn.iocoder.dashboard.framework.idempotent.config;
 
 import cn.iocoder.dashboard.framework.idempotent.core.aop.IdempotentAspect;
-import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.DefaultIdempotentKeyResolver;
-import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.ExpressionIdempotentKeyResolver;
+import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
+import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
 import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
 import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO;
 import org.springframework.boot.autoconfigure.AutoConfigureAfter;

+ 1 - 1
src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java

@@ -1,6 +1,6 @@
 package cn.iocoder.dashboard.framework.idempotent.core.annotation;
 
-import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.DefaultIdempotentKeyResolver;
+import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver;
 import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
 
 import java.lang.annotation.ElementType;

+ 0 - 19
src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/ExpressionIdempotentKeyResolver.java

@@ -1,19 +0,0 @@
-package cn.iocoder.dashboard.framework.idempotent.core.keyresolver;
-
-import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
-import org.aspectj.lang.JoinPoint;
-
-/**
- * 基于 Spring EL 表达式,
- *
- * @author 芋道源码
- */
-public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
-
-    @Override
-    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
-        // TODO 稍后实现
-        return null;
-    }
-
-}

+ 2 - 1
src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/DefaultIdempotentKeyResolver.java → src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java

@@ -1,8 +1,9 @@
-package cn.iocoder.dashboard.framework.idempotent.core.keyresolver;
+package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl;
 
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.crypto.SecureUtil;
 import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
+import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
 import org.aspectj.lang.JoinPoint;
 
 /**

+ 63 - 0
src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java

@@ -0,0 +1,63 @@
+package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
+import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
+import org.springframework.core.ParameterNameDiscoverer;
+import org.springframework.expression.Expression;
+import org.springframework.expression.ExpressionParser;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+
+import java.lang.reflect.Method;
+
+/**
+ * 基于 Spring EL 表达式,
+ *
+ * @author 芋道源码
+ */
+public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver {
+
+    private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
+    private final ExpressionParser expressionParser = new SpelExpressionParser();
+
+    @Override
+    public String resolver(JoinPoint joinPoint, Idempotent idempotent) {
+        // 获得被拦截方法参数名列表
+        Method method = getMethod(joinPoint);
+        Object[] args = joinPoint.getArgs();
+        String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method);
+        // 准备 Spring EL 表达式解析的上下文
+        StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
+        if (ArrayUtil.isNotEmpty(parameterNames)) {
+            for (int i = 0; i < parameterNames.length; i++) {
+                evaluationContext.setVariable(parameterNames[i], args[i]);
+            }
+        }
+
+        // 解析参数
+        Expression expression = expressionParser.parseExpression(idempotent.keyArg());
+        return expression.getValue(evaluationContext, String.class);
+    }
+
+    private static Method getMethod(JoinPoint point) {
+        // 处理,声明在类上的情况
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        Method method = signature.getMethod();
+        if (!method.getDeclaringClass().isInterface()) {
+            return method;
+        }
+
+        // 处理,声明在接口上的情况
+        try {
+            return point.getTarget().getClass().getDeclaredMethod(
+                    point.getSignature().getName(), method.getParameterTypes());
+        } catch (NoSuchMethodException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+}

+ 8 - 0
src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java

@@ -1,4 +1,12 @@
 /**
  * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现
+ * 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。
+ *
+ * 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。
+ *
+ * 和 it4alla/idempotent 组件的差异点,主要体现在两点:
+ *  1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力
+ * 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。
+ *  2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。
  */
 package cn.iocoder.dashboard.framework.idempotent;

+ 2 - 1
src/main/java/cn/iocoder/dashboard/modules/infra/controller/config/InfConfigController.java

@@ -5,6 +5,7 @@ import cn.iocoder.dashboard.common.pojo.CommonResult;
 import cn.iocoder.dashboard.common.pojo.PageResult;
 import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils;
 import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent;
+import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver;
 import cn.iocoder.dashboard.modules.infra.controller.config.vo.*;
 import cn.iocoder.dashboard.modules.infra.convert.config.InfConfigConvert;
 import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO;
@@ -92,7 +93,7 @@ public class InfConfigController {
     @PostMapping("/create")
 //    @PreAuthorize("@ss.hasPermi('infra:config:add')")
 //    @Log(title = "参数管理", businessType = BusinessType.INSERT)
-    @Idempotent(timeout = 10)
+    @Idempotent(timeout = 60, keyResolver = ExpressionIdempotentKeyResolver.class, keyArg = "#reqVO.key")
     public CommonResult<Long> createConfig(@Validated @RequestBody InfConfigCreateReqVO reqVO) {
         return success(configService.createConfig(reqVO));
     }