Commit 139a0c15 authored by liaozan's avatar liaozan 🏀

Refactor RateLimiter

parent 77232905
package com.schbrain.common.util;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SpelUtils {
private static final ExpressionParser spELParser = new SpelExpressionParser();
private static final ConcurrentHashMap<String, Expression> expressionCache = new ConcurrentHashMap<>();
public static <T> T parse(String express, Map<String, Object> variables, Class<T> valueType, BeanFactory beanFactory) {
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setBeanResolver(new BeanFactoryResolver(beanFactory));
ctx.setVariables(variables);
return parse(express, ctx, valueType);
}
public static <T> T parse(String express, EvaluationContext context, Class<T> valueType) {
if (StringUtils.isBlank(express)) {
return null;
}
return getExpression(express).getValue(context, valueType);
}
private static Expression getExpression(String exp) {
Expression expression = expressionCache.get(exp);
if (null == expression) {
expression = spELParser.parseExpression(exp);
expressionCache.put(exp, expression);
}
return expression;
}
}
...@@ -38,16 +38,6 @@ ...@@ -38,16 +38,6 @@
</dependency> </dependency>
<!-- Optional --> <!-- Optional -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId> <artifactId>easyexcel</artifactId>
......
...@@ -5,7 +5,6 @@ import com.schbrain.common.web.properties.WebProperties; ...@@ -5,7 +5,6 @@ import com.schbrain.common.web.properties.WebProperties;
import com.schbrain.common.web.result.ResponseBodyHandler; import com.schbrain.common.web.result.ResponseBodyHandler;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages; import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
...@@ -24,10 +23,14 @@ import java.util.List; ...@@ -24,10 +23,14 @@ import java.util.List;
* @author liaozan * @author liaozan
* @since 2021/11/19 * @since 2021/11/19
*/ */
@AutoConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET) @ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(WebProperties.class) @EnableConfigurationProperties(WebProperties.class)
@Import({AuthenticationConfiguration.class, ExceptionHandingConfiguration.class, ServletComponentConfiguration.class, CorsFilterConfiguration.class}) @Import({
AuthenticationConfiguration.class,
ExceptionHandingConfiguration.class,
ServletComponentConfiguration.class,
CorsFilterConfiguration.class
})
public class WebCommonAutoConfiguration { public class WebCommonAutoConfiguration {
@Bean @Bean
......
...@@ -26,6 +26,14 @@ ...@@ -26,6 +26,14 @@
<groupId>org.apache.commons</groupId> <groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId> <artifactId>commons-pool2</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>
package com.schbrain.framework.autoconfigure.cache; package com.schbrain.framework.autoconfigure.cache;
import com.schbrain.common.util.JacksonUtils;
import com.schbrain.framework.autoconfigure.cache.concurrent.RateLimitAspect;
import com.schbrain.framework.autoconfigure.cache.properties.CacheProperties; import com.schbrain.framework.autoconfigure.cache.properties.CacheProperties;
import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider; import com.schbrain.framework.autoconfigure.cache.provider.CacheProvider;
import com.schbrain.framework.autoconfigure.cache.provider.CacheProviderDelegate; import com.schbrain.framework.autoconfigure.cache.provider.CacheProviderDelegate;
import com.schbrain.framework.autoconfigure.cache.provider.redis.RedisCacheConfiguration; import com.schbrain.framework.autoconfigure.cache.provider.redis.RedisCacheConfiguration;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
...@@ -30,4 +37,17 @@ public class CacheAutoConfiguration { ...@@ -30,4 +37,17 @@ public class CacheAutoConfiguration {
return provider; return provider;
} }
@Bean
@ConditionalOnMissingBean
@ConditionalOnBean(RedissonClient.class)
public RateLimitAspect rateLimitAspect(ConfigurableListableBeanFactory beanFactory, RedissonClient redissonClient) {
return new RateLimitAspect(beanFactory, redissonClient);
}
@Bean
@ConditionalOnBean(RedissonClient.class)
public RedissonAutoConfigurationCustomizer redissonConfigurationCodecCustomizer() {
return config -> config.setCodec(new JsonJacksonCodec(JacksonUtils.getObjectMapper()));
}
} }
package com.schbrain.framework.autoconfigure.cache.concurrent;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author liaozan
* @since 2023-07-15
*/
public class ExpressionParser {
private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>(256);
private final SpelExpressionParser parser = new SpelExpressionParser();
private final ParserContext parserContext = new TemplateParserContext();
private final ConfigurableListableBeanFactory beanFactory;
public ExpressionParser(ConfigurableListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
public String parse(String value, Map<String, Object> variables) {
String resolved = beanFactory.resolveEmbeddedValue(value);
Expression expression = expressionCache.computeIfAbsent(resolved, this::parseExpression);
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
context.setVariables(variables);
return expression.getValue(context, String.class);
}
private Expression parseExpression(String value) {
return parser.parseExpression(value, parserContext);
}
}
package com.schbrain.common.web.support.concurrent; package com.schbrain.framework.autoconfigure.cache.concurrent;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
......
package com.schbrain.common.web.support.concurrent; package com.schbrain.framework.autoconfigure.cache.concurrent;
import com.schbrain.common.exception.BaseException; import com.schbrain.common.exception.BaseException;
import com.schbrain.common.util.ApplicationName; import com.schbrain.common.util.ApplicationName;
import com.schbrain.common.util.SpelUtils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils; import org.apache.commons.collections4.MapUtils;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.Advice;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RRateLimiter;
import org.redisson.api.RateIntervalUnit;
import org.redisson.api.RateType;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundValueOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import static com.schbrain.common.util.ParameterDiscoverUtils.getMethodArgsMap; import static com.schbrain.common.util.ParameterDiscoverUtils.getMethodArgsMap;
/** /**
* @author liaozan * @author liaozan
* @see com.schbrain.common.web.support.concurrent.RateLimiter * @see RateLimiter
* @since 2022/5/5 * @since 2022/5/5
*/ */
@Slf4j @Slf4j
@Aspect @Aspect
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnClass({Advice.class, RedisConnectionFactory.class})
public class RateLimitAspect { public class RateLimitAspect {
private final Map<String, RRateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
private final Map<Class<?>, RateLimitCacheKeyVariablesContributor> contributorMap = new ConcurrentHashMap<>(); private final Map<Class<?>, RateLimitCacheKeyVariablesContributor> contributorMap = new ConcurrentHashMap<>();
private final String keyPrefix; private final String keyPrefix;
private final BeanFactory beanFactory; private final RedissonClient redissonClient;
private final StringRedisTemplate stringRedisTemplate; private final ExpressionParser expressionParser;
public RateLimitAspect(StringRedisTemplate stringRedisTemplate, BeanFactory beanFactory) { public RateLimitAspect(ConfigurableListableBeanFactory beanFactory, RedissonClient redissonClient) {
this.keyPrefix = ApplicationName.get(); this.keyPrefix = ApplicationName.get();
this.beanFactory = beanFactory; this.redissonClient = redissonClient;
this.stringRedisTemplate = stringRedisTemplate; this.expressionParser = new ExpressionParser(beanFactory);
} }
@Before("@annotation(rateLimiter)") @Before("@annotation(annotation)")
public void beforeExecute(JoinPoint joinPoint, RateLimiter rateLimiter) { public void beforeExecute(JoinPoint joinPoint, RateLimiter annotation) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
doRateLimit(rateLimiter, joinPoint, methodSignature); doRateLimit(annotation, joinPoint, methodSignature);
} }
protected void doRateLimit(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) { protected void doRateLimit(RateLimiter annotation, JoinPoint joinPoint, MethodSignature signature) {
Map<String, Object> variables = prepareVariables(rateLimiter, joinPoint, signature); Map<String, Object> variables = prepareVariables(annotation, joinPoint, signature);
String cacheKey = SpelUtils.parse(rateLimiter.cacheKey(), variables, String.class, beanFactory); String cacheKey = expressionParser.parse(annotation.cacheKey(), variables);
if (cacheKey == null) { if (cacheKey == null) {
throw new BaseException("cacheKey should not be null"); throw new BaseException("cacheKey should not be null");
} }
String formattedCacheKey = formatCacheKey(cacheKey); String formattedCacheKey = formatCacheKey(cacheKey);
BoundValueOperations<String, String> rateLimitOps = stringRedisTemplate.boundValueOps(formattedCacheKey); RRateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(formattedCacheKey, key -> createRateLimiter(key, annotation));
try { if (rateLimiter.tryAcquire()) {
long accessCount = Optional.ofNullable(rateLimitOps.increment()).orElse(1L); return;
if (accessCount > rateLimiter.permits()) {
throw new BaseException("The access frequency is too fast, please try again later");
}
if (accessCount <= 1) {
rateLimitOps.expire(rateLimiter.expireTime(), rateLimiter.unit());
}
} catch (Exception ex) {
log.error("RateLimit encountered an unknown error, remove cacheKey: {}", formattedCacheKey, ex);
// Remove cacheKey to prevent the cache from never expiring
stringRedisTemplate.delete(formattedCacheKey);
throw ex;
} }
throw new BaseException("The access frequency is too fast, please try again later");
} }
protected String formatCacheKey(String cacheKey) { protected String formatCacheKey(String cacheKey) {
return "rateLimit:" + keyPrefix + ":" + cacheKey; return "rateLimit:" + keyPrefix + ":" + cacheKey;
} }
private Map<String, Object> prepareVariables(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) { protected RRateLimiter createRateLimiter(String cacheKey, RateLimiter annotation) {
RRateLimiter rateLimiter = redissonClient.getRateLimiter(cacheKey);
rateLimiter.setRate(RateType.PER_CLIENT, annotation.permits(), annotation.expireTime(), RateIntervalUnit.valueOf(annotation.unit().name()));
return rateLimiter;
}
private Map<String, Object> prepareVariables(RateLimiter annotation, JoinPoint joinPoint, MethodSignature signature) {
Map<String, Object> variables = getMethodArgsMap(signature.getMethod(), joinPoint.getArgs()); Map<String, Object> variables = getMethodArgsMap(signature.getMethod(), joinPoint.getArgs());
Class<? extends RateLimitCacheKeyVariablesContributor> contributorClass = rateLimiter.contributor(); Class<? extends RateLimitCacheKeyVariablesContributor> contributorClass = annotation.contributor();
if (contributorClass == null || contributorClass == NoOpRateLimitCacheKeyVariablesContributor.class) { if (contributorClass == null || contributorClass == NoOpRateLimitCacheKeyVariablesContributor.class) {
return variables; return variables;
} }
...@@ -97,7 +87,7 @@ public class RateLimitAspect { ...@@ -97,7 +87,7 @@ public class RateLimitAspect {
contributor = BeanUtils.instantiateClass(contributorClass); contributor = BeanUtils.instantiateClass(contributorClass);
contributorMap.put(contributorClass, contributor); contributorMap.put(contributorClass, contributor);
} }
Map<String, Object> contributeVariables = contributor.contribute(rateLimiter, joinPoint, signature); Map<String, Object> contributeVariables = contributor.contribute(annotation, joinPoint, signature);
if (MapUtils.isNotEmpty(contributeVariables)) { if (MapUtils.isNotEmpty(contributeVariables)) {
variables.putAll(contributeVariables); variables.putAll(contributeVariables);
} }
......
package com.schbrain.common.web.support.concurrent; package com.schbrain.framework.autoconfigure.cache.concurrent;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
......
package com.schbrain.common.web.support.concurrent; package com.schbrain.framework.autoconfigure.cache.concurrent;
import java.lang.annotation.*; import java.lang.annotation.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment