Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Method level @RateLimiting annoation #250 #251

Merged
merged 9 commits into from
Mar 11, 2024
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
Loading