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 @@
</dependency>
<!-- 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>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
......
......@@ -5,7 +5,6 @@ import com.schbrain.common.web.properties.WebProperties;
import com.schbrain.common.web.result.ResponseBodyHandler;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
......@@ -24,10 +23,14 @@ import java.util.List;
* @author liaozan
* @since 2021/11/19
*/
@AutoConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(WebProperties.class)
@Import({AuthenticationConfiguration.class, ExceptionHandingConfiguration.class, ServletComponentConfiguration.class, CorsFilterConfiguration.class})
@Import({
AuthenticationConfiguration.class,
ExceptionHandingConfiguration.class,
ServletComponentConfiguration.class,
CorsFilterConfiguration.class
})
public class WebCommonAutoConfiguration {
@Bean
......
......@@ -26,6 +26,14 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</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>
</project>
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.provider.CacheProvider;
import com.schbrain.framework.autoconfigure.cache.provider.CacheProviderDelegate;
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.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
......@@ -30,4 +37,17 @@ public class CacheAutoConfiguration {
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.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.util.ApplicationName;
import com.schbrain.common.util.SpelUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.Advice;
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.factory.BeanFactory;
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 org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import static com.schbrain.common.util.ParameterDiscoverUtils.getMethodArgsMap;
/**
* @author liaozan
* @see com.schbrain.common.web.support.concurrent.RateLimiter
* @see RateLimiter
* @since 2022/5/5
*/
@Slf4j
@Aspect
@ConditionalOnBean(StringRedisTemplate.class)
@ConditionalOnClass({Advice.class, RedisConnectionFactory.class})
public class RateLimitAspect {
private final Map<String, RRateLimiter> rateLimiterMap = new ConcurrentHashMap<>();
private final Map<Class<?>, RateLimitCacheKeyVariablesContributor> contributorMap = new ConcurrentHashMap<>();
private final String keyPrefix;
private final BeanFactory beanFactory;
private final StringRedisTemplate stringRedisTemplate;
private final RedissonClient redissonClient;
private final ExpressionParser expressionParser;
public RateLimitAspect(StringRedisTemplate stringRedisTemplate, BeanFactory beanFactory) {
public RateLimitAspect(ConfigurableListableBeanFactory beanFactory, RedissonClient redissonClient) {
this.keyPrefix = ApplicationName.get();
this.beanFactory = beanFactory;
this.stringRedisTemplate = stringRedisTemplate;
this.redissonClient = redissonClient;
this.expressionParser = new ExpressionParser(beanFactory);
}
@Before("@annotation(rateLimiter)")
public void beforeExecute(JoinPoint joinPoint, RateLimiter rateLimiter) {
@Before("@annotation(annotation)")
public void beforeExecute(JoinPoint joinPoint, RateLimiter annotation) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
doRateLimit(rateLimiter, joinPoint, methodSignature);
doRateLimit(annotation, joinPoint, methodSignature);
}
protected void doRateLimit(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) {
Map<String, Object> variables = prepareVariables(rateLimiter, joinPoint, signature);
protected void doRateLimit(RateLimiter annotation, JoinPoint joinPoint, MethodSignature 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) {
throw new BaseException("cacheKey should not be null");
}
String formattedCacheKey = formatCacheKey(cacheKey);
BoundValueOperations<String, String> rateLimitOps = stringRedisTemplate.boundValueOps(formattedCacheKey);
RRateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(formattedCacheKey, key -> createRateLimiter(key, annotation));
try {
long accessCount = Optional.ofNullable(rateLimitOps.increment()).orElse(1L);
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;
if (rateLimiter.tryAcquire()) {
return;
}
throw new BaseException("The access frequency is too fast, please try again later");
}
protected String formatCacheKey(String 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());
Class<? extends RateLimitCacheKeyVariablesContributor> contributorClass = rateLimiter.contributor();
Class<? extends RateLimitCacheKeyVariablesContributor> contributorClass = annotation.contributor();
if (contributorClass == null || contributorClass == NoOpRateLimitCacheKeyVariablesContributor.class) {
return variables;
}
......@@ -97,7 +87,7 @@ public class RateLimitAspect {
contributor = BeanUtils.instantiateClass(contributorClass);
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)) {
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.reflect.MethodSignature;
......
package com.schbrain.common.web.support.concurrent;
package com.schbrain.framework.autoconfigure.cache.concurrent;
import java.lang.annotation.*;
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