Skip to content

Commit

Permalink
feat: annotations and aspect added
Browse files Browse the repository at this point in the history
  • Loading branch information
Changolaxtra committed Dec 12, 2023
1 parent 740e39a commit a65dffa
Show file tree
Hide file tree
Showing 21 changed files with 258 additions and 89 deletions.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.changolaxtra.cloud.ratelimiter.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ApiRateLimited {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.changolaxtra.cloud.ratelimiter.annotations;

import org.springframework.context.annotation.ComponentScan;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@ComponentScan(basePackages = "com.changolaxtra.cloud.ratelimiter")
public @interface EnableApiRateLimiter {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.changolaxtra.cloud.ratelimiter.aspect;

import com.changolaxtra.cloud.ratelimiter.configuration.RateLimiterConfiguration;
import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket;
import com.changolaxtra.cloud.ratelimiter.storage.PlanLimitStorage;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Optional;

@Slf4j
@Aspect
@Component
public class ApiRateLimitedAspect {

private final PlanLimitStorage planLimitStorage;
private final RateLimiterConfiguration rateLimiterConfiguration;

public ApiRateLimitedAspect(final PlanLimitStorage planLimitStorage,
final RateLimiterConfiguration rateLimiterConfiguration) {
this.planLimitStorage = planLimitStorage;
this.rateLimiterConfiguration = rateLimiterConfiguration;
}

@Around("@annotation(com.changolaxtra.cloud.ratelimiter.annotations.ApiRateLimited)")
public Object rateLimitCheck(final ProceedingJoinPoint joinPoint) throws Throwable {
final String apiKey = getApiKey();
final PlanLimitBucket planLimit = planLimitStorage.getPlanLimit(apiKey);
if (planLimit.isAllowed()) {
return joinPoint.proceed();
} else {
return ResponseEntity
.status(HttpStatus.TOO_MANY_REQUESTS)
.body("API-Keys does not allow more calls at this moment.");
}
}

private String getApiKey() {
return Optional.ofNullable(RequestContextHolder.getRequestAttributes())
.map(requestAttributes -> (ServletRequestAttributes) requestAttributes)
.map(ServletRequestAttributes::getRequest)
.map(request -> request.getHeader(rateLimiterConfiguration.getApiKeyHeaderName()))
.orElse(rateLimiterConfiguration.getDefaultApiKey());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.changolaxtra.cloud.ratelimiter.configuration;

import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket;
import com.changolaxtra.cloud.ratelimiter.exception.RateLimitException;
import com.changolaxtra.cloud.ratelimiter.core.policy.RateLimitPolicy;
import com.changolaxtra.cloud.ratelimiter.storage.PlanLimitStorage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class DefaultPlanLimitInitializer implements InitializingBean {

private final PlanLimitStorage planLimitStorage;
private final RateLimiterConfiguration rateLimiterConfiguration;

public DefaultPlanLimitInitializer(final PlanLimitStorage planLimitStorage,
final RateLimiterConfiguration rateLimiterConfiguration) {
this.planLimitStorage = planLimitStorage;
this.rateLimiterConfiguration = rateLimiterConfiguration;
}


@Override
public void afterPropertiesSet() throws Exception {
final RateLimitPolicy defaultPolicy = getDefaultPolicy();
final boolean isDefaultPolicyCreated = planLimitStorage.saveOrUpdate(new PlanLimitBucket(defaultPolicy));
if (!isDefaultPolicyCreated) {
log.error("Default policy wasn't created.");
throw new RateLimitException("Default policy wasn't created.");
}
log.info("Default policy was created correctly: {}", defaultPolicy);
}

private RateLimitPolicy getDefaultPolicy() {
return RateLimitPolicy
.builder()
.apiKey(rateLimiterConfiguration.getDefaultApiKey())
.allowedRequests(rateLimiterConfiguration.getDefaultAllowedRequests())
.windowSizeInMilliSeconds(rateLimiterConfiguration.getDefaultWindowSizeInMilliSeconds())
.isUnlimited(rateLimiterConfiguration.isDefaultIsUnlimited())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
package com.changolaxtra.cloud.ratelimiter.configuration;

import lombok.Getter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Getter
@Configuration
public class RateLimiterConfiguration {

@Value("${rate-limiter.apiKey-header-name}")
private String apiKeyHeaderName;

@Value("${rate-limiter.default-policy.apiKey}")
private String defaultApiKey;

@Value("${rate-limiter.default-policy.allowedRequests}")
private long defaultAllowedRequests;

@Value("${rate-limiter.default-policy.windowSizeInMilliSeconds}")
private long defaultWindowSizeInMilliSeconds;

@Value("${rate-limiter.default-policy.isUnlimited}")
private boolean defaultIsUnlimited;

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.changolaxtra.cloud.ratelimiter.configuration;

import com.changolaxtra.cloud.ratelimiter.data.ApiLimitDataRoot;
import com.changolaxtra.cloud.ratelimiter.core.PlanLimitDataRoot;
import lombok.extern.slf4j.Slf4j;
import org.eclipse.store.storage.embedded.types.EmbeddedStorage;
import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
Expand All @@ -15,7 +15,7 @@ public class StorageManagerConfiguration {
@Bean
public EmbeddedStorageManager.Default embeddedStorageManager() {
final EmbeddedStorageManager.Default storageManager =
(EmbeddedStorageManager.Default) EmbeddedStorage.start(new ApiLimitDataRoot());
(EmbeddedStorageManager.Default) EmbeddedStorage.start(new PlanLimitDataRoot());
storageManager.storeRoot();
return storageManager;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.changolaxtra.cloud.ratelimiter.core;

import com.changolaxtra.cloud.ratelimiter.core.policy.RateLimitPolicy;
import lombok.EqualsAndHashCode;

@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class PlanLimitBucket {

@EqualsAndHashCode.Include
private final String apiKey;
private final TokenBucket tokenBucket;

public PlanLimitBucket(final RateLimitPolicy rateLimitPolicy) {
this.apiKey = rateLimitPolicy.getApiKey();
this.tokenBucket = new TokenBucket(rateLimitPolicy);
}

public String getApiKey() {
return apiKey;
}

public boolean isAllowed() {
return tokenBucket.isAllowed();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.changolaxtra.cloud.ratelimiter.core;

import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket;
import lombok.Getter;

import java.util.HashSet;
import java.util.Set;

@Getter
public class PlanLimitDataRoot {
private final Set<PlanLimitBucket> planLimitBuckets;

public PlanLimitDataRoot() {
planLimitBuckets = new HashSet<>();
}

public boolean addPlanLimitBucket(final PlanLimitBucket planLimitBucket) {
return planLimitBuckets.add(planLimitBucket);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.changolaxtra.cloud.ratelimiter.core;

import com.changolaxtra.cloud.ratelimiter.policy.RateLimitPolicy;
import com.changolaxtra.cloud.ratelimiter.core.policy.RateLimitPolicy;

public class TokenBucket {

Expand All @@ -20,7 +20,7 @@ public TokenBucket(final RateLimitPolicy rateLimitPolicy) {
this.refill();
}

public boolean processRequest() {
public boolean isAllowed() {
return isUnlimited || processLimitedRequest();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.changolaxtra.cloud.ratelimiter.policy;
package com.changolaxtra.cloud.ratelimiter.core.policy;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;

@Getter
@Builder
@AllArgsConstructor
@ToString
public class RateLimitPolicy {
private String apiKey;
private long allowedRequests;
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.changolaxtra.cloud.ratelimiter.exception;

public class RateLimitException extends RuntimeException {

public RateLimitException(final String message) {
super(message);
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.changolaxtra.cloud.ratelimiter.storage;

import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket;
import com.changolaxtra.cloud.ratelimiter.core.PlanLimitDataRoot;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.store.storage.embedded.types.EmbeddedStorageManager;
import org.springframework.stereotype.Repository;

import java.util.HashSet;
import java.util.Optional;

@Repository
public class EmbeddedPlanLimitStorage implements PlanLimitStorage {

private final EmbeddedStorageManager.Default embeddedStorageManager;

public EmbeddedPlanLimitStorage(final EmbeddedStorageManager.Default embeddedStorageManager) {
this.embeddedStorageManager = embeddedStorageManager;
}

@Override
public PlanLimitBucket getPlanLimit(final String apiKey) {
final PlanLimitDataRoot dataRoot = (PlanLimitDataRoot) embeddedStorageManager.root();
return Optional.ofNullable(dataRoot)
.map(PlanLimitDataRoot::getPlanLimitBuckets)
.orElse(new HashSet<>())
.stream()
.filter(planLimitBucket -> StringUtils.equalsIgnoreCase(apiKey, planLimitBucket.getApiKey()))
.findFirst()
.orElse(null);
}

@Override
public boolean saveOrUpdate(final PlanLimitBucket planLimitBucket) {
final PlanLimitDataRoot dataRoot = (PlanLimitDataRoot) embeddedStorageManager.root();
return Optional.ofNullable(dataRoot)
.map(root -> root.addPlanLimitBucket(planLimitBucket))
.orElse(false);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.changolaxtra.cloud.ratelimiter.storage;

import com.changolaxtra.cloud.ratelimiter.core.PlanLimitBucket;

public interface PlanLimitStorage {
PlanLimitBucket getPlanLimit(String apiKey);
boolean saveOrUpdate(PlanLimitBucket planLimitBucket);
}
1 change: 0 additions & 1 deletion src/main/resources/application.properties

This file was deleted.

7 changes: 7 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
rate-limiter:
apiKey-header-name: "X-API-Key"
default-policy:
apiKey: ""
isUnlimited: false
allowedRequests: 1
windowSizeInMilliSeconds: 60
Loading

0 comments on commit a65dffa

Please sign in to comment.