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,6 +16,13 @@ | ||
| 16 | <url>https://github.com/YunaiV/ruoyi-vue-pro</url> | 16 | <url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
| 17 | 17 | ||
| 18 | <dependencies> | 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 | <!-- DB 相关 --> | 26 | <!-- DB 相关 --> |
| 20 | <dependency> | 27 | <dependency> |
| 21 | <groupId>cn.iocoder.boot</groupId> | 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,6 +4,7 @@ import cn.iocoder.yudao.framework.idempotent.core.aop.IdempotentAspect; | ||
| 4 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; | 4 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; |
| 5 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; | 5 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.ExpressionIdempotentKeyResolver; |
| 6 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; | 6 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; |
| 7 | +import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.UserIdempotentKeyResolver; | ||
| 7 | import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; | 8 | import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; |
| 8 | import org.springframework.boot.autoconfigure.AutoConfiguration; | 9 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 9 | import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; | 10 | import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; |
| @@ -33,6 +34,11 @@ public class YudaoIdempotentConfiguration { | @@ -33,6 +34,11 @@ public class YudaoIdempotentConfiguration { | ||
| 33 | } | 34 | } |
| 34 | 35 | ||
| 35 | @Bean | 36 | @Bean |
| 37 | + public UserIdempotentKeyResolver userIdempotentKeyResolver() { | ||
| 38 | + return new UserIdempotentKeyResolver(); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Bean | ||
| 36 | public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { | 42 | public ExpressionIdempotentKeyResolver expressionIdempotentKeyResolver() { |
| 37 | return new ExpressionIdempotentKeyResolver(); | 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,6 +2,8 @@ package cn.iocoder.yudao.framework.idempotent.core.annotation; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; | 3 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.impl.DefaultIdempotentKeyResolver; |
| 4 | import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResolver; | 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 | import java.lang.annotation.ElementType; | 8 | import java.lang.annotation.ElementType; |
| 7 | import java.lang.annotation.Retention; | 9 | import java.lang.annotation.Retention; |
| @@ -36,6 +38,10 @@ public @interface Idempotent { | @@ -36,6 +38,10 @@ public @interface Idempotent { | ||
| 36 | 38 | ||
| 37 | /** | 39 | /** |
| 38 | * 使用的 Key 解析器 | 40 | * 使用的 Key 解析器 |
| 41 | + * | ||
| 42 | + * @see DefaultIdempotentKeyResolver 全局级别 | ||
| 43 | + * @see UserIdempotentKeyResolver 用户级别 | ||
| 44 | + * @see ExpressionIdempotentKeyResolver 自定义表达式,通过 {@link #keyArg()} 计算 | ||
| 39 | */ | 45 | */ |
| 40 | Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class; | 46 | Class<? extends IdempotentKeyResolver> keyResolver() default DefaultIdempotentKeyResolver.class; |
| 41 | /** | 47 | /** |
| @@ -43,4 +49,15 @@ public @interface Idempotent { | @@ -43,4 +49,15 @@ public @interface Idempotent { | ||
| 43 | */ | 49 | */ |
| 44 | String keyArg() default ""; | 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,14 +2,14 @@ package cn.iocoder.yudao.framework.idempotent.core.aop; | ||
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.common.exception.ServiceException; | 3 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 4 | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; | 4 | import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants; |
| 5 | +import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | ||
| 5 | import cn.iocoder.yudao.framework.idempotent.core.annotation.Idempotent; | 6 | 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.idempotent.core.keyresolver.IdempotentKeyResolver; |
| 7 | import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; | 8 | import cn.iocoder.yudao.framework.idempotent.core.redis.IdempotentRedisDAO; |
| 8 | -import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; | ||
| 9 | import lombok.extern.slf4j.Slf4j; | 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 | import org.aspectj.lang.annotation.Aspect; | 12 | import org.aspectj.lang.annotation.Aspect; |
| 12 | -import org.aspectj.lang.annotation.Before; | ||
| 13 | import org.springframework.util.Assert; | 13 | import org.springframework.util.Assert; |
| 14 | 14 | ||
| 15 | import java.util.List; | 15 | import java.util.List; |
| @@ -36,21 +36,33 @@ public class IdempotentAspect { | @@ -36,21 +36,33 @@ public class IdempotentAspect { | ||
| 36 | this.idempotentRedisDAO = idempotentRedisDAO; | 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 | // 获得 IdempotentKeyResolver | 41 | // 获得 IdempotentKeyResolver |
| 42 | IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); | 42 | IdempotentKeyResolver keyResolver = keyResolvers.get(idempotent.keyResolver()); |
| 43 | Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); | 43 | Assert.notNull(keyResolver, "找不到对应的 IdempotentKeyResolver"); |
| 44 | // 解析 Key | 44 | // 解析 Key |
| 45 | String key = keyResolver.resolver(joinPoint, idempotent); | 45 | String key = keyResolver.resolver(joinPoint, idempotent); |
| 46 | 46 | ||
| 47 | - // 锁定 Key。 | 47 | + // 1. 锁定 Key |
| 48 | boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); | 48 | boolean success = idempotentRedisDAO.setIfAbsent(key, idempotent.timeout(), idempotent.timeUnit()); |
| 49 | // 锁定失败,抛出异常 | 49 | // 锁定失败,抛出异常 |
| 50 | if (!success) { | 50 | if (!success) { |
| 51 | log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); | 51 | log.info("[beforePointCut][方法({}) 参数({}) 存在重复请求]", joinPoint.getSignature().toString(), joinPoint.getArgs()); |
| 52 | throw new ServiceException(GlobalErrorCodeConstants.REPEATED_REQUESTS.getCode(), idempotent.message()); | 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 +7,7 @@ import cn.iocoder.yudao.framework.idempotent.core.keyresolver.IdempotentKeyResol | ||
| 7 | import org.aspectj.lang.JoinPoint; | 7 | import org.aspectj.lang.JoinPoint; |
| 8 | 8 | ||
| 9 | /** | 9 | /** |
| 10 | - * 默认幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key | 10 | + * 默认(全局级别)幂等 Key 解析器,使用方法名 + 方法参数,组装成一个 Key |
| 11 | * | 11 | * |
| 12 | * 为了避免 Key 过长,使用 MD5 进行“压缩” | 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,6 +29,11 @@ public class IdempotentRedisDAO { | ||
| 29 | return redisTemplate.opsForValue().setIfAbsent(redisKey, "", timeout, timeUnit); | 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 | private static String formatKey(String key) { | 37 | private static String formatKey(String key) { |
| 33 | return String.format(IDEMPOTENT, key); | 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
| 1 | ### 请求 /system/user/page 接口 => 没有权限 | 1 | ### 请求 /system/user/page 接口 => 没有权限 |
| 2 | GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 | 2 | GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10 |
| 3 | Authorization: Bearer {{token}} | 3 | Authorization: Bearer {{token}} |
| 4 | +#Authorization: Bearer test100 | ||
| 4 | tenant-id: {{adminTenentId}} | 5 | tenant-id: {{adminTenentId}} |