Skip to content

Commit

Permalink
mokies#13 simple spring + aop module
Browse files Browse the repository at this point in the history
  • Loading branch information
ecostanzi committed Jan 30, 2018
1 parent b95fdfa commit e48ea8d
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 0 deletions.
30 changes: 30 additions & 0 deletions ratelimitj-spring/build.gradle
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'
)

}
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);
}
}
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;
}
}
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;
}
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);
}
}
}
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;
}
}
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;
}

}
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){

}
}
12 changes: 12 additions & 0 deletions ratelimitj-spring/src/test/resources/test-application.xml
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>
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ include 'ratelimitj-hazelcast'
include 'ratelimitj-inmemory'
include 'ratelimitj-dropwizard'
include 'ratelimitj-test'
include 'ratelimitj-spring'

0 comments on commit e48ea8d

Please sign in to comment.