Commit 38db5fe0087afd98c61dd7b54e811f40ae93cdde
1 parent
321f71de
【新增】新增 UserIdempotentKeyResolver 解析器,支持用户级别的幂等
Showing
8 changed files
with
83 additions
and
7 deletions
yudao-framework/yudao-spring-boot-starter-protection/pom.xml
| ... | ... | @@ -16,6 +16,13 @@ |
| 16 | 16 | <url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
| 17 | 17 | |
| 18 | 18 | <dependencies> |
| 19 | + <!-- Web 相关 --> | |
| 20 | + <dependency> | |
| 21 | + <groupId>cn.iocoder.boot</groupId> | |
| 22 | + <artifactId>yudao-spring-boot-starter-web</artifactId> | |
| 23 | + <scope>provided</scope> <!-- 设置为 provided,只有 OncePerRequestFilter 使用到 --> | |
| 24 | + </dependency> | |
| 25 | + | |
| 19 | 26 | <!-- DB 相关 --> |
| 20 | 27 | <dependency> |
| 21 | 28 | <groupId>cn.iocoder.boot</groupId> | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/config/YudaoIdempotentConfiguration.java
| ... | ... | @@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.idempotent.core.aop.IdempotentAspect; |
| 4 | 4 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; |
| 5 | 5 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; |
| 6 | 6 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| 7 | +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver; | |
| 7 | 8 | import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; |
| 8 | 9 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 9 | 10 | import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; |
| ... | ... | @@ -33,6 +34,11 @@ public class YudaoIdempotentConfiguration { |
| 33 | 34 | } |
| 34 | 35 | |
| 35 | 36 | @Bean |
| 37 | + public UserIdempotentKeyResolver userIdempotentKeyResolver() { | |
| 38 | + return new UserIdempotentKeyResolver(); | |
| 39 | + } | |
| 40 | + | |
| 41 | + @Bean | |
| 36 | 42 | public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { |
| 37 | 43 | return new ExpressionIdempotentKeyResolver(); |
| 38 | 44 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/annotation/Idempotent.java
| ... | ... | @@ -2,6 +2,8 @@ package cn.iocoder.yudao.framework.idempotent.core.annotation; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; |
| 4 | 4 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| 5 | +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; | |
| 6 | +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver; | |
| 5 | 7 | |
| 6 | 8 | import java.lang.annotation.ElementType; |
| 7 | 9 | import java.lang.annotation.Retention; |
| ... | ... | @@ -36,6 +38,10 @@ public @interface Idempotent { |
| 36 | 38 | |
| 37 | 39 | /** |
| 38 | 40 | * 使用的 Key 解析器 |
| 41 | + * | |
| 42 | + * @see DefaultIdempotentKeyResolver 全局级别 | |
| 43 | + * @see UserIdempotentKeyResolver 用户级别 | |
| 44 | + * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算 | |
| 39 | 45 | */ |
| 40 | 46 | Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class; |
| 41 | 47 | /** |
| ... | ... | @@ -43,4 +49,15 @@ public @interface Idempotent { |
| 43 | 49 | */ |
| 44 | 50 | String keyArg() default ""; |
| 45 | 51 | |
| 52 | + /** | |
| 53 | + * 删除 Key,当发生异常时候 | |
| 54 | + * | |
| 55 | + * 问题:为什么发生异常时,需要删除 Key 呢? | |
| 56 | + * 回答:发生异常时,说明业务发生错误,此时需要删除 Key,避免下次请求无法正常执行。 | |
| 57 | + * | |
| 58 | + * 问题:为什么不搞 deleteWhenSuccess 执行成功时,需要删除 Key 呢? | |
| 59 | + * 回答:这种情况下,本质上是分布式锁,推荐使用 @Lock4j 注解 | |
| 60 | + */ | |
| 61 | + boolean deleteKeyWhenException() default true; | |
| 62 | + | |
| 46 | 63 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/aop/IdempotentAspect.java
| ... | ... | @@ -2,14 +2,14 @@ package cn.iocoder.yudao.framework.idempotent.core.aop; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 4 | 4 | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; |
| 5 | +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | |
| 5 | 6 | import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; |
| 6 | 7 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| 7 | 8 | import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; |
| 8 | -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | |
| 9 | 9 | import lombok.extern.slf4j.Slf4j; |
| 10 | -import org.aspectj.lang.JoinPoint; | |
| 10 | +import org.aspectj.lang.ProceedingJoinPoint; | |
| 11 | +import org.aspectj.lang.annotation.Around; | |
| 11 | 12 | import org.aspectj.lang.annotation.Aspect; |
| 12 | -import org.aspectj.lang.annotation.Before; | |
| 13 | 13 | import org.springframework.util.Assert; |
| 14 | 14 | |
| 15 | 15 | import java.util.List; |
| ... | ... | @@ -36,21 +36,33 @@ public class IdempotentAspect { |
| 36 | 36 | this.idempotentRedisDAO = idempotentRedisDAO; |
| 37 | 37 | } |
| 38 | 38 | |
| 39 | - @Before("@annotation(idempotent)") | |
| 40 | - public void beforePointCut(JoinPoint joinPoint, Idempotent idempotent) { | |
| 39 | + @Around(value = "@annotation(idempotent)") | |
| 40 | + public Object beforePointCut(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { | |
| 41 | 41 | // 获得 IdempotentKeyResolver |
| 42 | 42 | IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); |
| 43 | 43 | Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); |
| 44 | 44 | // 解析 Key |
| 45 | 45 | String key = keyResolver.resolver(joinPoint, idempotent); |
| 46 | 46 | |
| 47 | - // 锁定 Key。 | |
| 47 | + // 1. 锁定 Key | |
| 48 | 48 | boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); |
| 49 | 49 | // 锁定失败,抛出异常 |
| 50 | 50 | if (!success) { |
| 51 | 51 | log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); |
| 52 | 52 | throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); |
| 53 | 53 | } |
| 54 | + | |
| 55 | + // 2. 执行逻辑 | |
| 56 | + try { | |
| 57 | + return joinPoint.proceed(); | |
| 58 | + } catch (Throwable throwable) { | |
| 59 | + // 3. 异常时,删除 Key | |
| 60 | + // 参考美团 GTIS 思路:https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html | |
| 61 | + if (idempotent.deleteKeyWhenException()) { | |
| 62 | + idempotentRedisDAO.delete(key); | |
| 63 | + } | |
| 64 | + throw throwable; | |
| 65 | + } | |
| 54 | 66 | } |
| 55 | 67 | |
| 56 | 68 | } | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/DefaultIdempotentKeyResolver.java
| ... | ... | @@ -7,7 +7,7 @@ import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResol |
| 7 | 7 | import org.aspectj.lang.JoinPoint; |
| 8 | 8 | |
| 9 | 9 | /** |
| 10 | - * 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key | |
| 10 | + * 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key | |
| 11 | 11 | * |
| 12 | 12 | * 为了避免 Key 过长,使用 MD5 进行“压缩” |
| 13 | 13 | * | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/keyresolver/impl/UserIdempotentKeyResolver.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl; | |
| 2 | + | |
| 3 | +import cn.hutool.core.util.StrUtil; | |
| 4 | +import cn.hutool.crypto.SecureUtil; | |
| 5 | +import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; | |
| 6 | +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; | |
| 7 | +import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | |
| 8 | +import org.aspectj.lang.JoinPoint; | |
| 9 | + | |
| 10 | +/** | |
| 11 | + * 用户级别的幂等 Key 解析器,使用方法名 + 方法参数 + userId + userType,组装成一个 Key | |
| 12 | + * | |
| 13 | + * 为了避免 Key 过长,使用 MD5 进行“压缩” | |
| 14 | + * | |
| 15 | + * @author 芋道源码 | |
| 16 | + */ | |
| 17 | +public class UserIdempotentKeyResolver implements IdempotentKeyResolver { | |
| 18 | + | |
| 19 | + @Override | |
| 20 | + public String resolver(JoinPoint joinPoint, Idempotent idempotent) { | |
| 21 | + String methodName = joinPoint.getSignature().toString(); | |
| 22 | + String argsStr = StrUtil.join(",", joinPoint.getArgs()); | |
| 23 | + Long userId = WebFrameworkUtils.getLoginUserId(); | |
| 24 | + Integer userType = WebFrameworkUtils.getLoginUserType(); | |
| 25 | + return SecureUtil.md5(methodName + argsStr + userId + userType); | |
| 26 | + } | |
| 27 | + | |
| 28 | +} | ... | ... |
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/idempotent/core/redis/IdempotentRedisDAO.java
| ... | ... | @@ -29,6 +29,11 @@ public class IdempotentRedisDAO { |
| 29 | 29 | return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit); |
| 30 | 30 | } |
| 31 | 31 | |
| 32 | + public void delete(String key) { | |
| 33 | + String redisKey = formatKey(key); | |
| 34 | + redisTemplate.delete(redisKey); | |
| 35 | + } | |
| 36 | + | |
| 32 | 37 | private static String formatKey(String key) { |
| 33 | 38 | return String.format(IDEMPOTENT, key); |
| 34 | 39 | } | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/UserController.http