Commit 1c1f1c49fa897c576b595cec4cf5e77d34e6ae69
1 parent
8fa9ba8e
基于 Redis 实现幂等性操作
Showing
8 changed files
with
79 additions
and
24 deletions
README.md
| @@ -45,6 +45,7 @@ | @@ -45,6 +45,7 @@ | ||
| 45 | 1. 代码生成:前后端代码的生成(Java、Vue、SQL),支持 CRUD 下载 | 45 | 1. 代码生成:前后端代码的生成(Java、Vue、SQL),支持 CRUD 下载 |
| 46 | 1. 系统接口:基于 Swagger 自动生成相关的 RESTful API 接口文档 | 46 | 1. 系统接口:基于 Swagger 自动生成相关的 RESTful API 接口文档 |
| 47 | 1. 数据库文档:基于 Screw 自动生成数据库文档 | 47 | 1. 数据库文档:基于 Screw 自动生成数据库文档 |
| 48 | +1. 幂等组件:基于 Redis 实现幂等组件,解决重复请求问题 | ||
| 48 | 49 | ||
| 49 | ## 在线体验 | 50 | ## 在线体验 |
| 50 | 51 |
src/main/java/cn/iocoder/dashboard/framework/idempotent/config/IdempotentConfiguration.java
| 1 | package cn.iocoder.dashboard.framework.idempotent.config; | 1 | package cn.iocoder.dashboard.framework.idempotent.config; |
| 2 | 2 | ||
| 3 | import cn.iocoder.dashboard.framework.idempotent.core.aop.IdempotentAspect; | 3 | import cn.iocoder.dashboard.framework.idempotent.core.aop.IdempotentAspect; |
| 4 | -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.DefaultIdempotentKeyResolver; | ||
| 5 | -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.ExpressionIdempotentKeyResolver; | 4 | +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; |
| 5 | +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; | ||
| 6 | import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; | 6 | import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| 7 | import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO; | 7 | import cn.iocoder.dashboard.framework.idempotent.core.redis.IdempotentRedisDAO; |
| 8 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; | 8 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; |
src/main/java/cn/iocoder/dashboard/framework/idempotent/core/annotation/Idempotent.java
| 1 | package cn.iocoder.dashboard.framework.idempotent.core.annotation; | 1 | package cn.iocoder.dashboard.framework.idempotent.core.annotation; |
| 2 | 2 | ||
| 3 | -import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.DefaultIdempotentKeyResolver; | 3 | +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; |
| 4 | import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; | 4 | import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| 5 | 5 | ||
| 6 | import java.lang.annotation.ElementType; | 6 | import java.lang.annotation.ElementType; |
src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/ExpressionIdempotentKeyResolver.java deleted
| 1 | -package cn.iocoder.dashboard.framework.idempotent.core.keyresolver; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; | ||
| 4 | -import org.aspectj.lang.JoinPoint; | ||
| 5 | - | ||
| 6 | -/** | ||
| 7 | - * 基于 Spring EL 表达式, | ||
| 8 | - * | ||
| 9 | - * @author 芋道源码 | ||
| 10 | - */ | ||
| 11 | -public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { | ||
| 12 | - | ||
| 13 | - @Override | ||
| 14 | - public String resolver(JoinPoint joinPoint, Idempotent idempotent) { | ||
| 15 | - // TODO 稍后实现 | ||
| 16 | - return null; | ||
| 17 | - } | ||
| 18 | - | ||
| 19 | -} |
src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/DefaultIdempotentKeyResolver.java renamed to src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java
| 1 | -package cn.iocoder.dashboard.framework.idempotent.core.keyresolver; | 1 | +package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.util.StrUtil; | 3 | import cn.hutool.core.util.StrUtil; |
| 4 | import cn.hutool.crypto.SecureUtil; | 4 | import cn.hutool.crypto.SecureUtil; |
| 5 | import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; | 5 | import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; |
| 6 | +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; | ||
| 6 | import org.aspectj.lang.JoinPoint; | 7 | import org.aspectj.lang.JoinPoint; |
| 7 | 8 | ||
| 8 | /** | 9 | /** |
src/main/java/cn/iocoder/dashboard/framework/idempotent/core/keyresolver/impl/ExpressionIdempotentKeyResolver.java
0 → 100644
| 1 | +package cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.ArrayUtil; | ||
| 4 | +import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; | ||
| 5 | +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.IdempotentKeyResolver; | ||
| 6 | +import org.aspectj.lang.JoinPoint; | ||
| 7 | +import org.aspectj.lang.reflect.MethodSignature; | ||
| 8 | +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; | ||
| 9 | +import org.springframework.core.ParameterNameDiscoverer; | ||
| 10 | +import org.springframework.expression.Expression; | ||
| 11 | +import org.springframework.expression.ExpressionParser; | ||
| 12 | +import org.springframework.expression.spel.standard.SpelExpressionParser; | ||
| 13 | +import org.springframework.expression.spel.support.StandardEvaluationContext; | ||
| 14 | + | ||
| 15 | +import java.lang.reflect.Method; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * 基于 Spring EL 表达式, | ||
| 19 | + * | ||
| 20 | + * @author 芋道源码 | ||
| 21 | + */ | ||
| 22 | +public class ExpressionIdempotentKeyResolver implements IdempotentKeyResolver { | ||
| 23 | + | ||
| 24 | + private final ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); | ||
| 25 | + private final ExpressionParser expressionParser = new SpelExpressionParser(); | ||
| 26 | + | ||
| 27 | + @Override | ||
| 28 | + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { | ||
| 29 | + // 获得被拦截方法参数名列表 | ||
| 30 | + Method method = getMethod(joinPoint); | ||
| 31 | + Object[] args = joinPoint.getArgs(); | ||
| 32 | + String[] parameterNames = this.parameterNameDiscoverer.getParameterNames(method); | ||
| 33 | + // 准备 Spring EL 表达式解析的上下文 | ||
| 34 | + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); | ||
| 35 | + if (ArrayUtil.isNotEmpty(parameterNames)) { | ||
| 36 | + for (int i = 0; i < parameterNames.length; i++) { | ||
| 37 | + evaluationContext.setVariable(parameterNames[i], args[i]); | ||
| 38 | + } | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + // 解析参数 | ||
| 42 | + Expression expression = expressionParser.parseExpression(idempotent.keyArg()); | ||
| 43 | + return expression.getValue(evaluationContext, String.class); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + private static Method getMethod(JoinPoint point) { | ||
| 47 | + // 处理,声明在类上的情况 | ||
| 48 | + MethodSignature signature = (MethodSignature) point.getSignature(); | ||
| 49 | + Method method = signature.getMethod(); | ||
| 50 | + if (!method.getDeclaringClass().isInterface()) { | ||
| 51 | + return method; | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | + // 处理,声明在接口上的情况 | ||
| 55 | + try { | ||
| 56 | + return point.getTarget().getClass().getDeclaredMethod( | ||
| 57 | + point.getSignature().getName(), method.getParameterTypes()); | ||
| 58 | + } catch (NoSuchMethodException e) { | ||
| 59 | + throw new RuntimeException(e); | ||
| 60 | + } | ||
| 61 | + } | ||
| 62 | + | ||
| 63 | +} |
src/main/java/cn/iocoder/dashboard/framework/idempotent/package-info.java
| 1 | /** | 1 | /** |
| 2 | * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现 | 2 | * 幂等组件,参考 https://github.com/it4alla/idempotent 项目实现 |
| 3 | + * 实现原理是,相同参数的方法,一段时间内,有且仅能执行一次。通过这样的方式,保证幂等性。 | ||
| 4 | + * | ||
| 5 | + * 使用场景:例如说,用户快速的双击了某个按钮,前端没有禁用该按钮,导致发送了两次重复的请求。 | ||
| 6 | + * | ||
| 7 | + * 和 it4alla/idempotent 组件的差异点,主要体现在两点: | ||
| 8 | + * 1. 我们去掉了 @Idempotent 注解的 delKey 属性。原因是,本质上 delKey 为 true 时,实现的是分布式锁的能力 | ||
| 9 | + * 此时,我们偏向使用 Lock4j 组件。原则上,一个组件只提供一种单一的能力。 | ||
| 10 | + * 2. 考虑到组件的通用性,我们并未像 it4alla/idempotent 组件一样使用 Redisson RMap 结构,而是直接使用 Redis 的 String 数据格式。 | ||
| 3 | */ | 11 | */ |
| 4 | package cn.iocoder.dashboard.framework.idempotent; | 12 | package cn.iocoder.dashboard.framework.idempotent; |
src/main/java/cn/iocoder/dashboard/modules/infra/controller/config/InfConfigController.java
| @@ -5,6 +5,7 @@ import cn.iocoder.dashboard.common.pojo.CommonResult; | @@ -5,6 +5,7 @@ import cn.iocoder.dashboard.common.pojo.CommonResult; | ||
| 5 | import cn.iocoder.dashboard.common.pojo.PageResult; | 5 | import cn.iocoder.dashboard.common.pojo.PageResult; |
| 6 | import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils; | 6 | import cn.iocoder.dashboard.framework.excel.core.util.ExcelUtils; |
| 7 | import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; | 7 | import cn.iocoder.dashboard.framework.idempotent.core.annotation.Idempotent; |
| 8 | +import cn.iocoder.dashboard.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; | ||
| 8 | import cn.iocoder.dashboard.modules.infra.controller.config.vo.*; | 9 | import cn.iocoder.dashboard.modules.infra.controller.config.vo.*; |
| 9 | import cn.iocoder.dashboard.modules.infra.convert.config.InfConfigConvert; | 10 | import cn.iocoder.dashboard.modules.infra.convert.config.InfConfigConvert; |
| 10 | import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO; | 11 | import cn.iocoder.dashboard.modules.infra.dal.dataobject.config.InfConfigDO; |
| @@ -92,7 +93,7 @@ public class InfConfigController { | @@ -92,7 +93,7 @@ public class InfConfigController { | ||
| 92 | @PostMapping("/create") | 93 | @PostMapping("/create") |
| 93 | // @PreAuthorize("@ss.hasPermi('infra:config:add')") | 94 | // @PreAuthorize("@ss.hasPermi('infra:config:add')") |
| 94 | // @Log(title = "参数管理", businessType = BusinessType.INSERT) | 95 | // @Log(title = "参数管理", businessType = BusinessType.INSERT) |
| 95 | - @Idempotent(timeout = 10) | 96 | + @Idempotent(timeout = 60, keyResolver = ExpressionIdempotentKeyResolver.class, keyArg = "#reqVO.key") |
| 96 | public CommonResult<Long> createConfig(@Validated @RequestBody InfConfigCreateReqVO reqVO) { | 97 | public CommonResult<Long> createConfig(@Validated @RequestBody InfConfigCreateReqVO reqVO) { |
| 97 | return success(configService.createConfig(reqVO)); | 98 | return success(configService.createConfig(reqVO)); |
| 98 | } | 99 | } |