Commit 38db5fe0087afd98c61dd7b54e811f40ae93cdde

Authored by YunaiV
1 parent 321f71de

【新增】新增 UserIdempotentKeyResolver 解析器,支持用户级别的幂等

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
1 1 ### 请求 /system/user/page 接口 => 没有权限
2 2 GET {{baseUrl}}/system/user/page?pageNo=1&pageSize=10
3 3 Authorization: Bearer {{token}}
  4 +#Authorization: Bearer test100
4 5 tenant-id: {{adminTenentId}}
... ...