forked from mokies/ratelimitj
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
mokies#13 simple spring + aop module
- Loading branch information
Showing
10 changed files
with
316 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
description 'RateLimitJ Spring' | ||
|
||
dependencies { | ||
|
||
api( | ||
project(':ratelimitj-core'), | ||
'io.lettuce:lettuce-core:5.0.1.RELEASE', | ||
'org.springframework:spring-core:4.3.14.RELEASE', | ||
'org.springframework:spring-context:4.3.14.RELEASE' | ||
) | ||
|
||
implementation( | ||
'com.eclipsesource.minimal-json:minimal-json:0.9.4', | ||
'org.aspectj:aspectjrt:1.8.13', | ||
'org.aspectj:aspectjweaver:1.8.13', | ||
libraries.findbugs, | ||
libraries.slf4j, | ||
) | ||
|
||
testImplementation( | ||
project(':ratelimitj-test'), | ||
project(':ratelimitj-inmemory'), | ||
libraries.assertj, | ||
libraries.guava, | ||
libraries.mockito, | ||
libraries.logback, | ||
'org.springframework:spring-test:4.3.14.RELEASE' | ||
) | ||
|
||
} |
13 changes: 13 additions & 0 deletions
13
ratelimitj-spring/src/main/java/es/moki/ratelimitj/spring/RateLimitExceededException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package es.moki.ratelimitj.spring; | ||
|
||
/** | ||
* Exception to be thrown by the aspect method when a rate limiter has exceeded | ||
* | ||
* @author Enrico Costanzi | ||
*/ | ||
public class RateLimitExceededException extends RuntimeException { | ||
|
||
public RateLimitExceededException(String message) { | ||
super(message); | ||
} | ||
} |
86 changes: 86 additions & 0 deletions
86
ratelimitj-spring/src/main/java/es/moki/ratelimitj/spring/RateLimitjAspect.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
package es.moki.ratelimitj.spring; | ||
|
||
import es.moki.ratelimitj.core.limiter.request.RequestRateLimiter; | ||
import es.moki.ratelimitj.spring.annotations.RateLimit; | ||
import org.aspectj.lang.JoinPoint; | ||
import org.aspectj.lang.annotation.Aspect; | ||
import org.aspectj.lang.annotation.Before; | ||
import org.aspectj.lang.annotation.Pointcut; | ||
import org.aspectj.lang.reflect.MethodSignature; | ||
import org.springframework.beans.BeansException; | ||
import org.springframework.context.ApplicationContext; | ||
import org.springframework.context.ApplicationContextAware; | ||
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.lang.reflect.Method; | ||
import java.text.MessageFormat; | ||
|
||
/** | ||
* Class responsible | ||
* @author Enrico Costanzi | ||
*/ | ||
@Aspect | ||
public class RateLimitjAspect implements ApplicationContextAware { | ||
|
||
private ApplicationContext applicationContext; | ||
|
||
@Pointcut("@annotation(es.moki.ratelimitj.spring.annotations.RateLimit)") | ||
public void rateLimitPointcut(){} | ||
|
||
@Before("rateLimitPointcut()") | ||
public void beforeRequestRate(JoinPoint joinPoint) throws RuntimeException { | ||
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); | ||
RateLimit[] rateLimitAnnotation = methodSignature.getMethod().getAnnotationsByType(RateLimit.class); | ||
|
||
for (RateLimit rateLimit : rateLimitAnnotation) { | ||
evaluateRateLimit(methodSignature, joinPoint, rateLimit); | ||
} | ||
|
||
} | ||
|
||
private void evaluateRateLimit(MethodSignature methodSignature, JoinPoint joinPoint, RateLimit rateLimit){ | ||
String rateLimiterName = rateLimit.value(); | ||
RequestRateLimiter rateLimiter = applicationContext.getBean(rateLimiterName, RequestRateLimiter.class); | ||
|
||
String[] parameterNames = methodSignature.getParameterNames(); | ||
EvaluationContext context = new StandardEvaluationContext(); | ||
Object[] parameters = joinPoint.getArgs(); | ||
for (int i = 0; i < parameterNames.length; i++) { | ||
context.setVariable(parameterNames[i], parameters[i]); | ||
} | ||
|
||
ExpressionParser parser = new SpelExpressionParser(); | ||
Expression exp = parser.parseExpression(rateLimit.key()); | ||
String evaluatedSpelExpression = (String) exp.getValue(context); | ||
|
||
String rateLimitKey; | ||
if(rateLimit.methodSignaturePrefix()){ | ||
rateLimitKey = getMethodKey(methodSignature) + "_" + evaluatedSpelExpression; | ||
} else { | ||
rateLimitKey = evaluatedSpelExpression; | ||
} | ||
boolean rateLimitExceeded = rateLimiter.overLimitWhenIncremented(rateLimitKey); | ||
|
||
if(rateLimitExceeded){ | ||
throw new RateLimitExceededException("Rate limit exceeded"); | ||
} | ||
|
||
} | ||
|
||
private String getMethodKey(MethodSignature methodSignature){ | ||
|
||
Method method = methodSignature.getMethod(); | ||
String methodName = method.getName(); | ||
String className = method.getDeclaringClass().getSimpleName(); | ||
return MessageFormat.format("{0}.{1}", className, methodName); | ||
} | ||
|
||
@Override | ||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { | ||
this.applicationContext = applicationContext; | ||
} | ||
} |
29 changes: 29 additions & 0 deletions
29
ratelimitj-spring/src/main/java/es/moki/ratelimitj/spring/annotations/RateLimit.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package es.moki.ratelimitj.spring.annotations; | ||
|
||
import java.lang.annotation.ElementType; | ||
import java.lang.annotation.Retention; | ||
import java.lang.annotation.RetentionPolicy; | ||
import java.lang.annotation.Target; | ||
|
||
/** | ||
* @author Enrico Costanzi | ||
*/ | ||
@Retention(RetentionPolicy.RUNTIME) | ||
@Target(ElementType.METHOD) | ||
public @interface RateLimit { | ||
|
||
/** | ||
* The name of the {@link es.moki.ratelimitj.core.limiter.request.RequestRateLimiter} bean | ||
*/ | ||
String value(); | ||
|
||
/** | ||
* The suffix to be used as key in the rate limiter map. Can be a SpEL expression. | ||
*/ | ||
String key(); | ||
|
||
/** | ||
* True if the method signature has to the prefix for the rate limiter map key. False otherwise. | ||
*/ | ||
boolean methodSignaturePrefix() default true; | ||
} |
52 changes: 52 additions & 0 deletions
52
ratelimitj-spring/src/test/java/es/moki/ratelimitj/spring/test/LimitedServiceTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package es.moki.ratelimitj.spring.test; | ||
|
||
import es.moki.ratelimitj.core.limiter.request.RequestRateLimiter; | ||
import es.moki.ratelimitj.spring.RateLimitExceededException; | ||
import es.moki.ratelimitj.spring.test.config.RateLimitConfiguration; | ||
import es.moki.ratelimitj.spring.test.service.LimitedService; | ||
import org.junit.Test; | ||
import org.junit.runner.RunWith; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.test.context.ContextConfiguration; | ||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; | ||
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; | ||
|
||
/** | ||
* @author Enrico Costanzi | ||
*/ | ||
@RunWith(SpringJUnit4ClassRunner.class) | ||
@ContextConfiguration("classpath:test-application.xml") | ||
public class LimitedServiceTest { | ||
|
||
@Autowired | ||
LimitedService limitedService; | ||
|
||
@Autowired | ||
RequestRateLimiter requestRateLimiter; | ||
|
||
@Test(timeout = RateLimitConfiguration.MAX_DURATION_MILLISECONDS, expected = RateLimitExceededException.class) | ||
public void testRateLimitWithStringParam() throws Exception { | ||
for (int i = 1; i <= RateLimitConfiguration.RATE_LIMIT + 2; i++) { | ||
limitedService.limitedMethodOnStringParam("hello ratelimitj"); | ||
//check i'm not allowed to do 11 or 12 requests | ||
assertThat(i).isLessThanOrEqualTo(RateLimitConfiguration.RATE_LIMIT) ; | ||
} | ||
} | ||
|
||
@Test | ||
public void testPojoParamsAreRead(){ | ||
UserPojo userPojo = new UserPojo("Mr.Paul"); | ||
limitedService.limitedMethodOnUserParamSimpleKey(userPojo); | ||
boolean keyRemoved = requestRateLimiter.resetLimit("Mr.Paul"); | ||
assertThat(keyRemoved).isTrue(); | ||
} | ||
|
||
@Test(timeout = RateLimitConfiguration.MAX_DURATION_MILLISECONDS, expected = RateLimitExceededException.class) | ||
public void testRateLimitWithObjectParam() throws Exception { | ||
UserPojo userPojo = new UserPojo("Mr.Jones"); | ||
for (int i = 1; i <= RateLimitConfiguration.RATE_LIMIT + 2; i++) { | ||
limitedService.limitedMethodOnUserParam(userPojo); | ||
assertThat(i).isLessThanOrEqualTo(RateLimitConfiguration.RATE_LIMIT); | ||
} | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
ratelimitj-spring/src/test/java/es/moki/ratelimitj/spring/test/UserPojo.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package es.moki.ratelimitj.spring.test; | ||
|
||
/** | ||
* @author Enrico Costanzi | ||
*/ | ||
public class UserPojo { | ||
|
||
private String name; | ||
|
||
public UserPojo(String name) { | ||
this.name = name; | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
} | ||
|
||
public void setName(String name) { | ||
this.name = name; | ||
} | ||
} |
48 changes: 48 additions & 0 deletions
48
...tj-spring/src/test/java/es/moki/ratelimitj/spring/test/config/RateLimitConfiguration.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package es.moki.ratelimitj.spring.test.config; | ||
|
||
import es.moki.ratelimitj.core.limiter.request.RequestLimitRule; | ||
import es.moki.ratelimitj.core.limiter.request.RequestRateLimiter; | ||
|
||
import es.moki.ratelimitj.inmemory.request.InMemorySlidingWindowRequestRateLimiter; | ||
import es.moki.ratelimitj.spring.test.service.LimitedService; | ||
import es.moki.ratelimitj.spring.RateLimitjAspect; | ||
import org.springframework.beans.factory.annotation.Autowired; | ||
import org.springframework.context.ApplicationContext; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
|
||
import java.util.Collections; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
/** | ||
* @author Enrico Costanzi | ||
*/ | ||
@Configuration | ||
public class RateLimitConfiguration { | ||
|
||
public static final int MAX_DURATION_MILLISECONDS = 1000; | ||
public static final int RATE_LIMIT = 10; | ||
|
||
@Bean | ||
public RequestLimitRule rule() { | ||
return RequestLimitRule.of(MAX_DURATION_MILLISECONDS, TimeUnit.MILLISECONDS, RATE_LIMIT); | ||
} | ||
|
||
@Bean(name = "defaultLimiter") | ||
public RequestRateLimiter requestRateLimiter(@Autowired RequestLimitRule requestLimitRule){ | ||
return new InMemorySlidingWindowRequestRateLimiter(Collections.singleton(requestLimitRule)); | ||
} | ||
|
||
@Bean | ||
public LimitedService limitedService() { | ||
return new LimitedService(); | ||
} | ||
|
||
@Bean | ||
public RateLimitjAspect ratelimitjAspect(ApplicationContext applicationContext){ | ||
RateLimitjAspect ratelimitjAspect = new RateLimitjAspect(); | ||
ratelimitjAspect.setApplicationContext(applicationContext); | ||
return ratelimitjAspect; | ||
} | ||
|
||
} |
24 changes: 24 additions & 0 deletions
24
ratelimitj-spring/src/test/java/es/moki/ratelimitj/spring/test/service/LimitedService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package es.moki.ratelimitj.spring.test.service; | ||
|
||
import es.moki.ratelimitj.spring.annotations.RateLimit; | ||
import es.moki.ratelimitj.spring.test.UserPojo; | ||
|
||
/** | ||
* @author Enrico Costanzi | ||
*/ | ||
public class LimitedService { | ||
|
||
@RateLimit(value = "defaultLimiter", key = "#param") | ||
public void limitedMethodOnStringParam(String param){ | ||
} | ||
|
||
@RateLimit(value = "defaultLimiter", key = "#user.name") | ||
public void limitedMethodOnUserParam(UserPojo user){ | ||
|
||
} | ||
|
||
@RateLimit(value = "defaultLimiter", key = "#user.name", methodSignaturePrefix = false) | ||
public void limitedMethodOnUserParamSimpleKey(UserPojo user){ | ||
|
||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xmlns:aop="http://www.springframework.org/schema/aop" | ||
xmlns:context="http://www.springframework.org/schema/context" | ||
xmlns="http://www.springframework.org/schema/beans" | ||
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> | ||
|
||
<context:annotation-config/> | ||
<aop:aspectj-autoproxy/> | ||
|
||
<bean class="es.moki.ratelimitj.spring.test.config.RateLimitConfiguration"/> | ||
|
||
</beans> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters