Browse Source

重构限流器框架,调整core-rate-limit,data-redis,starter-api模块相关内容

Signed-off-by: luoshi <luoshi@qmth.com.cn>
luoshi 2 years ago
parent
commit
b3b356fe7e
25 changed files with 351 additions and 247 deletions
  1. 45 0
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/annotation/RateLimit.java
  2. 7 0
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitRule.java
  3. 6 2
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitScope.java
  4. 9 2
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitTarget.java
  5. 0 18
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimitPolicy.java
  6. 4 5
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimitService.java
  7. 16 0
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimiter.java
  8. 15 49
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/MemoryRateLimitService.java
  9. 48 0
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/MemoryRateLimiter.java
  10. 0 38
      core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/RateLimitor.java
  11. 6 3
      core-rate-limit/src/test/java/com/qmth/boot/test/core/rateLimit/RateLimitTest.java
  12. 32 28
      data-redis/src/main/java/com/qmth/boot/redis/rateLimit/RedisRateLimitService.java
  13. 37 0
      data-redis/src/main/java/com/qmth/boot/redis/rateLimit/RedisRateLimiter.java
  14. 2 1
      starter-api/src/main/java/com/qmth/boot/api/annotation/Aac.java
  15. 16 26
      starter-api/src/main/java/com/qmth/boot/api/config/ApiConfig.java
  16. 17 7
      starter-api/src/main/java/com/qmth/boot/api/config/ApiConfigService.java
  17. 1 1
      starter-api/src/main/java/com/qmth/boot/api/config/ApiInterceptorConfig.java
  18. 0 12
      starter-api/src/main/java/com/qmth/boot/api/config/ApiProperties.java
  19. 4 3
      starter-api/src/main/java/com/qmth/boot/api/constant/ApiConstant.java
  20. 9 17
      starter-api/src/main/java/com/qmth/boot/api/interceptor/AbstractInterceptor.java
  21. 5 8
      starter-api/src/main/java/com/qmth/boot/api/interceptor/impl/AuthorizationInterceptor.java
  22. 0 9
      starter-api/src/main/java/com/qmth/boot/api/interceptor/impl/MetricsInterceptor.java
  23. 49 14
      starter-api/src/main/java/com/qmth/boot/api/interceptor/impl/RateLimitInterceptor.java
  24. 14 4
      starter-api/src/main/java/com/qmth/boot/api/interceptor/impl/ValveInterceptor.java
  25. 9 0
      starter-api/src/main/java/com/qmth/boot/api/utils/RequestUtil.java

+ 45 - 0
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/annotation/RateLimit.java

@@ -0,0 +1,45 @@
+package com.qmth.boot.core.rateLimit.annotation;
+
+import com.qmth.boot.core.rateLimit.entity.RateLimitScope;
+import com.qmth.boot.core.rateLimit.entity.RateLimitTarget;
+
+import java.lang.annotation.*;
+
+/**
+ * 限流设置注解
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Documented
+public @interface RateLimit {
+
+    /**
+     * 数量限制
+     *
+     * @return
+     */
+    int count();
+
+    /**
+     * 作用范围,默认为整个集群
+     *
+     * @return
+     */
+    RateLimitScope scope() default RateLimitScope.CLUSTER;
+
+    /**
+     * 限流对象,默认为所有访问者
+     *
+     * @return
+     */
+    RateLimitTarget target() default RateLimitTarget.ALL;
+
+    /**
+     * 时间范围,单位为毫秒,默认为0;
+     * 为0时表示不针对时间段,而是控制并发数量
+     *
+     * @return
+     */
+    long period() default 0;
+
+}

+ 7 - 0
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitRule.java

@@ -1,5 +1,6 @@
 package com.qmth.boot.core.rateLimit.entity;
 
+import com.qmth.boot.core.rateLimit.annotation.RateLimit;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.boot.convert.DurationStyle;
 
@@ -9,6 +10,7 @@ import java.time.Duration;
  * 限流规则对象,可以基于标准表达式构建
  * 表达式格式为:100/2s,斜杠左边为数量,右边为时间间隔
  * 可选参数格式:100/id/2s,i表示单实例,d表示单设备
+ * 可选参数格式:100/u/0s,u表示单用户,0s表示全局并发控制
  * 未配置情况下,默认参数为集群环境下的所有设备
  */
 public class RateLimitRule {
@@ -23,6 +25,11 @@ public class RateLimitRule {
 
     private Duration period;
 
+    public static RateLimitRule parse(RateLimit annotation) {
+        return new RateLimitRule(annotation.count(), annotation.scope(), annotation.target(),
+                Duration.ofMillis(annotation.period()));
+    }
+
     public static RateLimitRule parse(String expression) {
         String[] values = StringUtils.split(StringUtils.trimToNull(expression), EXPRESSION_SPLIT);
         if (values != null) {

+ 6 - 2
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitScope.java

@@ -2,11 +2,15 @@ package com.qmth.boot.core.rateLimit.entity;
 
 public enum RateLimitScope {
 
-    CLUSTER(""), INSTANCE("i");
+    //作用整个集群
+    CLUSTER(""),
+
+    //仅限当前实例
+    INSTANCE("i");
 
     private String code;
 
-    private RateLimitScope(String code) {
+    RateLimitScope(String code) {
         this.code = code;
     }
 

+ 9 - 2
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/entity/RateLimitTarget.java

@@ -2,11 +2,18 @@ package com.qmth.boot.core.rateLimit.entity;
 
 public enum RateLimitTarget {
 
-    ALL(""), DEVICE("d");
+    //所有访问者
+    ALL(""),
+
+    //按签名区分访问者
+    USER("u"),
+
+    //按设备区分访问者
+    DEVICE("d");
 
     private String code;
 
-    private RateLimitTarget(String code) {
+    RateLimitTarget(String code) {
         this.code = code;
     }
 

+ 0 - 18
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimitPolicy.java

@@ -1,18 +0,0 @@
-package com.qmth.boot.core.rateLimit.service;
-
-import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
-
-/**
- * 动态获取限流规则扩展接口
- */
-public interface RateLimitPolicy {
-
-    /**
-     * 根据API地址动态获取限流规则
-     *
-     * @param endpoint
-     * @return
-     */
-    RateLimitRule[] getRules(String endpoint);
-
-}

+ 4 - 5
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimitService.java

@@ -8,14 +8,13 @@ import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
 public interface RateLimitService {
 
     /**
-     * 根据控制点、访问设备、规则集合执行限流判定
-     * 多个规则必须全满足才判定通过
+     * 根据控制点、访问对象、限流规则获取限流器
      *
      * @param endpoint
-     * @param device
-     * @param rules
+     * @param target
+     * @param rule
      * @return
      */
-    boolean accept(String endpoint, String device, RateLimitRule... rules);
+    RateLimiter getRateLimiter(String endpoint, String target, RateLimitRule rule);
 
 }

+ 16 - 0
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/RateLimiter.java

@@ -0,0 +1,16 @@
+package com.qmth.boot.core.rateLimit.service;
+
+public interface RateLimiter {
+
+    /**
+     * 尝试获取限流许可
+     *
+     * @return
+     */
+    boolean acquire();
+
+    /**
+     * 释放已获取的许可
+     */
+    void release();
+}

+ 15 - 49
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/MemoryRateLimitService.java

@@ -1,68 +1,34 @@
 package com.qmth.boot.core.rateLimit.service.impl;
 
 import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
-import com.qmth.boot.core.rateLimit.entity.RateLimitTarget;
 import com.qmth.boot.core.rateLimit.service.RateLimitService;
-import org.apache.commons.lang3.ArrayUtils;
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
 
-import java.time.Duration;
 import java.util.HashMap;
 import java.util.Map;
 
 public class MemoryRateLimitService implements RateLimitService {
 
-    protected static final String KEY_JOINER = "_";
+    private static final String TARGET_ALL_KEY = "*";
 
-    protected static final int LIMITOR_ARRAY_COUNT = 10;
-
-    private final Map<String, RateLimitor[]> limitorMap = new HashMap<>();
+    private final Map<String, Map<String, MemoryRateLimiter>> limiterMap = new HashMap<>();
 
     @Override
-    public boolean accept(String endpoint, String device, RateLimitRule... rules) {
-        if (ArrayUtils.isEmpty(rules)) {
-            return true;
-        }
-        for (RateLimitRule rule : rules) {
-            if (!accept(endpoint, device, rule)) {
-                return false;
+    public RateLimiter getRateLimiter(String endpoint, String target, RateLimitRule rule) {
+        target = target == null ? TARGET_ALL_KEY : target;
+        Map<String, MemoryRateLimiter> map = limiterMap.get(endpoint);
+        if (map == null) {
+            synchronized (limiterMap) {
+                map = limiterMap.computeIfAbsent(endpoint, k -> new HashMap<>());
             }
         }
-        return true;
-    }
-
-    protected boolean accept(String endpoint, String device, RateLimitRule rule) {
-        String key = endpoint;
-        if (rule.getTarget() == RateLimitTarget.DEVICE) {
-            // 若限流对象是单个设备,则KEY需要包含设备标识
-            key = key.concat(KEY_JOINER).concat(device);
-        }
-        RateLimitor limitor = getLimitor(key, rule.getPeriod());
-        return limitor.count() <= rule.getCount() && limitor.incr() <= rule.getCount();
-    }
-
-    private RateLimitor getLimitor(String key, Duration period) {
-        RateLimitor[] array = limitorMap.get(key);
-        if (array == null) {
-            synchronized (limitorMap) {
-                array = limitorMap.computeIfAbsent(key, k -> initArray());
+        MemoryRateLimiter limiter = map.get(target);
+        if (limiter == null) {
+            synchronized (map) {
+                limiter = map.computeIfAbsent(target,
+                        k -> new MemoryRateLimiter(rule.getCount(), rule.getPeriod().toMillis()));
             }
         }
-        long time = System.currentTimeMillis() / period.toMillis();
-        int index = (int) (time % LIMITOR_ARRAY_COUNT);
-        RateLimitor limitor = array[index];
-        // 判断当前限流器归属的时间窗口,不是当前则重置
-        if (limitor.time() != time) {
-            limitor.reset(time);
-        }
-        return limitor;
-    }
-
-    private RateLimitor[] initArray() {
-        RateLimitor[] array = new RateLimitor[LIMITOR_ARRAY_COUNT];
-        for (int i = 0; i < LIMITOR_ARRAY_COUNT; i++) {
-            array[i] = new RateLimitor(0);
-        }
-        return array;
+        return limiter;
     }
-
 }

+ 48 - 0
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/MemoryRateLimiter.java

@@ -0,0 +1,48 @@
+package com.qmth.boot.core.rateLimit.service.impl;
+
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
+
+import java.util.concurrent.Semaphore;
+
+/**
+ * 内存限流控制单元
+ */
+public class MemoryRateLimiter implements RateLimiter {
+
+    private volatile long expireTime;
+
+    private long interval;
+
+    private Semaphore counter;
+
+    private int maxCount;
+
+    public MemoryRateLimiter(int count, long interval) {
+        this.maxCount = count;
+        this.counter = new Semaphore(count);
+        //interval大于0表示需要限流的时间段
+        //为0表示不分时间段,控制绝对并发数
+        this.interval = interval;
+        this.expireTime = interval > 0 ? (System.currentTimeMillis() / interval + 1) * interval : Long.MAX_VALUE;
+    }
+
+    @Override
+    public boolean acquire() {
+        long time = System.currentTimeMillis();
+        while (time > expireTime) {
+            synchronized (this) {
+                if (time > expireTime) {
+                    expireTime = (time / interval + 1) * interval;
+                    counter = new Semaphore(maxCount);
+                }
+            }
+        }
+        return counter.tryAcquire();
+    }
+
+    @Override
+    public void release() {
+        this.counter.release();
+    }
+
+}

+ 0 - 38
core-rate-limit/src/main/java/com/qmth/boot/core/rateLimit/service/impl/RateLimitor.java

@@ -1,38 +0,0 @@
-package com.qmth.boot.core.rateLimit.service.impl;
-
-import java.util.concurrent.atomic.AtomicInteger;
-
-/**
- * 内存限流控制单元
- */
-public class RateLimitor {
-
-    private long time;
-
-    private AtomicInteger count;
-
-    public RateLimitor(long time) {
-        this.time = time;
-        this.count = new AtomicInteger(0);
-    }
-
-    public synchronized void reset(long time) {
-        if (time != this.time) {
-            this.time = time;
-            this.count.set(0);
-        }
-    }
-
-    public int incr() {
-        return this.count.incrementAndGet();
-    }
-
-    public long time() {
-        return time;
-    }
-
-    public int count() {
-        return count.get();
-    }
-
-}

+ 6 - 3
core-rate-limit/src/test/java/com/qmth/boot/test/core/rateLimit/RateLimitTest.java

@@ -3,6 +3,7 @@ package com.qmth.boot.test.core.rateLimit;
 import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
 import com.qmth.boot.core.rateLimit.entity.RateLimitScope;
 import com.qmth.boot.core.rateLimit.entity.RateLimitTarget;
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
 import com.qmth.boot.core.rateLimit.service.impl.MemoryRateLimitService;
 import org.apache.commons.lang3.RandomUtils;
 import org.junit.Assert;
@@ -37,17 +38,19 @@ public class RateLimitTest {
         Assert.assertEquals(rule.getTarget(), RateLimitTarget.DEVICE);
     }
 
-    @Test
+    //@Test
     public void testSimpleRule() throws InterruptedException {
         RateLimitRule rule = RateLimitRule.parse("1/1s");
         String endpoint = "/api/test";
-        String device = "test";
+        String target = "test";
+        RateLimiter limiter = service.getRateLimiter(endpoint, target, rule);
 
         int loop = 0;
         long time = 0;
         while (loop < 50) {
             long current = System.currentTimeMillis() / rule.getPeriod().toMillis();
-            boolean result = service.accept(endpoint, device, rule);
+            boolean result = limiter.acquire();
+            System.out.println(current + "-" + time + "-" + result);
             if (current != time) {
                 Assert.assertTrue(result);
             } else {

+ 32 - 28
data-redis/src/main/java/com/qmth/boot/redis/rateLimit/RedisRateLimitService.java

@@ -2,14 +2,11 @@ package com.qmth.boot.redis.rateLimit;
 
 import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
 import com.qmth.boot.core.rateLimit.entity.RateLimitScope;
-import com.qmth.boot.core.rateLimit.entity.RateLimitTarget;
 import com.qmth.boot.core.rateLimit.service.RateLimitService;
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
+import com.qmth.boot.core.uid.service.UidMachineService;
 import com.qmth.boot.redis.constant.RedisConstant;
-import org.apache.commons.lang3.ArrayUtils;
-import org.redisson.api.RRateLimiter;
-import org.redisson.api.RateIntervalUnit;
-import org.redisson.api.RateType;
-import org.redisson.api.RedissonClient;
+import org.redisson.api.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
@@ -23,35 +20,42 @@ public class RedisRateLimitService implements RateLimitService, RedisConstant {
 
     private RedissonClient redissonClient;
 
-    public RedisRateLimitService(RedissonClient redissonClient) {
+    private UidMachineService uidMachineService;
+
+    public RedisRateLimitService(RedissonClient redissonClient, UidMachineService uidMachineService) {
         this.redissonClient = redissonClient;
+        this.uidMachineService = uidMachineService;
         log.info("Redis rate limit service inited.");
     }
 
     @Override
-    public boolean accept(String endpoint, String device, RateLimitRule... rules) {
-        if (ArrayUtils.isEmpty(rules)) {
-            return true;
+    public RateLimiter getRateLimiter(String endpoint, String target, RateLimitRule rule) {
+        String key = RATE_LIMIT_PREFIX.concat(KEY_SEPARATOR).concat(endpoint);
+        //控制范围为非集群时,加入machineId作为本实例标识
+        if (rule.getScope() == RateLimitScope.INSTANCE) {
+            key = key.concat(KEY_SEPARATOR).concat(String.valueOf(uidMachineService.getMachineId()));
         }
-        for (RateLimitRule rule : rules) {
-            if (!accept(endpoint, device, rule)) {
-                return false;
-            }
+        // 若包含限流对象,则key加入对象标识
+        if (target != null) {
+            key = key.concat(KEY_SEPARATOR).concat(target);
         }
-        return true;
-    }
-
-    protected boolean accept(String endpoint, String device, RateLimitRule rule) {
-        String key = RATE_LIMIT_PREFIX.concat(KEY_SEPARATOR).concat(endpoint);
-        if (rule.getTarget() == RateLimitTarget.DEVICE) {
-            // 若限流对象是单个设备,则KEY需要包含设备标识
-            key = key.concat(KEY_SEPARATOR).concat(device);
+        //设置了限流时间段,使用Redisson内置限流器
+        if (rule.getPeriod().toMillis() > 0) {
+            RRateLimiter limiter = redissonClient.getRateLimiter(key);
+            limiter.trySetRate(RateType.OVERALL, rule.getCount(), rule.getPeriod().toMillis(),
+                    RateIntervalUnit.MILLISECONDS);
+            //限流单元默认24小时后自动清理
+            limiter.expireAsync(24, TimeUnit.HOURS);
+            return new RedisRateLimiter(limiter);
+        }
+        //限流时间段为0时,使用Redisson内置信号量控制全局并发数
+        else {
+            RSemaphore semaphore = redissonClient.getSemaphore(key);
+            semaphore.trySetPermits(rule.getCount());
+            //限流单元默认24小时后自动清理
+            semaphore.expireAsync(24, TimeUnit.HOURS);
+            return new RedisRateLimiter(semaphore);
         }
-        RRateLimiter limiter = redissonClient.getRateLimiter(key);
-        limiter.trySetRate(rule.getScope() == RateLimitScope.INSTANCE ? RateType.PER_CLIENT : RateType.OVERALL,
-                rule.getCount(), rule.getPeriod().toMillis(), RateIntervalUnit.MILLISECONDS);
-        //限流单元默认24小时后自动清理
-        limiter.expireAsync(24, TimeUnit.HOURS);
-        return limiter.tryAcquire();
     }
+
 }

+ 37 - 0
data-redis/src/main/java/com/qmth/boot/redis/rateLimit/RedisRateLimiter.java

@@ -0,0 +1,37 @@
+package com.qmth.boot.redis.rateLimit;
+
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
+import org.redisson.api.RRateLimiter;
+import org.redisson.api.RSemaphore;
+
+public class RedisRateLimiter implements RateLimiter {
+
+    private RRateLimiter limiter;
+
+    private RSemaphore semaphore;
+
+    public RedisRateLimiter(RRateLimiter limiter) {
+        this.limiter = limiter;
+    }
+
+    public RedisRateLimiter(RSemaphore semaphore) {
+        this.semaphore = semaphore;
+    }
+
+    @Override
+    public boolean acquire() {
+        if (limiter != null) {
+            return limiter.tryAcquire();
+        } else if (semaphore != null) {
+            return semaphore.tryAcquire();
+        }
+        return false;
+    }
+
+    @Override
+    public void release() {
+        if (semaphore != null) {
+            semaphore.release();
+        }
+    }
+}

+ 2 - 1
starter-api/src/main/java/com/qmth/boot/api/annotation/Aac.java

@@ -1,6 +1,7 @@
 package com.qmth.boot.api.annotation;
 
 import com.qmth.boot.core.enums.Platform;
+import com.qmth.boot.core.rateLimit.annotation.RateLimit;
 import com.qmth.boot.tools.signature.SignatureType;
 
 import java.lang.annotation.*;
@@ -46,7 +47,7 @@ public @interface Aac {
      *
      * @return
      */
-    String[] rateLimit() default {};
+    RateLimit[] rateLimit() default {};
 
     /**
      * 是否需要鉴权

+ 16 - 26
starter-api/src/main/java/com/qmth/boot/api/config/AacConfig.java → starter-api/src/main/java/com/qmth/boot/api/config/ApiConfig.java

@@ -3,21 +3,23 @@ package com.qmth.boot.api.config;
 import com.qmth.boot.api.annotation.Aac;
 import com.qmth.boot.api.annotation.BOOL;
 import com.qmth.boot.core.enums.Platform;
+import com.qmth.boot.core.rateLimit.annotation.RateLimit;
 import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
 import com.qmth.boot.tools.signature.SignatureType;
 import org.apache.commons.lang3.ArrayUtils;
+import org.springframework.util.CollectionUtils;
 
-import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
 import java.util.List;
+import java.util.stream.Collectors;
 
-public class AacConfig {
+public class ApiConfig {
 
     public static final String[] EMPTY_STRING_ARRAY = new String[] {};
 
     public static final Platform[] EMPTY_PLATFORM_ARRAY = new Platform[] {};
 
-    public static final RateLimitRule[] EMPTY_RATE_LIMIT_CONFIG = new RateLimitRule[] {};
-
     public static final SignatureType[] EMPTY_SIGNATURE_TYPE = new SignatureType[] {};
 
     private Boolean strict;
@@ -28,20 +30,18 @@ public class AacConfig {
 
     private String[] ipDeny = EMPTY_STRING_ARRAY;
 
-    private RateLimitRule[] rateLimit = EMPTY_RATE_LIMIT_CONFIG;
+    private List<RateLimitRule> rateLimit = Collections.emptyList();
 
     private Boolean auth;
 
     private SignatureType[] signType = EMPTY_SIGNATURE_TYPE;
 
-    public AacConfig(ApiProperties properties) {
+    public ApiConfig(ApiProperties properties) {
         this.strict = properties.isGlobalStrict();
         this.auth = properties.isGlobalAuth();
-
-        buildRateLimit(properties.getGlobalRateLimit());
     }
 
-    public AacConfig(Aac annotation) {
+    public ApiConfig(Aac annotation) {
         if (annotation == null) {
             return;
         }
@@ -64,10 +64,10 @@ public class AacConfig {
             this.signType = annotation.signType();
         }
 
-        buildRateLimit(annotation.rateLimit());
+        buildRateLimitRule(annotation.rateLimit());
     }
 
-    public void merge(AacConfig config) {
+    public void merge(ApiConfig config) {
         if (config == null) {
             return;
         }
@@ -89,7 +89,7 @@ public class AacConfig {
         if (ArrayUtils.isNotEmpty(config.signType)) {
             this.signType = config.signType;
         }
-        if (ArrayUtils.isNotEmpty(config.rateLimit)) {
+        if (!CollectionUtils.isEmpty(config.rateLimit)) {
             this.rateLimit = config.rateLimit;
         }
     }
@@ -110,7 +110,7 @@ public class AacConfig {
         return ipDeny;
     }
 
-    public RateLimitRule[] getRateLimit() {
+    public List<RateLimitRule> getRateLimit() {
         return rateLimit;
     }
 
@@ -122,19 +122,9 @@ public class AacConfig {
         return signType;
     }
 
-    private void buildRateLimit(String[] expressions) {
-        if (ArrayUtils.isNotEmpty(expressions)) {
-            List<RateLimitRule> list = new ArrayList<>(expressions.length);
-            for (String expression : expressions) {
-                RateLimitRule config = RateLimitRule.parse(expression);
-                if (config != null) {
-                    list.add(config);
-                }
-            }
-            if (!list.isEmpty()) {
-                this.rateLimit = list.toArray(new RateLimitRule[0]);
-            }
+    private void buildRateLimitRule(RateLimit[] annotations) {
+        if (ArrayUtils.isNotEmpty(annotations)) {
+            this.rateLimit = Arrays.stream(annotations).map(RateLimitRule::parse).collect(Collectors.toList());
         }
     }
-
 }

+ 17 - 7
starter-api/src/main/java/com/qmth/boot/api/config/ApiConfigService.java

@@ -9,7 +9,7 @@ import org.springframework.context.event.ContextRefreshedEvent;
 import org.springframework.core.annotation.AnnotationUtils;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 import org.springframework.web.method.HandlerMethod;
 
 import java.lang.reflect.Method;
@@ -23,14 +23,14 @@ public class ApiConfigService implements ApplicationListener<ContextRefreshedEve
 
     private ApiProperties apiProperties;
 
-    private Map<Method, AacConfig> configMap;
+    private Map<Method, ApiConfig> configMap;
 
     public ApiConfigService(ApiProperties apiProperties) {
         this.apiProperties = apiProperties;
         this.configMap = new HashMap<>();
     }
 
-    public AacConfig getAacConfig(HandlerMethod handlerMethod) {
+    public ApiConfig getApiConfig(HandlerMethod handlerMethod) {
         return configMap.get(handlerMethod.getMethod());
     }
 
@@ -53,15 +53,25 @@ public class ApiConfigService implements ApplicationListener<ContextRefreshedEve
         Class<?> clazz = AopUtils.getTargetClass(bean);
         Method[] methods = clazz.getMethods();
         for (Method method : methods) {
-            initMethod(clazz, method);
+            if (filter(method)) {
+                initMethod(clazz, method);
+            }
         }
     }
 
     private void initMethod(Class<?> clazz, Method method) {
-        AacConfig ac = new AacConfig(apiProperties);
-        ac.merge(new AacConfig(AnnotationUtils.findAnnotation(clazz, Aac.class)));
-        ac.merge(new AacConfig(AnnotationUtils.findAnnotation(method, Aac.class)));
+        ApiConfig ac = new ApiConfig(apiProperties);
+        ac.merge(new ApiConfig(AnnotationUtils.findAnnotation(clazz, Aac.class)));
+        ac.merge(new ApiConfig(AnnotationUtils.findAnnotation(method, Aac.class)));
         configMap.put(method, ac);
         log.debug("Api method inited, {}:{}", clazz.getName(), method.getName());
     }
+
+    private boolean filter(Method method) {
+        return AnnotationUtils.findAnnotation(method, RequestMapping.class) != null
+                || AnnotationUtils.findAnnotation(method, GetMapping.class) != null
+                || AnnotationUtils.findAnnotation(method, PostMapping.class) != null
+                || AnnotationUtils.findAnnotation(method, PutMapping.class) != null
+                || AnnotationUtils.findAnnotation(method, DeleteMapping.class) != null;
+    }
 }

+ 1 - 1
starter-api/src/main/java/com/qmth/boot/api/config/ApiInterceptorConfig.java

@@ -46,9 +46,9 @@ public class ApiInterceptorConfig implements WebMvcConfigurer {
             prefix = prefix.concat("/**");
         }
         registry.addInterceptor(valveInterceptor).addPathPatterns(prefix);
-        registry.addInterceptor(rateLimitInterceptor).addPathPatterns(prefix);
         registry.addInterceptor(metricsInterceptor).addPathPatterns(prefix);
         registry.addInterceptor(authorizationInterceptor).addPathPatterns(prefix);
+        registry.addInterceptor(rateLimitInterceptor).addPathPatterns(prefix);
         if (extendInterceptor != null) {
             registry.addInterceptor(extendInterceptor).addPathPatterns(prefix);
         }

+ 0 - 12
starter-api/src/main/java/com/qmth/boot/api/config/ApiProperties.java

@@ -2,7 +2,6 @@ package com.qmth.boot.api.config;
 
 import com.qmth.boot.api.constant.ApiConstant;
 import org.springframework.boot.context.properties.ConfigurationProperties;
-import org.springframework.lang.Nullable;
 import org.springframework.stereotype.Component;
 import org.springframework.validation.annotation.Validated;
 
@@ -21,9 +20,6 @@ public class ApiProperties implements ApiConstant {
 
     private boolean globalStrict = false;
 
-    @Nullable
-    private String[] globalRateLimit = null;
-
     private boolean globalAuth = false;
 
     private boolean httpTrace = false;
@@ -55,14 +51,6 @@ public class ApiProperties implements ApiConstant {
         this.globalStrict = globalStrict;
     }
 
-    public String[] getGlobalRateLimit() {
-        return globalRateLimit;
-    }
-
-    public void setGlobalRateLimit(String[] globalRateLimit) {
-        this.globalRateLimit = globalRateLimit;
-    }
-
     public boolean isGlobalAuth() {
         return globalAuth;
     }

+ 4 - 3
starter-api/src/main/java/com/qmth/boot/api/constant/ApiConstant.java

@@ -26,12 +26,13 @@ public interface ApiConstant {
 
     String HEADER_TRACE_ID = "traceId";
 
-    String ATTRIBUTE_AAC_CONFIG = "aacConfig";
+    String ATTRIBUTE_API_CONFIG = "aacConfig";
+
+    String ATTRIBUTE_RATE_LIMITER_STACK = "rateLimiterStack";
 
     /**
-     * 默认对象名称即将废弃
+     * 默认对象名称
      */
-    @Deprecated
     String ATTRIBUTE_ACCESS_ENTITY = "accessEntity";
 
     String ATTRIBUTE_START_TIME = "startTime";

+ 9 - 17
starter-api/src/main/java/com/qmth/boot/api/interceptor/AbstractInterceptor.java

@@ -1,9 +1,10 @@
 package com.qmth.boot.api.interceptor;
 
-import com.qmth.boot.api.config.AacConfig;
+import com.qmth.boot.api.config.ApiConfig;
 import com.qmth.boot.api.config.ApiConfigService;
 import com.qmth.boot.api.config.ApiProperties;
 import com.qmth.boot.api.constant.ApiConstant;
+import com.qmth.boot.api.utils.RequestUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.web.method.HandlerMethod;
@@ -15,27 +16,18 @@ public abstract class AbstractInterceptor extends HandlerInterceptorAdapter impl
 
     protected final Logger log = LoggerFactory.getLogger(this.getClass());
 
-    protected AacConfig getAacConfig(ApiProperties apiProperties, ApiConfigService apiConfigService,
+    protected ApiConfig getApiConfig(ApiProperties apiProperties, ApiConfigService apiConfigService,
             HttpServletRequest request, Object handler) {
-        AacConfig aacConfig = getAacConfig(request);
-        if (aacConfig == null) {
+        ApiConfig apiConfig = RequestUtil.getAttribute(request, ATTRIBUTE_API_CONFIG);
+        if (apiConfig == null) {
             if (handler instanceof HandlerMethod) {
-                aacConfig = apiConfigService.getAacConfig((HandlerMethod) handler);
+                apiConfig = apiConfigService.getApiConfig((HandlerMethod) handler);
             } else {
-                aacConfig = new AacConfig(apiProperties);
+                apiConfig = new ApiConfig(apiProperties);
             }
-            request.setAttribute(ATTRIBUTE_AAC_CONFIG, aacConfig);
-        }
-        return aacConfig;
-    }
-
-    private AacConfig getAacConfig(HttpServletRequest request) {
-        Object obj = request.getAttribute(ATTRIBUTE_AAC_CONFIG);
-        if (obj instanceof AacConfig) {
-            return (AacConfig) obj;
-        } else {
-            return null;
+            request.setAttribute(ATTRIBUTE_API_CONFIG, apiConfig);
         }
+        return apiConfig;
     }
 
 }

+ 5 - 8
starter-api/src/main/java/com/qmth/boot/api/interceptor/impl/AuthorizationInterceptor.java

@@ -1,11 +1,10 @@
 package com.qmth.boot.api.interceptor.impl;
 
-import com.qmth.boot.api.config.AacConfig;
+import com.qmth.boot.api.config.ApiConfig;
 import com.qmth.boot.api.config.ApiConfigService;
 import com.qmth.boot.api.config.ApiProperties;
 import com.qmth.boot.api.exception.DefaultExceptionEnum;
 import com.qmth.boot.api.interceptor.AbstractInterceptor;
-import com.qmth.boot.api.utils.RequestUtil;
 import com.qmth.boot.api.valve.IPFilterService;
 import com.qmth.boot.core.logger.constant.LoggerConstant;
 import com.qmth.boot.core.security.exception.AuthorizationException;
@@ -35,7 +34,7 @@ public class AuthorizationInterceptor extends AbstractInterceptor implements Log
     private IPFilterService ipFilterService;
 
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
-        AacConfig config = getAacConfig(apiProperties, apiConfigService, request, handler);
+        ApiConfig config = getApiConfig(apiProperties, apiConfigService, request, handler);
         if (config.isAuth()) {
             AccessEntity entity;
             try {
@@ -43,16 +42,14 @@ public class AuthorizationInterceptor extends AbstractInterceptor implements Log
                 entity = authorizationSupport
                         .validateSignature(request.getHeader(HEADER_AUTHORIZATION), request.getMethod(),
                                 request.getServletPath(), request.getHeader(HEADER_TIME), config.getSignType());
-                // 默认对象名,即将废弃
+                // 默认对象名
                 request.setAttribute(ATTRIBUTE_ACCESS_ENTITY, entity);
                 // 以实现类名首字母小写后作为对象名
                 request.setAttribute(StringUtils.uncapitalize(entity.getClass().getSimpleName()), entity);
                 // 默认取签名标识作为接口访问者标识
-                if (request.getAttribute(ATTRIBUTE_CALLER) == null) {
-                    request.setAttribute(ATTRIBUTE_CALLER, entity.getLogName());
-                }
+                request.setAttribute(ATTRIBUTE_CALLER, entity.getLogName());
                 // 设置日志CALLER变量
-                MDC.put(MDC_CALLER, RequestUtil.getAttribute(request, ATTRIBUTE_CALLER, "-"));
+                MDC.put(MDC_CALLER, entity.getLogName());
             } catch (AuthorizationException ae) {
                 log.warn("Authorization faile: path={}, reason={}", request.getServletPath(), ae.getMessage());
                 throw DefaultExceptionEnum.AUTHORIZATION_FAILE.exception(ae.getCode(), ae.getMessage());

+ 0 - 9
starter-api/src/main/java/com/qmth/boot/api/interceptor/impl/MetricsInterceptor.java

@@ -4,8 +4,6 @@ import com.qmth.boot.api.constant.ApiConstant;
 import com.qmth.boot.api.interceptor.AbstractInterceptor;
 import com.qmth.boot.api.utils.RequestUtil;
 import com.qmth.boot.core.logger.constant.LoggerConstant;
-import com.qmth.boot.tools.uuid.FastUUID;
-import org.apache.commons.lang3.StringUtils;
 import org.slf4j.MDC;
 import org.springframework.stereotype.Component;
 
@@ -17,13 +15,6 @@ public class MetricsInterceptor extends AbstractInterceptor implements ApiConsta
 
     @Override
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
-        // 优先从header读取traceId,否则自动生成
-        String traceId = StringUtils.trimToNull(request.getHeader(HEADER_TRACE_ID));
-        if (traceId == null) {
-            traceId = FastUUID.get();
-        }
-        request.setAttribute(ATTRIBUTE_TRACE_ID, traceId);
-        MDC.put(MDC_TRACE_ID, traceId);
         // 设置开始处理时间
         request.setAttribute(ATTRIBUTE_START_TIME, System.currentTimeMillis());
         return true;

+ 49 - 14
starter-api/src/main/java/com/qmth/boot/api/interceptor/impl/RateLimitInterceptor.java

@@ -1,21 +1,25 @@
 package com.qmth.boot.api.interceptor.impl;
 
+import com.qmth.boot.api.config.ApiConfig;
 import com.qmth.boot.api.config.ApiConfigService;
 import com.qmth.boot.api.config.ApiProperties;
+import com.qmth.boot.api.constant.ApiConstant;
 import com.qmth.boot.api.exception.DefaultExceptionEnum;
 import com.qmth.boot.api.interceptor.AbstractInterceptor;
 import com.qmth.boot.api.utils.RequestUtil;
 import com.qmth.boot.core.rateLimit.entity.RateLimitRule;
-import com.qmth.boot.core.rateLimit.service.RateLimitPolicy;
+import com.qmth.boot.core.rateLimit.entity.RateLimitTarget;
 import com.qmth.boot.core.rateLimit.service.RateLimitService;
-import org.apache.commons.lang3.ArrayUtils;
+import com.qmth.boot.core.rateLimit.service.RateLimiter;
+import com.qmth.boot.core.security.model.AccessEntity;
 import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
 import javax.annotation.Resource;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+import java.util.ArrayDeque;
+import java.util.Deque;
 
 @Component
 public class RateLimitInterceptor extends AbstractInterceptor {
@@ -29,26 +33,48 @@ public class RateLimitInterceptor extends AbstractInterceptor {
     @Resource
     private RateLimitService rateLimitService;
 
-    @Autowired(required = false)
-    private RateLimitPolicy rateLimitPolicy;
-
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
-        String path = request.getServletPath();
-        RateLimitRule[] rules = rateLimitPolicy != null ? rateLimitPolicy.getRules(path) : null;
-        if (ArrayUtils.isEmpty(rules)) {
-            rules = getAacConfig(apiProperties, apiConfigService, request, handler).getRateLimit();
-        }
+        ApiConfig ac = getApiConfig(apiProperties, apiConfigService, request, handler);
+        String endpoint = request.getServletPath();
         String device = getDeviceIdOrIpAddress(request);
+        String user = getUserIdentity(request);
         // 所有限流规则需要全部匹配才通过
-        for (RateLimitRule rule : rules) {
-            if (!rateLimitService.accept(path, device, rule)) {
-                log.warn("Rate limited: path={}, rule={}, device={}", path, rule.toString(), device);
+        // 已通过限流堆栈
+        Deque<RateLimiter> stack = new ArrayDeque<>();
+        for (RateLimitRule rule : ac.getRateLimit()) {
+            String target = null;
+            if (rule.getTarget() == RateLimitTarget.DEVICE) {
+                target = device;
+            } else if (rule.getTarget() == RateLimitTarget.USER) {
+                target = user;
+            }
+            RateLimiter limiter = rateLimitService.getRateLimiter(endpoint, target, rule);
+            if (limiter.acquire()) {
+                stack.push(limiter);
+            } else {
+                while (!stack.isEmpty()) {
+                    stack.pop().release();
+                }
+                log.warn("Rate limited: endpoint={}, rule={}, target={}", endpoint, rule.toString(), target);
                 throw DefaultExceptionEnum.RATE_LIMITED.exception();
             }
         }
+        request.setAttribute(ATTRIBUTE_RATE_LIMITER_STACK, stack);
         return true;
     }
 
+    @Override
+    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
+            Exception ex) {
+        Deque<RateLimiter> stack = RequestUtil.getAttribute(request, ApiConstant.ATTRIBUTE_RATE_LIMITER_STACK);
+        if (stack != null) {
+            log.debug("Rate limit stack size: " + stack.size());
+            while (!stack.isEmpty()) {
+                stack.pop().release();
+            }
+        }
+    }
+
     private String getDeviceIdOrIpAddress(HttpServletRequest request) {
         // deviceId不存在时取IP地址作为设备标识
         String device = StringUtils.trimToNull(request.getHeader(HEADER_DEVICE_ID));
@@ -58,4 +84,13 @@ public class RateLimitInterceptor extends AbstractInterceptor {
         return device;
     }
 
+    private String getUserIdentity(HttpServletRequest request) {
+        AccessEntity accessEntity = RequestUtil.getAttribute(request, ApiConstant.ATTRIBUTE_ACCESS_ENTITY);
+        if (accessEntity != null) {
+            return accessEntity.getIdentity();
+        } else {
+            return StringUtils.EMPTY;
+        }
+    }
+
 }

+ 14 - 4
starter-api/src/main/java/com/qmth/boot/api/interceptor/impl/ValveInterceptor.java

@@ -1,6 +1,6 @@
 package com.qmth.boot.api.interceptor.impl;
 
-import com.qmth.boot.api.config.AacConfig;
+import com.qmth.boot.api.config.ApiConfig;
 import com.qmth.boot.api.config.ApiConfigService;
 import com.qmth.boot.api.config.ApiProperties;
 import com.qmth.boot.api.constant.ApiConstant;
@@ -9,8 +9,11 @@ import com.qmth.boot.api.interceptor.AbstractInterceptor;
 import com.qmth.boot.api.valve.IPFilterPolicy;
 import com.qmth.boot.api.valve.IPFilterService;
 import com.qmth.boot.core.enums.Platform;
+import com.qmth.boot.core.logger.constant.LoggerConstant;
+import com.qmth.boot.tools.uuid.FastUUID;
 import org.apache.commons.lang3.ArrayUtils;
 import org.apache.commons.lang3.StringUtils;
+import org.slf4j.MDC;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -38,7 +41,7 @@ public class ValveInterceptor extends AbstractInterceptor implements ApiConstant
     }
 
     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
-        AacConfig config = getAacConfig(apiProperties, apiConfigService, request, handler);
+        ApiConfig config = getApiConfig(apiProperties, apiConfigService, request, handler);
         Platform platform = Platform.findByName(StringUtils.trimToNull(request.getHeader(HEADER_PLATFORM)));
         String deviceId = StringUtils.trimToNull(request.getHeader(HEADER_DEVICE_ID));
         if (config.isStrict()) {
@@ -54,10 +57,17 @@ public class ValveInterceptor extends AbstractInterceptor implements ApiConstant
         }
         // 执行IP过滤判断
         validateIP(request, config);
+        // 优先从header读取traceId,否则自动生成
+        String traceId = StringUtils.trimToNull(request.getHeader(HEADER_TRACE_ID));
+        if (traceId == null) {
+            traceId = FastUUID.get();
+        }
+        request.setAttribute(ATTRIBUTE_TRACE_ID, traceId);
+        MDC.put(LoggerConstant.MDC_TRACE_ID, traceId);
         return true;
     }
 
-    private void validatePlatform(HttpServletRequest request, Platform platform, AacConfig config) {
+    private void validatePlatform(HttpServletRequest request, Platform platform, ApiConfig config) {
         if (ArrayUtils.isNotEmpty(config.getPlatform())) {
             boolean match = false;
             for (Platform p : config.getPlatform()) {
@@ -74,7 +84,7 @@ public class ValveInterceptor extends AbstractInterceptor implements ApiConstant
         }
     }
 
-    private void validateIP(HttpServletRequest request, AacConfig config) {
+    private void validateIP(HttpServletRequest request, ApiConfig config) {
         // 先从接口获取动态规则
         String[] ipAllow = ipFilterPolicy != null ? ipFilterPolicy.getAllowIP(request.getServletPath()) : null;
         String[] ipDeny = ipFilterPolicy != null ? ipFilterPolicy.getDenyIP(request.getServletPath()) : null;

+ 9 - 0
starter-api/src/main/java/com/qmth/boot/api/utils/RequestUtil.java

@@ -40,6 +40,15 @@ public class RequestUtil implements ApiConstant {
         }
     }
 
+    public static <T> T getAttribute(HttpServletRequest request, String name) {
+        Object obj = request.getAttribute(name);
+        try {
+            return (T) obj;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     public static String getAttribute(HttpServletRequest request, String name, String defaultValue) {
         Object obj = request.getAttribute(name);
         if (obj instanceof String) {