This commit is contained in:
amos wong
2025-05-10 21:16:46 +08:00
commit b3fbe9f9e4
18 changed files with 852 additions and 0 deletions

33
.gitignore vendored Normal file
View File

@@ -0,0 +1,33 @@
HELP.md
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

100
pom.xml Normal file
View File

@@ -0,0 +1,100 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.deepinnet</groupId>
<artifactId>rate-limit-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rate-limit</name>
<description>rate-limit</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<version>2.7.14</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.29</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.18.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.11</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.28</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<!-- 要将源码放上去,需要加入这个插件 -->
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<attach>true</attach>
</configuration>
<executions>
<execution>
<phase>compile</phase>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 发布 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</build>
</project>

59
readme.md Normal file
View File

@@ -0,0 +1,59 @@
## 简介
rate-limit-starter是基于redisson框架封装的限流框架提供注解式@RateLimit限流
全部特性:
- Spring Boot支持
- @RateLimit注解限流
- 多限流策略组合
- 可扩展限流方式自己扩展IRateLimitHandler进行实现默认是redisson
## 使用说明
**引入maven依赖**
需要引入redisson依赖 以及 hutool
```xml
<dependency>
<groupId>com.deepinnet</groupId>
<artifactId>rate-limit-starter</artifactId>
<version>0.0.1-SNAPSHOT</version><!--最新版本-->
</dependency>
```
**开启@EnableRateLimit**
```java
@SpringBootApplication
@EnableRateLimit
public class Application {
public static void main(String[] args) {
SpringApplication.run(PropertyTestApplication.class, args);
}
}
```
**需要限流的方法标记注解@RateLimit**
```java
@RateLimit(
// 唯一标示支持SpEL表达式可无#name 为获取当前访问参数 name 内容
// key = "#code",
// 限定阈值,时间间隔 interval 范围内超过该数量会触发锁
count = 3,
// 限制间隔时长(可无,默认 3 分钟)例如 5s 五秒6m 六分钟7h 七小时8d 八天
interval = "6m",
// 策略(可无) ip 为获取当前访问IP地址内置策略
strategy = {IpKeyGenerateStrategy.TYPE}
)
public void limit(){
}
```

View File

@@ -0,0 +1,18 @@
package com.deepinnet.ratelimit.annotation;
import com.deepinnet.ratelimit.configure.LimiterConfiguration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 速率限制注解类
*/
@Target(value = {ElementType.TYPE})
@Retention(value = RetentionPolicy.RUNTIME)
@Import(LimiterConfiguration.class)
public @interface EnableRateLimit {
}

View File

@@ -0,0 +1,46 @@
package com.deepinnet.ratelimit.annotation;
import com.deepinnet.ratelimit.strategy.DefaultKeyGenerateStrategy;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 速率限制注解类
*/
@Target(value = {ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface RateLimit {
/**
* 唯一标示支持SpEL表达式
*/
String key() default "";
/**
* 限定阈值
* <p>
* 时间间隔 interval 范围内超过该数量会触发锁
*/
long count();
/**
* 时间间隔,默认 3 分钟
* <p>
* 例如 5s 五秒6m 六分钟7h 七小时8d 八天
*/
String interval() default "3m";
/**
* 限制策略
*/
String[] strategy() default {DefaultKeyGenerateStrategy.TYPE};
/**
* 提示消息,非必须
*/
String message() default "请勿频繁操作";
}

View File

@@ -0,0 +1,71 @@
package com.deepinnet.ratelimit.aspect;
import com.deepinnet.ratelimit.annotation.RateLimit;
import com.deepinnet.ratelimit.exception.RateLimitException;
import com.deepinnet.ratelimit.handler.IRateLimitHandler;
import com.deepinnet.ratelimit.metadata.MethodMetadata;
import com.deepinnet.ratelimit.metadata.RateLimitMethodMetaData;
import com.deepinnet.ratelimit.util.MethodUtils;
import lombok.AllArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 速率限制拦截切面处理类
*/
@Aspect
@AllArgsConstructor
public class RateLimitAspect {
/**
* 缓存方法上的源注解信息。减少反射的开销
*/
private static final Map<String, RateLimit> RATE_LIMIT_MAP = new ConcurrentHashMap<>();
private final IRateLimitHandler rateLimitHandler;
/**
* 限流注解切面
*
* @param pjp {@link ProceedingJoinPoint}
* @return {@link Object}
* @throws Throwable 限流异常
*/
@Around("@annotation(com.deepinnet.ratelimit.annotation.RateLimit)")
public Object interceptor(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
final String classMethodName = MethodUtils.getClassMethodName(method);
final RateLimit rateLimit = this.getRateLimit(method, classMethodName);
MethodMetadata methodMetadata = this.buildMethodMetadata(pjp);
if (rateLimitHandler.proceed(methodMetadata)) {
return pjp.proceed();
} else {
throw new RateLimitException(StringUtils.hasLength(rateLimit.message()) ? rateLimit.message() : "current limiting rule triggered");
}
}
/**
* 获取执行速率限制注解,缓存反射信息
*
* @param method 执行方法
* @param classMethodName 执行类方法名
* @return 方法对应的注解源信息,如果有,直接返回,如果无,获取放入缓存。
*/
public RateLimit getRateLimit(Method method, String classMethodName) {
return RATE_LIMIT_MAP.computeIfAbsent(classMethodName, k -> method.getAnnotation(RateLimit.class));
}
private MethodMetadata buildMethodMetadata(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
String classMethodName = MethodUtils.getClassMethodName(method);
RateLimit rateLimit = this.getRateLimit(method, classMethodName);
return new RateLimitMethodMetaData(method, joinPoint::getArgs, rateLimit);
}
}

View File

@@ -0,0 +1,49 @@
package com.deepinnet.ratelimit.configure;
import com.deepinnet.ratelimit.aspect.RateLimitAspect;
import com.deepinnet.ratelimit.exception.RateLimitException;
import com.deepinnet.ratelimit.handler.IRateLimitHandler;
import com.deepinnet.ratelimit.handler.RateLimitKeyParser;
import com.deepinnet.ratelimit.handler.RedissonRateLimitHandler;
import com.deepinnet.ratelimit.strategy.IKeyGenerateStrategy;
import com.deepinnet.ratelimit.strategy.IpKeyGenerateStrategy;
import org.redisson.api.RedissonClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.util.List;
/**
* 速率限制配置
*/
@Order(Ordered.LOWEST_PRECEDENCE)
public class LimiterConfiguration {
@Bean
public IpKeyGenerateStrategy ipRateLimitStrategy() {
return new IpKeyGenerateStrategy();
}
@Bean
public RateLimitKeyParser rateLimitKeyParser(List<IKeyGenerateStrategy> rateLimitStrategyList) {
return new RateLimitKeyParser(rateLimitStrategyList);
}
@Bean
@ConditionalOnMissingBean
public IRateLimitHandler rateLimitHandler(RedissonClient redissonClient,
RateLimitKeyParser rateLimitKeyParser) {
if (redissonClient == null) {
throw new RateLimitException("RedissonClient is a must!!!");
}
return new RedissonRateLimitHandler(redissonClient, rateLimitKeyParser);
}
@Bean
public RateLimitAspect rateLimitAspect(IRateLimitHandler rateLimitHandler) {
return new RateLimitAspect(rateLimitHandler);
}
}

View File

@@ -0,0 +1,14 @@
package com.deepinnet.ratelimit.exception;
/**
* 速率限制异常
*/
public class RateLimitException extends RuntimeException {
private static final long serialVersionUID = 4856751949960004774L;
public RateLimitException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,20 @@
package com.deepinnet.ratelimit.handler;
import com.deepinnet.ratelimit.metadata.MethodMetadata;
import com.deepinnet.ratelimit.metadata.RateLimitMethodMetaData;
/**
* 速率限制处理器接口
*/
public interface IRateLimitHandler {
/**
* 继续执行
*
* @param methodMetadata {@link RateLimitMethodMetaData}
* @return true 继续执行 false 限流不执行
*/
boolean proceed(MethodMetadata methodMetadata);
}

View File

@@ -0,0 +1,76 @@
package com.deepinnet.ratelimit.handler;
import cn.hutool.core.util.ArrayUtil;
import com.deepinnet.ratelimit.metadata.MethodMetadata;
import com.deepinnet.ratelimit.strategy.DefaultKeyGenerateStrategy;
import com.deepinnet.ratelimit.strategy.IKeyGenerateStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
/**
* 速率限制唯一标示 key 解析器
*/
@Slf4j
public class RateLimitKeyParser {
private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
private final ExpressionParser parser = new SpelExpressionParser();
private final List<IKeyGenerateStrategy> keyGenerateStrategyList;
private final IKeyGenerateStrategy defaultKeyGenerateStrategy;
public RateLimitKeyParser(List<IKeyGenerateStrategy> keyGenerateStrategyList) {
this.keyGenerateStrategyList = keyGenerateStrategyList;
// 默认key生成策略
defaultKeyGenerateStrategy = new DefaultKeyGenerateStrategy();
}
/**
* 构建唯一标示 KEY
*
* @param useDefaultStrategy 是否使用默认的key策略
*/
public String buildKey(MethodMetadata methodMetadata, String spELKey, String[] strategy, boolean useDefaultStrategy) {
StringBuilder key = new StringBuilder();
// SpEL Key 解析
String parseKey = Optional.ofNullable(spELKey)
.filter(StringUtils::hasLength)
.map(str -> {
Method method = methodMetadata.getMethod();
Object[] args = methodMetadata.getArgs();
return this.parser(spELKey, method, args);
}).orElse("");
if (useDefaultStrategy) {
key.append(defaultKeyGenerateStrategy.getKey(methodMetadata, parseKey));
}
// 组装自定义策略
keyGenerateStrategyList.stream()
.filter(rateLimitStrategy -> ArrayUtil.contains(strategy, rateLimitStrategy.getType()))
.forEach(rateLimitStrategy -> {
String strategyKey = rateLimitStrategy.getKey(methodMetadata, parseKey);
if (!key.toString().contains(strategyKey)) {
key.append(":").append(strategyKey);
}
});
return key.toString();
}
public String parser(String key, Method method, Object[] args) {
EvaluationContext context = new MethodBasedEvaluationContext(null, method, args, nameDiscoverer);
Object objKey = parser.parseExpression(key).getValue(context);
return ObjectUtils.nullSafeToString(objKey);
}
}

View File

@@ -0,0 +1,54 @@
package com.deepinnet.ratelimit.handler;
import com.deepinnet.ratelimit.annotation.RateLimit;
import com.deepinnet.ratelimit.metadata.MethodMetadata;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.*;
import org.springframework.boot.convert.DurationStyle;
import java.util.Objects;
/**
* Redisson 速率限制处理器
*/
@Slf4j
@AllArgsConstructor
public class RedissonRateLimitHandler implements IRateLimitHandler {
private final RedissonClient redissonClient;
private final RateLimitKeyParser rateLimitKeyParser;
@Override
public boolean proceed(MethodMetadata methodMetadata) {
RateLimit rateLimit = methodMetadata.getAnnotation();
// 间隔时间解析为毫秒
long interval = DurationStyle.detectAndParse(rateLimit.interval()).toMillis();
final String key = rateLimitKeyParser.buildKey(methodMetadata, rateLimit.key(), rateLimit.strategy(), true);
if (log.isDebugEnabled()) {
log.debug("rate.limit.key = {}", key);
}
RRateLimiter rateLimiter = redissonClient.getRateLimiter(key);
if (rateLimiter.isExists()) {
//判断限流器配置是否跟注解上一致 不一致需要重新生成配置
if (change(rateLimit, interval, rateLimiter.getConfig())) {
rateLimiter.delete();
rateLimiter.trySetRate(RateType.OVERALL, rateLimit.count(), interval, RateIntervalUnit.MILLISECONDS);
}
} else {
// 如果限流器不存在就创建一个RRateLimiter限流器
rateLimiter.trySetRate(RateType.OVERALL, rateLimit.count(), interval, RateIntervalUnit.MILLISECONDS);
}
// 是否触发限流
return rateLimiter.tryAcquire();
}
public boolean change(RateLimit rateLimit, Long interval, RateLimiterConfig config) {
if (!Objects.equals(config.getRate(), rateLimit.count())) {
return true;
}
if (!Objects.equals(config.getRateInterval(), interval)) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,41 @@
package com.deepinnet.ratelimit.metadata;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
/**
* 运行时的信息
*/
public interface MethodMetadata {
/**
* 获得当前运行时方法名
* {@link Method}
*
* @return 运行时方法名
*/
String getClassMethodName();
/**
* 获得当前运行时方法名
*
* @return 运行时方法
*/
Method getMethod();
/**
* 获得运行时方法的参数
*
* @return 参数数组
*/
Object[] getArgs();
/**
* 获得当前运行的参数
*
* @param <T> 当前运行的注解泛型
* @return 获取到的泛型的值
*/
<T extends Annotation> T getAnnotation();
}

View File

@@ -0,0 +1,63 @@
package com.deepinnet.ratelimit.metadata;
import com.deepinnet.ratelimit.annotation.RateLimit;
import com.deepinnet.ratelimit.util.MethodUtils;
import org.springframework.util.Assert;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.function.Supplier;
/**
* 当前速率限制的运行时的信息
*/
public class RateLimitMethodMetaData implements MethodMetadata {
/**
* 方法
*/
private final Method method;
/**
* 当前运行时方法中的参数
*/
private final Supplier<Object[]> argsSupplier;
/**
* 当前方法的全称
* 使用{@link MethodUtils#getClassMethodName(Method)}方法获得
*/
private final String classMethodName;
/**
* 速率限制所用到的注解
*/
private final RateLimit rateLimit;
public RateLimitMethodMetaData(Method method, Supplier<Object[]> argsSupplier, RateLimit rateLimit) {
Assert.notNull(method, "'method' cannot be null");
Assert.notNull(argsSupplier, "'argsSupplier' cannot be null");
Assert.notNull(rateLimit, "'rateLimit' cannot be null");
this.method = method;
this.argsSupplier = argsSupplier;
this.rateLimit = rateLimit;
this.classMethodName = MethodUtils.getClassMethodName(method);
}
@Override
public String getClassMethodName() {
return classMethodName;
}
@Override
public Method getMethod() {
return method;
}
@Override
public Object[] getArgs() {
return argsSupplier.get();
}
@SuppressWarnings("unchecked")
@Override
public <T extends Annotation> T getAnnotation() {
return (T) rateLimit;
}
}

View File

@@ -0,0 +1,25 @@
package com.deepinnet.ratelimit.strategy;
import com.deepinnet.ratelimit.metadata.MethodMetadata;
import org.springframework.util.StringUtils;
/**
* 默认key策略
*/
public class DefaultKeyGenerateStrategy implements IKeyGenerateStrategy {
public final static String TYPE = "default";
@Override
public String getType() {
return TYPE;
}
@Override
public String getKey(MethodMetadata methodMetadata, String parseKey) {
String result = methodMetadata.getClassMethodName();
if (StringUtils.hasLength(parseKey)) {
result += ":" + parseKey;
}
return result;
}
}

View File

@@ -0,0 +1,38 @@
package com.deepinnet.ratelimit.strategy;
import com.deepinnet.ratelimit.metadata.MethodMetadata;
import com.deepinnet.ratelimit.util.RequestUtils;
import javax.servlet.http.HttpServletRequest;
/**
* key生成策略接口
*/
public interface IKeyGenerateStrategy {
/**
* 策略类型
*
* @return 限流策略类型
*/
String getType();
/**
* 唯一标示 key
*
* @param methodMetadata {@link MethodMetadata}
* @param parseKey 解析spEL得到的Key
* @return 包装的key
*/
String getKey(MethodMetadata methodMetadata, String parseKey);
/**
* 当前请求
*
* @return 当前请求
*/
default HttpServletRequest getRequest() {
return RequestUtils.getRequest();
}
}

View File

@@ -0,0 +1,23 @@
package com.deepinnet.ratelimit.strategy;
import com.deepinnet.ratelimit.metadata.MethodMetadata;
import com.deepinnet.ratelimit.util.RequestUtils;
/**
* IP 速率限制策略
*/
public class IpKeyGenerateStrategy implements IKeyGenerateStrategy {
public final static String TYPE = "ip";
@Override
public String getType() {
return TYPE;
}
@Override
public String getKey(MethodMetadata methodMetadata, String parseKey) {
return RequestUtils.getIp(this.getRequest());
}
}

View File

@@ -0,0 +1,32 @@
package com.deepinnet.ratelimit.util;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
/**
* 反射方法相关工具类
*/
public class MethodUtils {
/**
* 获取执行类方法名
*
* @param method 执行方法
* @return 类名+方法名
*/
public static String getClassMethodName(Method method) {
return String.format("%s.%s", method.getDeclaringClass().getName(), method.getName());
}
/**
* 获取方法指定注解信息
*
* @param method {@link Method}
* @param annotationClass 注解类
* @param <T> 注解类
* @return 指定注解信息
*/
public static <T extends Annotation> T getAnnotation(Method method, Class<T> annotationClass) {
return method.getAnnotation(annotationClass);
}
}

View File

@@ -0,0 +1,90 @@
package com.deepinnet.ratelimit.util;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
public class RequestUtils {
public final static String LOCAL_IP = "127.0.0.1";
public final static String LOCAL_ADDRESS = "0:0:0:0:0:0:0:1";
/**
* 当前 HttpServletRequest 请求
*
* @return HttpServletRequest
*/
public static HttpServletRequest getRequest() {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}
private static final String[] HEADERS = {
"x-forwarded-for",
"Proxy-Client-IP",
"WL-Proxy-Client-IP",
"HTTP_X_FORWARDED_FOR",
"HTTP_X_FORWARDED",
"HTTP_X_CLUSTER_CLIENT_IP",
"HTTP_CLIENT_IP",
"HTTP_FORWARDED_FOR",
"HTTP_FORWARDED",
"HTTP_VIA",
"REMOTE_ADDR",
"X-Real-IP"
};
/**
* 获取 request 请求 IP 地址
*
* @return IP 地址
*/
public static String getIp(HttpServletRequest request) {
String ip = null;
for (String header : HEADERS) {
String currentIp = request.getHeader(header);
if (isNotUnknown(currentIp)) {
ip = currentIp;
break;
}
}
if (null == ip) {
ip = request.getRemoteAddr();
}
if (null == ip) {
return "";
}
if (LOCAL_ADDRESS.equals(ip)) {
return LOCAL_IP;
}
return getMultistageReverseProxyIp(ip);
}
/**
* 从多级反向代理中获得第一个非unknown IP地址
*
* @param ip 获得的IP地址
* @return 第一个非unknown IP地址
*/
private static String getMultistageReverseProxyIp(String ip) {
// 多级反向代理检测
String delimiter = ",";
if (ip != null && ip.indexOf(delimiter) > 0) {
String[] ips = ip.trim().split(delimiter);
for (String subIp : ips) {
if (isNotUnknown(subIp)) {
ip = subIp;
break;
}
}
}
return ip;
}
private static boolean isNotUnknown(String checkIp) {
return StringUtils.hasLength(checkIp) && !"unknown".equalsIgnoreCase(checkIp);
}
}