Skip to content

Commit

Permalink
Support for Method level @ratelimiting annoation #250 (#251)
Browse files Browse the repository at this point in the history
* Support for Method level @ratelimiting annoation #250
  • Loading branch information
MarcGiffing authored Mar 11, 2024
1 parent 7d4cbd9 commit 819217c
Show file tree
Hide file tree
Showing 47 changed files with 1,738 additions and 1,072 deletions.
724 changes: 372 additions & 352 deletions README.adoc

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ public interface Condition<R> {

/**
*
* @param request e.g. to skip or execute rate limit based on the IP address
* @param expressionParams parameters to evaluate the expression
* @return true if the rate limit check should be skipped
*/
boolean evalute(R request);
boolean evaluate(ExpressionParams<R> expressionParams);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.giffing.bucket4j.spring.boot.starter.context;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.expression.Expression;

import java.util.HashMap;
import java.util.Map;

/**
* Parameter information for the evaluation of a Spring {@link Expression}
*
* @param <R> the type of the root object which us used for the SpEl expression.
*/
@RequiredArgsConstructor
public class ExpressionParams<R> {

@Getter
private final R rootObject;

@Getter
private final Map<String, Object> params = new HashMap<>();

public void addParam(String name, Object value) {
params.put(name, value);
}

public ExpressionParams<R> addParams(Map<String, Object> params) {
this.params.putAll(params);
return this;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ public interface KeyFilter<R> {

/**
* Return the unique Bucket4j storage key. You can think of the key as a unique identifier
* which is for example an IP-Address or a user name. The rate limit is then applied to each individual key.
* which is for example an IP-Address or a username. The rate limit is then applied to each individual key.
*
* @param request HTTP request information of the current request
* @return the key to identify the the rate limit (IP, username, ...)
* @param expressionParams the expression params
* @return the key to identify the rate limit (IP, username, ...)
*/
String key(R request);
String key(ExpressionParams<R> expressionParams);

}
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
package com.giffing.bucket4j.spring.boot.starter.context;


import com.giffing.bucket4j.spring.boot.starter.context.properties.RateLimit;

/**
* Used to check if the rate limit should be performed independently from the servlet|webflux|gateway request filter
*
* Used to check if the rate limit should be performed independently from the servlet|webflux|gateway request filter
*/
@FunctionalInterface
public interface RateLimitCheck<R> {

/**
* @param request the request information object
*
* @return null if no rate limit should be performed. (maybe skipped or shouldn't be executed).
*/
RateLimitResultWrapper rateLimit(R request);
/**
* @param params parameter information
* @param mainRateLimit overwrites the rate limit configuration from the properties
* @return null if no rate limit should be performed. (maybe skipped or shouldn't be executed).
*/
RateLimitResultWrapper rateLimit(ExpressionParams<R> params, RateLimit mainRateLimit);

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,18 @@

/**
* Bad name :-)
*
* If multiple rate limits configured this strategy decides when to stop
* the evaluation.
*
*
* <p>
* If multiple rate limits configured this strategy decides when to stop the evaluation.
*/
public enum RateLimitConditionMatchingStrategy {

/**
* All rate limits should be evaluated
*/
ALL,
/**
* Only the first matching rate limit will be evaluated
*/
FIRST,
/**
* All rate limits should be evaluated
*/
ALL,
/**
* Only the first matching rate limit will be evaluated
*/
FIRST,

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.giffing.bucket4j.spring.boot.starter.context;

/**
* This exception is thrown when the rate limit is reached in the context of a method level when using the
* {@link RateLimiting} annotation.
*/
public class RateLimitException extends RuntimeException {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RateLimiting {

/**
* @return The name of the rate limit configuration as a reference to the property file
*/
String name();

/**
* The cache key which is mayby modified the e.g. the method name {@link RateLimiting#ratePerMethod()}
*
* @return the cache key.
*/
String cacheKey() default "";

/**
* An optional execute condition which overrides the execute condition from the property file
*
* @return the expression in the Spring Expression Language format.
*/
String executeCondition() default "";

/**
* An optional execute condition which overrides the execute condition from the property file
*
* @return the expression in the Spring Expression Language format.
*/
String skipCondition() default "";

/**
* The Name of the annotated method will be added to the cache key.
* It's maybe a problem
*
* @return true if the method name should be added to the cache key.
*/
boolean ratePerMethod() default false;

/**
* An optional fall back method when the rate limit occurs instead of throwing an exception.
* The return type must be the same...
*
* TODO
*
* @return the name of the public method which resists in the same class.
*/
String fallbackMethodName() default "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.ArrayList;
import java.util.List;

import com.giffing.bucket4j.spring.boot.starter.context.RateLimiting;
import jakarta.validation.Valid;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
Expand All @@ -15,58 +16,65 @@

/**
* Holds all the relevant starter properties which can be configured with
* Spring Boots application.properties / application.yml configuration files.
* Spring Boots application.properties / application.yml configuration files.
*/
@Data
@ConfigurationProperties(prefix = Bucket4JBootProperties.PROPERTY_PREFIX)
@Validated
public class Bucket4JBootProperties {

public static final String PROPERTY_PREFIX = "bucket4j";

/**
* Enables or disables the Bucket4j Spring Boot Starter.
*/
@NotNull
private Boolean enabled = true;

/**
* Sets the cache implementation which should be auto configured.
* This property can be used if multiple caches are configured by the starter
* and you have to choose one due to a startup error.
* <ul>
* <li>jcache</li>
* <li>hazelcast</li>
* <li>ignite</li>
* <li>redis</li>
* </ul>
*/
private String cacheToUse;

private boolean filterConfigCachingEnabled = false;

/**
* If Filter configuration caching is enabled, a cache with this name should exist, or it will cause an exception.
*/
@NotBlank
private String filterConfigCacheName = "filterConfigCache";

@Valid
private List<Bucket4JConfiguration> filters = new ArrayList<>();

@AssertTrue(message = "FilterConfiguration caching is enabled, but not all filters have an identifier configured")
public boolean isValidFilterIds(){
return !filterConfigCachingEnabled || filters.stream().noneMatch(filter -> filter.getId() == null);
}

/**
* A list of default metric tags which should be applied to all filters
*/
@Valid
private List<MetricTag> defaultMetricTags = new ArrayList<>();

public static String getPropertyPrefix() {
return PROPERTY_PREFIX;
}
public static final String PROPERTY_PREFIX = "bucket4j";

/**
* Enables or disables the Bucket4j Spring Boot Starter.
*/
@NotNull
private Boolean enabled = true;

/**
* Sets the cache implementation which should be auto configured.
* This property can be used if multiple caches are configured by the starter
* and you have to choose one due to a startup error.
* <ul>
* <li>jcache</li>
* <li>hazelcast</li>
* <li>ignite</li>
* <li>redis</li>
* </ul>
*/
private String cacheToUse;

/**
* Configuration for the {@link RateLimiting} annotation on method level.
*/
@Valid
private List<MethodProperties> methods = new ArrayList<>();

private boolean filterConfigCachingEnabled = false;

/**
* If Filter configuration caching is enabled, a cache with this name should exist, or it will cause an exception.
*/
@NotBlank
private String filterConfigCacheName = "filterConfigCache";


@Valid
private List<Bucket4JConfiguration> filters = new ArrayList<>();

@AssertTrue(message = "FilterConfiguration caching is enabled, but not all filters have an identifier configured")
public boolean isValidFilterIds() {
return !filterConfigCachingEnabled || filters.stream().noneMatch(filter -> filter.getId() == null);
}

/**
* A list of default metric tags which should be applied to all filters
*/
@Valid
private List<MetricTag> defaultMetricTags = new ArrayList<>();

public static String getPropertyPrefix() {
return PROPERTY_PREFIX;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.giffing.bucket4j.spring.boot.starter.context.properties;

import com.giffing.bucket4j.spring.boot.starter.context.RateLimiting;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.ToString;

@Data
@ToString
public class MethodProperties {

/**
* The name of the configuration to reference in the {@link RateLimiting} annotation.
*/
@NotBlank
private String name;

/**
* The name of the cache.
*/
@NotBlank
private String cacheName;

/**
* The rate limit configuration
*/
@NotNull
private RateLimit rateLimit;

}
Loading

0 comments on commit 819217c

Please sign in to comment.