Skip to content

Commit

Permalink
Support @ratelimiting on class level #256 (#257)
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcGiffing authored Mar 13, 2024
1 parent 7ce02df commit 5ae9285
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 6 deletions.
22 changes: 22 additions & 0 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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]]
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -107,6 +123,16 @@ public Object processMethodsAnnotatedWithRateLimitAnnotation(ProceedingJoinPoint
return methodResult;
}

private <R extends Annotation> R getAnnotationFromMethodOrClass(Method method, Class<R> 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<Method, Object> rateLimitConfigResult, Method method, Object methodResult) {
for (var rlc : rateLimitConfigResult.getPostRateLimitChecks()) {
var result = rlc.rateLimit(method, methodResult);
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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");
}

}
Original file line number Diff line number Diff line change
@@ -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");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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() {
Expand Down Expand Up @@ -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();
}
});
}

}

0 comments on commit 5ae9285

Please sign in to comment.