diff --git a/commons/common-util/src/main/java/com/schbrain/common/util/SpelUtils.java b/commons/common-util/src/main/java/com/schbrain/common/util/SpelUtils.java deleted file mode 100644 index ba851c8652d7f3cb0df37d200aabd061f31d0a2c..0000000000000000000000000000000000000000 --- a/commons/common-util/src/main/java/com/schbrain/common/util/SpelUtils.java +++ /dev/null @@ -1,44 +0,0 @@ -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 expressionCache = new ConcurrentHashMap<>(); - - public static T parse(String express, Map variables, Class valueType, BeanFactory beanFactory) { - StandardEvaluationContext ctx = new StandardEvaluationContext(); - ctx.setBeanResolver(new BeanFactoryResolver(beanFactory)); - ctx.setVariables(variables); - return parse(express, ctx, valueType); - } - - public static T parse(String express, EvaluationContext context, Class 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; - } - -} diff --git a/commons/web-common/pom.xml b/commons/web-common/pom.xml index 7118f087c8251b6a204b17f085a8b52e5ae97c7b..c2b5bba5db25d23d1774ad32cdda540c0dad1705 100644 --- a/commons/web-common/pom.xml +++ b/commons/web-common/pom.xml @@ -38,16 +38,6 @@ - - org.springframework.boot - spring-boot-starter-aop - true - - - org.springframework.boot - spring-boot-starter-data-redis - true - com.alibaba easyexcel @@ -55,4 +45,4 @@ - \ No newline at end of file + diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/WebCommonAutoConfiguration.java b/commons/web-common/src/main/java/com/schbrain/common/web/WebCommonAutoConfiguration.java index 300af1957683f536f4b4a918d4433d576375eb60..f658e3792fac03f0849f3e66f24202aedb49d4b8 100644 --- a/commons/web-common/src/main/java/com/schbrain/common/web/WebCommonAutoConfiguration.java +++ b/commons/web-common/src/main/java/com/schbrain/common/web/WebCommonAutoConfiguration.java @@ -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 @@ -55,4 +58,4 @@ public class WebCommonAutoConfiguration { return new ResponseBodyHandler(basePackages); } -} \ No newline at end of file +} diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitAspect.java b/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitAspect.java deleted file mode 100644 index 3089a56452a7d47dc729c4db69328678947e22f3..0000000000000000000000000000000000000000 --- a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitAspect.java +++ /dev/null @@ -1,107 +0,0 @@ -package com.schbrain.common.web.support.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.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 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 - * @since 2022/5/5 - */ -@Slf4j -@Aspect -@ConditionalOnBean(StringRedisTemplate.class) -@ConditionalOnClass({Advice.class, RedisConnectionFactory.class}) -public class RateLimitAspect { - - private final Map, RateLimitCacheKeyVariablesContributor> contributorMap = new ConcurrentHashMap<>(); - - private final String keyPrefix; - private final BeanFactory beanFactory; - private final StringRedisTemplate stringRedisTemplate; - - public RateLimitAspect(StringRedisTemplate stringRedisTemplate, BeanFactory beanFactory) { - this.keyPrefix = ApplicationName.get(); - this.beanFactory = beanFactory; - this.stringRedisTemplate = stringRedisTemplate; - } - - @Before("@annotation(rateLimiter)") - public void beforeExecute(JoinPoint joinPoint, RateLimiter rateLimiter) { - MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); - doRateLimit(rateLimiter, joinPoint, methodSignature); - } - - protected void doRateLimit(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) { - Map variables = prepareVariables(rateLimiter, joinPoint, signature); - - String cacheKey = SpelUtils.parse(rateLimiter.cacheKey(), variables, String.class, beanFactory); - if (cacheKey == null) { - throw new BaseException("cacheKey should not be null"); - } - - String formattedCacheKey = formatCacheKey(cacheKey); - BoundValueOperations rateLimitOps = stringRedisTemplate.boundValueOps(formattedCacheKey); - - 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; - } - } - - protected String formatCacheKey(String cacheKey) { - return "rateLimit:" + keyPrefix + ":" + cacheKey; - } - - private Map prepareVariables(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature) { - Map variables = getMethodArgsMap(signature.getMethod(), joinPoint.getArgs()); - Class contributorClass = rateLimiter.contributor(); - if (contributorClass == null || contributorClass == NoOpRateLimitCacheKeyVariablesContributor.class) { - return variables; - } - - RateLimitCacheKeyVariablesContributor contributor = contributorMap.get(contributorClass); - if (contributor == null) { - contributor = BeanUtils.instantiateClass(contributorClass); - contributorMap.put(contributorClass, contributor); - } - Map contributeVariables = contributor.contribute(rateLimiter, joinPoint, signature); - if (MapUtils.isNotEmpty(contributeVariables)) { - variables.putAll(contributeVariables); - } - return variables; - } - -} diff --git a/starters/cache-spring-boot-starter/pom.xml b/starters/cache-spring-boot-starter/pom.xml index 0ff73d253e5e993b4a93c6a0647ce45437363a95..75e466d0ea5d91b8c2a0a373b12825c7145924a7 100644 --- a/starters/cache-spring-boot-starter/pom.xml +++ b/starters/cache-spring-boot-starter/pom.xml @@ -26,6 +26,14 @@ org.apache.commons commons-pool2 + + org.springframework.boot + spring-boot-starter-aop + + + org.redisson + redisson-spring-boot-starter + - \ No newline at end of file + diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/CacheAutoConfiguration.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/CacheAutoConfiguration.java index 72540ee55c6f21e3b902dce98dca723318e60c94..bf6d068b0fae38e29de8b024d3feaf3db1bd01eb 100644 --- a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/CacheAutoConfiguration.java +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/CacheAutoConfiguration.java @@ -1,11 +1,18 @@ 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; } -} \ No newline at end of file + @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())); + } + +} diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/ExpressionParser.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/ExpressionParser.java new file mode 100644 index 0000000000000000000000000000000000000000..d191ee13b50c9b23bef8e0d1f1095a4c67e11b9c --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/ExpressionParser.java @@ -0,0 +1,44 @@ +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 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 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); + } + +} diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java similarity index 87% rename from commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java rename to starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java index 29660bf9ab7abdbe38fac855980c6de37882d261..a728680a71038bc953af4c229d7b4e065777cf65 100644 --- a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/NoOpRateLimitCacheKeyVariablesContributor.java @@ -1,4 +1,4 @@ -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; @@ -17,4 +17,4 @@ public class NoOpRateLimitCacheKeyVariablesContributor implements RateLimitCache return Collections.emptyMap(); } -} \ No newline at end of file +} diff --git a/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/RateLimitAspect.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/RateLimitAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..3e6b6a2203df14d78133f55582b3efc505a19f45 --- /dev/null +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/RateLimitAspect.java @@ -0,0 +1,97 @@ +package com.schbrain.framework.autoconfigure.cache.concurrent; + +import com.schbrain.common.exception.BaseException; +import com.schbrain.common.util.ApplicationName; +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.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.config.ConfigurableListableBeanFactory; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static com.schbrain.common.util.ParameterDiscoverUtils.getMethodArgsMap; + +/** + * @author liaozan + * @see RateLimiter + * @since 2022/5/5 + */ +@Slf4j +@Aspect +public class RateLimitAspect { + + private final Map rateLimiterMap = new ConcurrentHashMap<>(); + private final Map, RateLimitCacheKeyVariablesContributor> contributorMap = new ConcurrentHashMap<>(); + + private final String keyPrefix; + private final RedissonClient redissonClient; + private final ExpressionParser expressionParser; + + public RateLimitAspect(ConfigurableListableBeanFactory beanFactory, RedissonClient redissonClient) { + this.keyPrefix = ApplicationName.get(); + this.redissonClient = redissonClient; + this.expressionParser = new ExpressionParser(beanFactory); + } + + @Before("@annotation(annotation)") + public void beforeExecute(JoinPoint joinPoint, RateLimiter annotation) { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + doRateLimit(annotation, joinPoint, methodSignature); + } + + protected void doRateLimit(RateLimiter annotation, JoinPoint joinPoint, MethodSignature signature) { + Map variables = prepareVariables(annotation, joinPoint, signature); + + String cacheKey = expressionParser.parse(annotation.cacheKey(), variables); + if (cacheKey == null) { + throw new BaseException("cacheKey should not be null"); + } + + String formattedCacheKey = formatCacheKey(cacheKey); + RRateLimiter rateLimiter = rateLimiterMap.computeIfAbsent(formattedCacheKey, key -> createRateLimiter(key, annotation)); + + 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; + } + + 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 prepareVariables(RateLimiter annotation, JoinPoint joinPoint, MethodSignature signature) { + Map variables = getMethodArgsMap(signature.getMethod(), joinPoint.getArgs()); + Class contributorClass = annotation.contributor(); + if (contributorClass == null || contributorClass == NoOpRateLimitCacheKeyVariablesContributor.class) { + return variables; + } + + RateLimitCacheKeyVariablesContributor contributor = contributorMap.get(contributorClass); + if (contributor == null) { + contributor = BeanUtils.instantiateClass(contributorClass); + contributorMap.put(contributorClass, contributor); + } + Map contributeVariables = contributor.contribute(annotation, joinPoint, signature); + if (MapUtils.isNotEmpty(contributeVariables)) { + variables.putAll(contributeVariables); + } + return variables; + } + +} diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitCacheKeyVariablesContributor.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/RateLimitCacheKeyVariablesContributor.java similarity index 83% rename from commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitCacheKeyVariablesContributor.java rename to starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/RateLimitCacheKeyVariablesContributor.java index 1664c1f6a8547cef788b7139a8b3a95972ae43e2..41d0b7db76132adb57aa92f4674683191c6483f9 100644 --- a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimitCacheKeyVariablesContributor.java +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/RateLimitCacheKeyVariablesContributor.java @@ -1,4 +1,4 @@ -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; @@ -13,4 +13,4 @@ public interface RateLimitCacheKeyVariablesContributor { Map contribute(RateLimiter rateLimiter, JoinPoint joinPoint, MethodSignature signature); -} \ No newline at end of file +} diff --git a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimiter.java b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/RateLimiter.java similarity index 92% rename from commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimiter.java rename to starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/RateLimiter.java index 64b0067d4cfb4a9c324204ec2a069677933efdb3..ac04a76b5395841f0cc5f72bf46bc7a962a5b8cf 100644 --- a/commons/web-common/src/main/java/com/schbrain/common/web/support/concurrent/RateLimiter.java +++ b/starters/cache-spring-boot-starter/src/main/java/com/schbrain/framework/autoconfigure/cache/concurrent/RateLimiter.java @@ -1,4 +1,4 @@ -package com.schbrain.common.web.support.concurrent; +package com.schbrain.framework.autoconfigure.cache.concurrent; import java.lang.annotation.*; import java.util.concurrent.TimeUnit;