From b3d36945b5e8b2414ffc2a2073a45bf9fbc35832 Mon Sep 17 00:00:00 2001 From: Marc Giffing Date: Tue, 12 Mar 2024 20:58:15 +0100 Subject: [PATCH] Support @RateLimiting on class level #256 --- README.adoc | 22 ++++++++++++ .../starter/context/IgnoreRateLimiting.java | 14 ++++++++ .../boot/starter/context/RateLimiting.java | 2 +- ...{AopConfig.java => Bucket4jAopConfig.java} | 2 +- .../config/aspect/RateLimitAspect.java | 30 ++++++++++++++-- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- .../method/method/ClassLevelTestService.java | 22 ++++++++++++ .../method/IgnoreOnClassLevelTestService.java | 18 ++++++++++ .../method/method/MethodRateLimitTest.java | 36 ++++++++++++++++++- 9 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 bucket4j-spring-boot-starter-context/src/main/java/com/giffing/bucket4j/spring/boot/starter/context/IgnoreRateLimiting.java rename bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/{AopConfig.java => Bucket4jAopConfig.java} (98%) create mode 100644 examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/ClassLevelTestService.java create mode 100644 examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/IgnoreOnClassLevelTestService.java diff --git a/README.adoc b/README.adoc index 5da7349c..7798d0bb 100644 --- a/README.adoc +++ b/README.adoc @@ -124,6 +124,28 @@ bucket4j.methods[0].rate-limits[0].bandwidths[0].refill-speed=intervall } ---- +The '@RateLimiting' annotation on class level executes the rate limit on all public methods of the class. With '@IgnoreRateLimiting' you can ignore the rate limit at all on class level or for specific method on method level. + + +[source,java] +---- +@Component +@Slf4j +@RateLimiting(name = "default") +public class TestService { + + public void notAnnotatedMethod() { + log.info("Method notAnnotatedMethod"); + } + + @IgnoreRateLimiting + public void ignoreMethod() { + log.info("Method ignoreMethod"); + } + +} +---- + You can find some Configuration examples in the test project: {url-examples}/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method[Examples] [[project_configuration]] diff --git a/bucket4j-spring-boot-starter-context/src/main/java/com/giffing/bucket4j/spring/boot/starter/context/IgnoreRateLimiting.java b/bucket4j-spring-boot-starter-context/src/main/java/com/giffing/bucket4j/spring/boot/starter/context/IgnoreRateLimiting.java new file mode 100644 index 00000000..ac055570 --- /dev/null +++ b/bucket4j-spring-boot-starter-context/src/main/java/com/giffing/bucket4j/spring/boot/starter/context/IgnoreRateLimiting.java @@ -0,0 +1,14 @@ +package com.giffing.bucket4j.spring.boot.starter.context; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Ignores the rate limiting annotation for a class or method + */ +@Target(value = { ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface IgnoreRateLimiting { +} diff --git a/bucket4j-spring-boot-starter-context/src/main/java/com/giffing/bucket4j/spring/boot/starter/context/RateLimiting.java b/bucket4j-spring-boot-starter-context/src/main/java/com/giffing/bucket4j/spring/boot/starter/context/RateLimiting.java index 4ce760c2..89a60931 100644 --- a/bucket4j-spring-boot-starter-context/src/main/java/com/giffing/bucket4j/spring/boot/starter/context/RateLimiting.java +++ b/bucket4j-spring-boot-starter-context/src/main/java/com/giffing/bucket4j/spring/boot/starter/context/RateLimiting.java @@ -5,7 +5,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -@Target(ElementType.METHOD) +@Target(value = { ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimiting { diff --git a/bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/AopConfig.java b/bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/Bucket4jAopConfig.java similarity index 98% rename from bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/AopConfig.java rename to bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/Bucket4jAopConfig.java index a9e8570d..e8cc8b2a 100644 --- a/bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/AopConfig.java +++ b/bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/Bucket4jAopConfig.java @@ -28,7 +28,7 @@ @AutoConfigureAfter(value = { CacheAutoConfiguration.class, Bucket4jCacheConfiguration.class }) @ConditionalOnBean(value = SyncCacheResolver.class) @Import(value = {ServiceConfiguration.class, Bucket4jCacheConfiguration.class, SpringBootActuatorConfig.class}) -public class AopConfig { +public class Bucket4jAopConfig { @Bean public RateLimitAspect rateLimitAspect(RateLimitService rateLimitService, Bucket4JBootProperties bucket4JBootProperties, SyncCacheResolver syncCacheResolver) { diff --git a/bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/RateLimitAspect.java b/bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/RateLimitAspect.java index 81f286ac..85a87e36 100644 --- a/bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/RateLimitAspect.java +++ b/bucket4j-spring-boot-starter/src/main/java/com/giffing/bucket4j/spring/boot/starter/config/aspect/RateLimitAspect.java @@ -16,6 +16,7 @@ import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; @@ -58,15 +59,30 @@ public void init() { } } + @Pointcut("execution(public * *(..))") + public void publicMethod() {} + @Pointcut("@annotation(com.giffing.bucket4j.spring.boot.starter.context.RateLimiting)") private void methodsAnnotatedWithRateLimitAnnotation() { } - @Around("methodsAnnotatedWithRateLimitAnnotation()") + @Pointcut("@within(com.giffing.bucket4j.spring.boot.starter.context.RateLimiting) && publicMethod()") + private void classAnnotatedWithRateLimitAnnotation(){ + + } + + @Around("methodsAnnotatedWithRateLimitAnnotation() || classAnnotatedWithRateLimitAnnotation()") public Object processMethodsAnnotatedWithRateLimitAnnotation(ProceedingJoinPoint joinPoint) throws Throwable { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); - RateLimiting rateLimitAnnotation = method.getAnnotation(RateLimiting.class); + + var ignoreRateLimitAnnotation = getAnnotationFromMethodOrClass(method, IgnoreRateLimiting.class); + // if the class or method is annotated with IgnoreRateLimiting we will skip rate limiting + if(ignoreRateLimitAnnotation != null){ + return joinPoint.proceed(); + } + + var rateLimitAnnotation = getAnnotationFromMethodOrClass(method, RateLimiting.class); Method fallbackMethod = null; if(rateLimitAnnotation.fallbackMethodName() != null) { @@ -107,6 +123,16 @@ public Object processMethodsAnnotatedWithRateLimitAnnotation(ProceedingJoinPoint return methodResult; } + private R getAnnotationFromMethodOrClass(Method method, Class rateLimitingAnnotation) { + R rateLimitAnnotation; + if(method.getAnnotation(rateLimitingAnnotation) != null) { + rateLimitAnnotation = method.getAnnotation(rateLimitingAnnotation); + } else { + rateLimitAnnotation = method.getDeclaringClass().getAnnotation(rateLimitingAnnotation); + } + return rateLimitAnnotation; + } + private static void performPostRateLimit(RateLimitService.RateLimitConfigresult rateLimitConfigResult, Method method, Object methodResult) { for (var rlc : rateLimitConfigResult.getPostRateLimitChecks()) { var result = rlc.rateLimit(method, methodResult); diff --git a/bucket4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/bucket4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 3ef6295b..498c0d0d 100644 --- a/bucket4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/bucket4j-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,5 +1,5 @@ com.giffing.bucket4j.spring.boot.starter.config.cache.Bucket4jCacheConfiguration -com.giffing.bucket4j.spring.boot.starter.config.aspect.AopConfig +com.giffing.bucket4j.spring.boot.starter.config.aspect.Bucket4jAopConfig com.giffing.bucket4j.spring.boot.starter.config.filter.reactive.gateway.Bucket4JAutoConfigurationSpringCloudGatewayFilter com.giffing.bucket4j.spring.boot.starter.config.filter.servlet.Bucket4JAutoConfigurationServletFilter com.giffing.bucket4j.spring.boot.starter.config.filter.reactive.webflux.Bucket4JAutoConfigurationWebfluxFilter diff --git a/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/ClassLevelTestService.java b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/ClassLevelTestService.java new file mode 100644 index 00000000..7ede0d3e --- /dev/null +++ b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/ClassLevelTestService.java @@ -0,0 +1,22 @@ +package com.giffing.bucket4j.spring.boot.starter.general.tests.method.method; + +import com.giffing.bucket4j.spring.boot.starter.context.IgnoreRateLimiting; +import com.giffing.bucket4j.spring.boot.starter.context.RateLimiting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@RateLimiting(name = "default") +public class ClassLevelTestService { + + public void notAnnotatedMethod() { + log.info("Method notAnnotatedMethod"); + } + + @IgnoreRateLimiting + public void ignoreMethod() { + log.info("Method ignoreMethod"); + } + +} diff --git a/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/IgnoreOnClassLevelTestService.java b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/IgnoreOnClassLevelTestService.java new file mode 100644 index 00000000..0bf9d746 --- /dev/null +++ b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/IgnoreOnClassLevelTestService.java @@ -0,0 +1,18 @@ +package com.giffing.bucket4j.spring.boot.starter.general.tests.method.method; + +import com.giffing.bucket4j.spring.boot.starter.context.IgnoreRateLimiting; +import com.giffing.bucket4j.spring.boot.starter.context.RateLimiting; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +@IgnoreRateLimiting +public class IgnoreOnClassLevelTestService { + + @RateLimiting(name = "default") + public void execute() { + log.info("Method execute"); + } + +} diff --git a/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/MethodRateLimitTest.java b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/MethodRateLimitTest.java index 21e33600..cdb68d1a 100644 --- a/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/MethodRateLimitTest.java +++ b/examples/general-tests/src/main/java/com/giffing/bucket4j/spring/boot/starter/general/tests/method/method/MethodRateLimitTest.java @@ -10,7 +10,6 @@ import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(properties = { - "debug=true", "bucket4j.methods[0].name=default", "bucket4j.methods[0].cache-name=buckets", "bucket4j.methods[0].rate-limit.bandwidths[0].capacity=5", @@ -25,6 +24,12 @@ public class MethodRateLimitTest { @Autowired private TestService testService; + @Autowired + private ClassLevelTestService classLevelTestService; + + @Autowired + private IgnoreOnClassLevelTestService ignoreOnClassLevelTestService; + @Test public void assert_rate_limit_with_execute_condition_matches() { @@ -97,4 +102,33 @@ public void assert_rate_limit_with_rate_per_method() { assertThrows(RateLimitException.class, () -> testService.withRatePerMethod2("key2")); } + @Test + public void assert_rate_limit_with_class_level_rate_limit() { + for(int i = 0; i < 5; i++) { + // rate limit executed because it's not the admin + classLevelTestService.notAnnotatedMethod(); + } + assertThrows(RateLimitException.class, () -> classLevelTestService.notAnnotatedMethod()); + } + + @Test + public void assert_no_rate_limit_with_ignored_method() { + assertAll(() -> { + for (int i = 0; i < 20; i++) { + // rate limit executed because it's not the admin + classLevelTestService.ignoreMethod(); + } + }); + } + + @Test + public void assert_no_rate_limit_with_ignored_class() { + assertAll(() -> { + for (int i = 0; i < 20; i++) { + // rate limit executed because it's not the admin + classLevelTestService.ignoreMethod(); + } + }); + } + }