From 167655d659ea258532b4d0ea75cb6763452e2ca7 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 01:56:55 +0530 Subject: [PATCH 01/14] add bucket4j deps --- .../spring-boot-libraries/pom.xml | 213 ++++++++++-------- 1 file changed, 122 insertions(+), 91 deletions(-) diff --git a/spring-boot-modules/spring-boot-libraries/pom.xml b/spring-boot-modules/spring-boot-libraries/pom.xml index 090967d8a8..36b9ec17c9 100644 --- a/spring-boot-modules/spring-boot-libraries/pom.xml +++ b/spring-boot-modules/spring-boot-libraries/pom.xml @@ -87,7 +87,35 @@ javase ${zxing.version} - + + + com.github.vladimir-bukhtoyarov + bucket4j-core + ${bucket4j.version} + + + com.giffing.bucket4j.spring.boot.starter + bucket4j-spring-boot-starter + ${bucket4j-spring-boot-starter.version} + + + org.springframework.boot + spring-boot-starter-cache + + + javax.cache + cache-api + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + com.github.ben-manes.caffeine + jcache + ${caffeine.version} + @@ -97,109 +125,112 @@ - - spring-boot-libraries - - - src/main/resources - true - - + + spring-boot-libraries + + + src/main/resources + true + + - + - - org.apache.maven.plugins - maven-war-plugin - + + org.apache.maven.plugins + maven-war-plugin + - - pl.project13.maven - git-commit-id-plugin - ${git-commit-id-plugin.version} - - - get-the-git-infos - - revision - - initialize - - - validate-the-git-infos - - validateRevision - - package - - - - true - ${project.build.outputDirectory}/git.properties - - + + pl.project13.maven + git-commit-id-plugin + ${git-commit-id-plugin.version} + + + get-the-git-infos + + revision + + initialize + + + validate-the-git-infos + + validateRevision + + package + + + + true + ${project.build.outputDirectory}/git.properties + + - - - autoconfiguration - - - - org.apache.maven.plugins - maven-surefire-plugin - - - integration-test - - test - - - - **/*LiveTest.java - **/*IntegrationTest.java - **/*IntTest.java - - - **/AutoconfigurationTest.java - - - - - - - json - - - - - - - + + + autoconfiguration + + + + org.apache.maven.plugins + maven-surefire-plugin + + + integration-test + + test + + + + **/*LiveTest.java + **/*IntegrationTest.java + **/*IntTest.java + + + **/AutoconfigurationTest.java + + + + + + + json + + + + + + + - - + + com.baeldung.intro.App 8.5.11 2.4.1.Final 1.9.0 2.0.0 - 5.0.2 - 5.0.2 - 5.2.4 - 18.0 - 2.2.4 - 2.3.2 - 0.23.0 - 1.4.200 - 2.1.0 - 1.5-beta1 - 2.1 - 2.6.0 - 3.3.0 - + 5.0.2 + 5.0.2 + 5.2.4 + 18.0 + 2.2.4 + 2.3.2 + 0.23.0 + 1.4.200 + 2.1.0 + 1.5-beta1 + 2.1 + 2.6.0 + 3.3.0 + 4.10.0 + 0.2.0 + 2.8.2 + From e8c383d10f5b15bf741c7ba1e0dd9b66852317f6 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 01:58:05 +0530 Subject: [PATCH 02/14] area api impl --- .../controller/AreaCalculationController.java | 29 +++++++++++++++++++ .../com/baeldung/ratelimiting/dto/AreaV1.java | 20 +++++++++++++ .../dto/RectangleDimensionsV1.java | 15 ++++++++++ .../dto/TriangleDimensionsV1.java | 15 ++++++++++ 4 files changed, 79 insertions(+) create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/controller/AreaCalculationController.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/AreaV1.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/RectangleDimensionsV1.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/TriangleDimensionsV1.java diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/controller/AreaCalculationController.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/controller/AreaCalculationController.java new file mode 100644 index 0000000000..f3fb63ebdd --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/controller/AreaCalculationController.java @@ -0,0 +1,29 @@ +package com.baeldung.ratelimiting.controller; + +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.baeldung.ratelimiting.dto.AreaV1; +import com.baeldung.ratelimiting.dto.RectangleDimensionsV1; +import com.baeldung.ratelimiting.dto.TriangleDimensionsV1; + +@RestController +@RequestMapping(value = "/api/v1/area", consumes = MediaType.APPLICATION_JSON_VALUE) +class AreaCalculationController { + + @PostMapping(value = "/rectangle", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity rectangle(@RequestBody RectangleDimensionsV1 dimensions) { + + return ResponseEntity.ok(new AreaV1("rectangle", dimensions.getLength() * dimensions.getWidth())); + } + + @PostMapping(value = "/triangle", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity triangle(@RequestBody TriangleDimensionsV1 dimensions) { + + return ResponseEntity.ok(new AreaV1("triangle", 0.5d * dimensions.getHeight() * dimensions.getBase())); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/AreaV1.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/AreaV1.java new file mode 100644 index 0000000000..78097f55b2 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/AreaV1.java @@ -0,0 +1,20 @@ +package com.baeldung.ratelimiting.dto; + +public class AreaV1 { + + private String shape; + private Double area; + + public AreaV1(String shape, Double area) { + this.area = area; + this.shape = shape; + } + + public Double getArea() { + return area; + } + + public String getShape() { + return shape; + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/RectangleDimensionsV1.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/RectangleDimensionsV1.java new file mode 100644 index 0000000000..e3c17e1ba7 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/RectangleDimensionsV1.java @@ -0,0 +1,15 @@ +package com.baeldung.ratelimiting.dto; + +public class RectangleDimensionsV1 { + + private double length; + private double width; + + public double getLength() { + return length; + } + + public double getWidth() { + return width; + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/TriangleDimensionsV1.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/TriangleDimensionsV1.java new file mode 100644 index 0000000000..44c954bded --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/dto/TriangleDimensionsV1.java @@ -0,0 +1,15 @@ +package com.baeldung.ratelimiting.dto; + +public class TriangleDimensionsV1 { + + private double base; + private double height; + + public double getBase() { + return base; + } + + public double getHeight() { + return height; + } +} From 4e45b8f44ef8c0b5d0e8c6b3a18f6d07be7cf796 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 01:58:40 +0530 Subject: [PATCH 03/14] spring mvc using interceptor --- .../bucket4japp/Bucket4jRateLimitingApp.java | 29 +++++++++ .../bucket4japp/interceptor/PricingPlan.java | 48 ++++++++++++++ .../interceptor/RateLimitingInterceptor.java | 65 +++++++++++++++++++ .../ratelimiting/application-bucket4j.yml | 10 +++ 4 files changed, 152 insertions(+) create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java new file mode 100644 index 0000000000..2a42448b35 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java @@ -0,0 +1,29 @@ +package com.baeldung.ratelimiting.bucket4japp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import com.baeldung.ratelimiting.bucket4japp.interceptor.RateLimitingInterceptor; + +@SpringBootApplication(scanBasePackages = "com.baeldung.ratelimiting", exclude = { + DataSourceAutoConfiguration.class, + SecurityAutoConfiguration.class +}) +public class Bucket4jRateLimitingApp implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new RateLimitingInterceptor()) + .addPathPatterns("/api/v1/area/**"); + } + + public static void main(String[] args) { + new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) + .properties("spring.config.location=classpath:ratelimiting/application-bucket4j.yml") + .run(args); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java new file mode 100644 index 0000000000..e2b3ccb6c6 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java @@ -0,0 +1,48 @@ +package com.baeldung.ratelimiting.bucket4japp.interceptor; + +import java.time.Duration; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Refill; + +enum PricingPlan { + + FREE { + + @Override + Bandwidth getLimit() { + return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1))); + } + }, + + BASIC { + + @Override + Bandwidth getLimit() { + return Bandwidth.classic(40, Refill.intervally(40, Duration.ofHours(1))); + } + }, + + PROFESSIONAL { + + @Override + Bandwidth getLimit() { + return Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1))); + } + }; + + abstract Bandwidth getLimit(); + + static PricingPlan resolvePlanFromApiKey(String apiKey) { + if (apiKey == null || apiKey.isEmpty()) { + return FREE; + + } else if (apiKey.startsWith("PX001-")) { + return PROFESSIONAL; + + } else if (apiKey.startsWith("BX001-")) { + return BASIC; + } + return FREE; + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java new file mode 100644 index 0000000000..8aa8de531c --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java @@ -0,0 +1,65 @@ +package com.baeldung.ratelimiting.bucket4japp.interceptor; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.web.servlet.HandlerInterceptor; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.ConsumptionProbe; + +public class RateLimitingInterceptor implements HandlerInterceptor { + + private static final String HEADER_API_KEY = "X-api-key"; + private static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining"; + private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Milliseconds"; + + private final Map cache = new ConcurrentHashMap<>(); + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + String apiKey = request.getHeader(HEADER_API_KEY); + + if (apiKey == null || apiKey.isEmpty()) { + response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: " + HEADER_API_KEY); + return false; + } + + Bucket tokenBucket = cache.computeIfAbsent(apiKey, this::resolveBucket); + + ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1); + + if (probe.isConsumed()) { + + response.addHeader(HEADER_LIMIT_REMAINING, String.valueOf(probe.getRemainingTokens())); + return true; + + } else { + + long waitForRefillMilli = probe.getNanosToWaitForRefill() % 1_000_000; + + response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota"); // 429 + response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefillMilli)); + + return false; + } + } + + private Bucket resolveBucket(String apiKey) { + PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey); + return bucket(pricingPlan.getLimit()); + } + + private Bucket bucket(Bandwidth limit) { + return Bucket4j.builder() + .addLimit(limit) + .build(); + } +} \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml new file mode 100644 index 0000000000..1fb4d2cf12 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml @@ -0,0 +1,10 @@ +server: + port: 9000 + +spring: + application: + name: bucket4j-api-rate-limiting-app + mvc: + throw-exception-if-no-handler-found: true + resources: + add-mappings: false From 1b5a64c4a49b7fc9017d487aaab79ad01713a39d Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 01:59:03 +0530 Subject: [PATCH 04/14] bucket4j spring boot starter --- .../Bucket4jRateLimitingApp.java | 21 ++++++++++ .../application-bucket4j-starter.yml | 41 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java new file mode 100644 index 0000000000..de2ab41bf0 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java @@ -0,0 +1,21 @@ +package com.baeldung.ratelimiting.bootstarterapp; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cache.annotation.EnableCaching; + +@SpringBootApplication(scanBasePackages = "com.baeldung.ratelimiting", exclude = { + DataSourceAutoConfiguration.class, + SecurityAutoConfiguration.class, +}) +@EnableCaching +public class Bucket4jRateLimitingApp { + + public static void main(String[] args) { + new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) + .properties("spring.config.location=classpath:ratelimiting/application-bucket4j-starter.yml") + .run(args); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml new file mode 100644 index 0000000000..1c1337c611 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml @@ -0,0 +1,41 @@ +server: + port: 9001 + +spring: + application: + name: bucket4j-starter-api-rate-limiting-app + mvc: + throw-exception-if-no-handler-found: true + resources: + add-mappings: false + cache: + cache-names: + - rate-limiting-buckets + caffeine: + spec: maximumSize=100000,expireAfterAccess=3600s + +bucket4j: + enabled: true + filters: + - cache-name: rate-limiting-buckets + url: /api/v1/area.* + http-response-body: "{ \"status\": 429, \"error\": \"Too Many Requests\", \"message\": \"You have exhausted your API Request Quota\" }" + rate-limits: + - expression: "getHeader('X-api-key')" + execute-condition: "getHeader('X-api-key').startsWith('PX001-')" + bandwidths: + - capacity: 100 + time: 1 + unit: hours + - expression: "getHeader('X-api-key')" + execute-condition: "getHeader('X-api-key').startsWith('BX001-')" + bandwidths: + - capacity: 40 + time: 1 + unit: hours + - expression: "getHeader('X-api-key')" + bandwidths: + - capacity: 20 + time: 1 + unit: hours + \ No newline at end of file From 34bbce79950ca2808ee0307fc23c9c109f7b2437 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 23:19:54 +0530 Subject: [PATCH 05/14] refactor to move pricing plan logic out into a separate class --- ...tingApp.java => Bucket4jRateLimitApp.java} | 4 +-- ...tingApp.java => Bucket4jRateLimitApp.java} | 14 +++++--- ...rceptor.java => RateLimitInterceptor.java} | 32 +++++++------------ .../{interceptor => service}/PricingPlan.java | 4 +-- .../service/PricingPlanService.java | 32 +++++++++++++++++++ .../application-bucket4j-starter.yml | 7 ++-- .../ratelimiting/application-bucket4j.yml | 8 ++++- 7 files changed, 67 insertions(+), 34 deletions(-) rename spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/{Bucket4jRateLimitingApp.java => Bucket4jRateLimitApp.java} (88%) rename spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/{Bucket4jRateLimitingApp.java => Bucket4jRateLimitApp.java} (73%) rename spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/{RateLimitingInterceptor.java => RateLimitInterceptor.java} (68%) rename spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/{interceptor => service}/PricingPlan.java (86%) create mode 100644 spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitApp.java similarity index 88% rename from spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java rename to spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitApp.java index de2ab41bf0..f16d347f85 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitingApp.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jRateLimitApp.java @@ -11,10 +11,10 @@ import org.springframework.cache.annotation.EnableCaching; SecurityAutoConfiguration.class, }) @EnableCaching -public class Bucket4jRateLimitingApp { +public class Bucket4jRateLimitApp { public static void main(String[] args) { - new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) + new SpringApplicationBuilder(Bucket4jRateLimitApp.class) .properties("spring.config.location=classpath:ratelimiting/application-bucket4j-starter.yml") .run(args); } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitApp.java similarity index 73% rename from spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java rename to spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitApp.java index 2a42448b35..bb179b9b38 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitingApp.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitApp.java @@ -1,28 +1,34 @@ package com.baeldung.ratelimiting.bucket4japp; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.annotation.Lazy; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import com.baeldung.ratelimiting.bucket4japp.interceptor.RateLimitingInterceptor; +import com.baeldung.ratelimiting.bucket4japp.interceptor.RateLimitInterceptor; @SpringBootApplication(scanBasePackages = "com.baeldung.ratelimiting", exclude = { DataSourceAutoConfiguration.class, SecurityAutoConfiguration.class }) -public class Bucket4jRateLimitingApp implements WebMvcConfigurer { +public class Bucket4jRateLimitApp implements WebMvcConfigurer { + + @Autowired + @Lazy + private RateLimitInterceptor interceptor; @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new RateLimitingInterceptor()) + registry.addInterceptor(interceptor) .addPathPatterns("/api/v1/area/**"); } public static void main(String[] args) { - new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) + new SpringApplicationBuilder(Bucket4jRateLimitApp.class) .properties("spring.config.location=classpath:ratelimiting/application-bucket4j.yml") .run(args); } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java similarity index 68% rename from spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java rename to spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java index 8aa8de531c..c983251e56 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitingInterceptor.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java @@ -1,38 +1,39 @@ package com.baeldung.ratelimiting.bucket4japp.interceptor; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; -import io.github.bucket4j.Bandwidth; +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlanService; + import io.github.bucket4j.Bucket; -import io.github.bucket4j.Bucket4j; import io.github.bucket4j.ConsumptionProbe; -public class RateLimitingInterceptor implements HandlerInterceptor { +@Component +public class RateLimitInterceptor implements HandlerInterceptor { private static final String HEADER_API_KEY = "X-api-key"; private static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining"; private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Milliseconds"; - private final Map cache = new ConcurrentHashMap<>(); + @Autowired + private PricingPlanService pricingPlanService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - + String apiKey = request.getHeader(HEADER_API_KEY); if (apiKey == null || apiKey.isEmpty()) { response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: " + HEADER_API_KEY); return false; } - - Bucket tokenBucket = cache.computeIfAbsent(apiKey, this::resolveBucket); + + Bucket tokenBucket = pricingPlanService.resolveBucket(apiKey); ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1); @@ -51,15 +52,4 @@ public class RateLimitingInterceptor implements HandlerInterceptor { return false; } } - - private Bucket resolveBucket(String apiKey) { - PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey); - return bucket(pricingPlan.getLimit()); - } - - private Bucket bucket(Bandwidth limit) { - return Bucket4j.builder() - .addLimit(limit) - .build(); - } } \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java similarity index 86% rename from spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java rename to spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java index e2b3ccb6c6..85632abf0b 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/PricingPlan.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java @@ -1,4 +1,4 @@ -package com.baeldung.ratelimiting.bucket4japp.interceptor; +package com.baeldung.ratelimiting.bucket4japp.service; import java.time.Duration; @@ -11,7 +11,7 @@ enum PricingPlan { @Override Bandwidth getLimit() { - return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1))); + return Bandwidth.classic(2, Refill.intervally(2 , Duration.ofHours(1))); } }, diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java new file mode 100644 index 0000000000..713f4a6e1a --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java @@ -0,0 +1,32 @@ +package com.baeldung.ratelimiting.bucket4japp.service; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.stereotype.Service; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; + +@Service +public class PricingPlanService { + + private final Map cache = new ConcurrentHashMap<>(); + + // @Cacheable("rate-limit-buckets") + public Bucket resolveBucket(String apiKey) { + return cache.computeIfAbsent(apiKey, this::newBucket); + } + + private Bucket newBucket(String apiKey) { + PricingPlan pricingPlan = PricingPlan.resolvePlanFromApiKey(apiKey); + return bucket(pricingPlan.getLimit()); + } + + private Bucket bucket(Bandwidth limit) { + return Bucket4j.builder() + .addLimit(limit) + .build(); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml index 1c1337c611..ecc9f22e0a 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j-starter.yml @@ -3,21 +3,21 @@ server: spring: application: - name: bucket4j-starter-api-rate-limiting-app + name: bucket4j-starter-api-rate-limit-app mvc: throw-exception-if-no-handler-found: true resources: add-mappings: false cache: cache-names: - - rate-limiting-buckets + - rate-limit-buckets caffeine: spec: maximumSize=100000,expireAfterAccess=3600s bucket4j: enabled: true filters: - - cache-name: rate-limiting-buckets + - cache-name: rate-limit-buckets url: /api/v1/area.* http-response-body: "{ \"status\": 429, \"error\": \"Too Many Requests\", \"message\": \"You have exhausted your API Request Quota\" }" rate-limits: @@ -38,4 +38,3 @@ bucket4j: - capacity: 20 time: 1 unit: hours - \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml index 1fb4d2cf12..0cee593261 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml @@ -3,8 +3,14 @@ server: spring: application: - name: bucket4j-api-rate-limiting-app + name: bucket4j-api-rate-limit-app mvc: throw-exception-if-no-handler-found: true resources: add-mappings: false + cache: + cache-names: + - rate-limit-buckets + caffeine: + spec: maximumSize=100000,expireAfterAccess=3600s + \ No newline at end of file From 911f840af59281685b6c6c95ad56a4a1777c3a9d Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Wed, 13 May 2020 23:24:55 +0530 Subject: [PATCH 06/14] some cleanup --- .../bucket4japp/service/PricingPlanService.java | 1 - .../main/resources/ratelimiting/application-bucket4j.yml | 6 ------ 2 files changed, 7 deletions(-) diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java index 713f4a6e1a..7d8a718601 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlanService.java @@ -14,7 +14,6 @@ public class PricingPlanService { private final Map cache = new ConcurrentHashMap<>(); - // @Cacheable("rate-limit-buckets") public Bucket resolveBucket(String apiKey) { return cache.computeIfAbsent(apiKey, this::newBucket); } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml index 0cee593261..ae19622d9b 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml +++ b/spring-boot-modules/spring-boot-libraries/src/main/resources/ratelimiting/application-bucket4j.yml @@ -8,9 +8,3 @@ spring: throw-exception-if-no-handler-found: true resources: add-mappings: false - cache: - cache-names: - - rate-limit-buckets - caffeine: - spec: maximumSize=100000,expireAfterAccess=3600s - \ No newline at end of file From f304437ed8959bf3d4d3c6440f64ba9afa5e8c38 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Thu, 14 May 2020 02:26:46 +0530 Subject: [PATCH 07/14] retry after secomds --- .../bucket4japp/interceptor/RateLimitInterceptor.java | 6 +++--- .../ratelimiting/bucket4japp/service/PricingPlan.java | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java index c983251e56..d919214983 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java @@ -18,7 +18,7 @@ public class RateLimitInterceptor implements HandlerInterceptor { private static final String HEADER_API_KEY = "X-api-key"; private static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining"; - private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Milliseconds"; + private static final String HEADER_RETRY_AFTER = "X-Rate-Limit-Retry-After-Seconds"; @Autowired private PricingPlanService pricingPlanService; @@ -44,10 +44,10 @@ public class RateLimitInterceptor implements HandlerInterceptor { } else { - long waitForRefillMilli = probe.getNanosToWaitForRefill() % 1_000_000; + long waitForRefill = probe.getNanosToWaitForRefill() % 1_000_000_000; response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota"); // 429 - response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefillMilli)); + response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefill)); return false; } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java index 85632abf0b..e8b5513e8b 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java @@ -11,7 +11,7 @@ enum PricingPlan { @Override Bandwidth getLimit() { - return Bandwidth.classic(2, Refill.intervally(2 , Duration.ofHours(1))); + return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1))); } }, From faf562e8210bb9e8594c02d28585cc7354ba8c48 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sat, 16 May 2020 01:34:34 +0530 Subject: [PATCH 08/14] add basic usage as test --- .../bucket4j/Bucket4jUsageTest.java | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java new file mode 100644 index 0000000000..247e493324 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java @@ -0,0 +1,82 @@ +package com.baledung.ratelimiting.bucket4j; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import java.time.Duration; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import io.github.bucket4j.Bandwidth; +import io.github.bucket4j.Bucket; +import io.github.bucket4j.Bucket4j; +import io.github.bucket4j.Refill; + +public class Bucket4jUsageTest { + + @Test + public void givenBucketLimit_whenExceedLimit_thenConsumeReturnsFalse() { + Refill refill = Refill.intervally(10, Duration.ofMinutes(1)); + Bandwidth limit = Bandwidth.classic(10, refill); + Bucket bucket = Bucket4j.builder() + .addLimit(limit) + .build(); + + for (int i = 1; i <= 10; i++) { + assertTrue(bucket.tryConsume(1)); + } + assertFalse(bucket.tryConsume(1)); + } + + @Test + public void givenMultipletLimits_whenExceedSmallerLimit_thenConsumeReturnsFalse() { + Bucket bucket = Bucket4j.builder() + .addLimit(Bandwidth.classic(10, Refill.intervally(10, Duration.ofMinutes(1)))) + .addLimit(Bandwidth.classic(5, Refill.intervally(5, Duration.ofSeconds(20)))) + .build(); + + for (int i = 1; i <= 5; i++) { + assertTrue(bucket.tryConsume(1)); + } + assertFalse(bucket.tryConsume(1)); + } + + @Test + public void givenBucketLimit_whenThrottleRequests_thenConsumeReturnsTrue() throws InterruptedException { + Refill refill = Refill.intervally(1, Duration.ofSeconds(2)); + Bandwidth limit = Bandwidth.classic(1, refill); + Bucket bucket = Bucket4j.builder() + .addLimit(limit) + .build(); + + assertTrue(bucket.tryConsume(1)); + + ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + CountDownLatch latch = new CountDownLatch(1); + + executor.schedule(new AssertTryConsume(bucket, latch), 2, TimeUnit.SECONDS); + + latch.await(); + } + + static class AssertTryConsume implements Runnable { + + private Bucket bucket; + private CountDownLatch latch; + + AssertTryConsume(Bucket bucket, CountDownLatch latch) { + this.bucket = bucket; + this.latch = latch; + } + + @Override + public void run() { + assertTrue(bucket.tryConsume(1)); + latch.countDown(); + } + } +} From 8d79b7c5d6c58a6096c697d9da976c0bb9d0413b Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sat, 16 May 2020 17:20:15 +0530 Subject: [PATCH 09/14] unit test renamed --- .../{Bucket4jUsageTest.java => Bucket4jUsageUnitTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/{Bucket4jUsageTest.java => Bucket4jUsageUnitTest.java} (98%) diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java similarity index 98% rename from spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java rename to spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java index 247e493324..e6b774034e 100644 --- a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageTest.java +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java @@ -16,7 +16,7 @@ import io.github.bucket4j.Bucket; import io.github.bucket4j.Bucket4j; import io.github.bucket4j.Refill; -public class Bucket4jUsageTest { +public class Bucket4jUsageUnitTest { @Test public void givenBucketLimit_whenExceedLimit_thenConsumeReturnsFalse() { From dd0a8e2d11ff2d80acb22c4848c86d6f0cae247b Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Thu, 21 May 2020 17:36:25 +0530 Subject: [PATCH 10/14] add tests --- .../interceptor/RateLimitInterceptor.java | 6 +- .../bucket4japp/service/PricingPlan.java | 42 ++++++------- ...4jBootStarterRateLimitIntegrationTest.java | 63 +++++++++++++++++++ .../Bucket4jRateLimitIntegrationTest.java | 62 ++++++++++++++++++ .../bucket4japp}/Bucket4jUsageUnitTest.java | 2 +- .../PricingPlanServiceUnitTest.java | 36 +++++++++++ 6 files changed, 184 insertions(+), 27 deletions(-) create mode 100644 spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jBootStarterRateLimitIntegrationTest.java create mode 100644 spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java rename spring-boot-modules/spring-boot-libraries/src/test/java/com/{baledung/ratelimiting/bucket4j => baeldung/ratelimiting/bucket4japp}/Bucket4jUsageUnitTest.java (98%) create mode 100644 spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/PricingPlanServiceUnitTest.java diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java index d919214983..8a18d6c2b5 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/interceptor/RateLimitInterceptor.java @@ -5,6 +5,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; @@ -44,10 +45,11 @@ public class RateLimitInterceptor implements HandlerInterceptor { } else { - long waitForRefill = probe.getNanosToWaitForRefill() % 1_000_000_000; + long waitForRefill = probe.getNanosToWaitForRefill() / 1_000_000_000; - response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota"); // 429 + response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.addHeader(HEADER_RETRY_AFTER, String.valueOf(waitForRefill)); + response.sendError(HttpStatus.TOO_MANY_REQUESTS.value(), "You have exhausted your API Request Quota"); // 429 return false; } diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java index e8b5513e8b..2f225a83aa 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java @@ -5,34 +5,28 @@ import java.time.Duration; import io.github.bucket4j.Bandwidth; import io.github.bucket4j.Refill; -enum PricingPlan { +public enum PricingPlan { - FREE { + FREE(20), - @Override - Bandwidth getLimit() { - return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1))); - } - }, + BASIC(40), - BASIC { - - @Override - Bandwidth getLimit() { - return Bandwidth.classic(40, Refill.intervally(40, Duration.ofHours(1))); - } - }, - - PROFESSIONAL { - - @Override - Bandwidth getLimit() { - return Bandwidth.classic(100, Refill.intervally(100, Duration.ofHours(1))); - } - }; - - abstract Bandwidth getLimit(); + PROFESSIONAL(100);; + private int bucketCapacity; + + private PricingPlan(int bucketCapacity) { + this.bucketCapacity = bucketCapacity; + } + + Bandwidth getLimit() { + return Bandwidth.classic(bucketCapacity, Refill.intervally(bucketCapacity, Duration.ofHours(1))); + } + + public int bucketCapacity() { + return bucketCapacity; + } + static PricingPlan resolvePlanFromApiKey(String apiKey) { if (apiKey == null || apiKey.isEmpty()) { return FREE; diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jBootStarterRateLimitIntegrationTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jBootStarterRateLimitIntegrationTest.java new file mode 100644 index 0000000000..d93e61988b --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bootstarterapp/Bucket4jBootStarterRateLimitIntegrationTest.java @@ -0,0 +1,63 @@ +package com.baeldung.ratelimiting.bootstarterapp; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.RequestBuilder; + +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlan; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Bucket4jRateLimitApp.class) +@TestPropertySource(properties = "spring.config.location=classpath:ratelimiting/application-bucket4j-starter.yml") +@AutoConfigureMockMvc +public class Bucket4jBootStarterRateLimitIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void givenTriangleAreaCalculator_whenRequestsWithinRateLimit_thenAccepted() throws Exception { + + RequestBuilder request = post("/api/v1/area/triangle").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{ \"height\": 8, \"base\": 10 }") + .header("X-api-key", "FX001-UBSZ5YRYQ"); + + for (int i = 1; i <= PricingPlan.FREE.bucketCapacity(); i++) { + mockMvc.perform(request) + .andExpect(status().isOk()) + .andExpect(header().exists("X-Rate-Limit-Remaining")) + .andExpect(jsonPath("$.shape", equalTo("triangle"))) + .andExpect(jsonPath("$.area", equalTo(40d))); + } + } + + @Test + public void givenTriangleAreaCalculator_whenRequestRateLimitTriggered_thenRejected() throws Exception { + + RequestBuilder request = post("/api/v1/area/triangle").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{ \"height\": 8, \"base\": 10 }") + .header("X-api-key", "FX001-ZBSY6YSLP"); + + for (int i = 1; i <= PricingPlan.FREE.bucketCapacity(); i++) { + mockMvc.perform(request); // exhaust limit + } + + mockMvc.perform(request) + .andExpect(status().isTooManyRequests()) + .andExpect(jsonPath("$.message", equalTo("You have exhausted your API Request Quota"))) + .andExpect(header().exists("X-Rate-Limit-Retry-After-Seconds")); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java new file mode 100644 index 0000000000..b410b7b2c5 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java @@ -0,0 +1,62 @@ +package com.baeldung.ratelimiting.bucket4japp; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.RequestBuilder; + +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlan; + +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Bucket4jRateLimitApp.class) +@AutoConfigureMockMvc +public class Bucket4jRateLimitIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Test + public void givenRectangleAreaCalculator_whenRequestsWithinRateLimit_thenAccepted() throws Exception { + + RequestBuilder request = post("/api/v1/area/rectangle").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{ \"length\": 12, \"width\": 10 }") + .header("X-api-key", "FX001-UBSZ5YRYQ"); + + for (int i = 1; i <= PricingPlan.FREE.bucketCapacity(); i++) { + mockMvc.perform(request) + .andExpect(status().isOk()) + .andExpect(header().exists("X-Rate-Limit-Remaining")) + .andExpect(jsonPath("$.shape", equalTo("rectangle"))) + .andExpect(jsonPath("$.area", equalTo(120d))); + } + } + + @Test + public void givenReactangleAreaCalculator_whenRequestRateLimitTriggered_thenRejected() throws Exception { + + RequestBuilder request = post("/api/v1/area/rectangle").contentType(MediaType.APPLICATION_JSON_VALUE) + .content("{ \"length\": 12, \"width\": 10 }") + .header("X-api-key", "FX001-ZBSY6YSLP"); + + for (int i = 1; i <= PricingPlan.FREE.bucketCapacity(); i++) { + mockMvc.perform(request); // exhaust limit + } + + mockMvc.perform(request) + .andExpect(status().isTooManyRequests()) + .andExpect(status().reason("You have exhausted your API Request Quota")) + .andExpect(header().exists("X-Rate-Limit-Retry-After-Seconds")); + } +} diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jUsageUnitTest.java similarity index 98% rename from spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java rename to spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jUsageUnitTest.java index e6b774034e..fbf63ba403 100644 --- a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baledung/ratelimiting/bucket4j/Bucket4jUsageUnitTest.java +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jUsageUnitTest.java @@ -1,4 +1,4 @@ -package com.baledung.ratelimiting.bucket4j; +package com.baeldung.ratelimiting.bucket4japp; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/PricingPlanServiceUnitTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/PricingPlanServiceUnitTest.java new file mode 100644 index 0000000000..325b898779 --- /dev/null +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/PricingPlanServiceUnitTest.java @@ -0,0 +1,36 @@ +package com.baeldung.ratelimiting.bucket4japp; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlan; +import com.baeldung.ratelimiting.bucket4japp.service.PricingPlanService; + +import io.github.bucket4j.Bucket; + +public class PricingPlanServiceUnitTest { + + private PricingPlanService service = new PricingPlanService(); + + @Test + public void givenAPIKey_whenFreePlan_thenReturnFreePlanBucket() { + Bucket bucket = service.resolveBucket("FX001-UBSZ5YRYQ"); + + assertEquals(PricingPlan.FREE.bucketCapacity(), bucket.getAvailableTokens()); + } + + @Test + public void givenAPIKey_whenBasiclan_thenReturnBasicPlanBucket() { + Bucket bucket = service.resolveBucket("BX001-MBSZ5YRYP"); + + assertEquals(PricingPlan.BASIC.bucketCapacity(), bucket.getAvailableTokens()); + } + + @Test + public void givenAPIKey_whenProfessionalPlan_thenReturnProfessionalPlanBucket() { + Bucket bucket = service.resolveBucket("PX001-NBSZ5YRYY"); + + assertEquals(PricingPlan.PROFESSIONAL.bucketCapacity(), bucket.getAvailableTokens()); + } +} From 8ecf039b6b539ae626c93e8e4aa7b30b13b55e98 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Thu, 21 May 2020 22:53:20 +0530 Subject: [PATCH 11/14] remove unused import --- .../bucket4japp/Bucket4jRateLimitIntegrationTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java index b410b7b2c5..20f57a7021 100644 --- a/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java +++ b/spring-boot-modules/spring-boot-libraries/src/test/java/com/baeldung/ratelimiting/bucket4japp/Bucket4jRateLimitIntegrationTest.java @@ -11,7 +11,6 @@ import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.servlet.MockMvc; From d76966dab73c71c540d56262c9d0b58a60748ab5 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sun, 24 May 2020 16:53:24 +0530 Subject: [PATCH 12/14] review comments - indentation in pom --- .../spring-boot-libraries/pom.xml | 244 +++++++++--------- .../bucket4japp/service/PricingPlan.java | 2 +- 2 files changed, 123 insertions(+), 123 deletions(-) diff --git a/spring-boot-modules/spring-boot-libraries/pom.xml b/spring-boot-modules/spring-boot-libraries/pom.xml index 36b9ec17c9..da4e979c17 100644 --- a/spring-boot-modules/spring-boot-libraries/pom.xml +++ b/spring-boot-modules/spring-boot-libraries/pom.xml @@ -88,34 +88,34 @@ ${zxing.version} - - com.github.vladimir-bukhtoyarov - bucket4j-core - ${bucket4j.version} - - - com.giffing.bucket4j.spring.boot.starter - bucket4j-spring-boot-starter - ${bucket4j-spring-boot-starter.version} - - - org.springframework.boot - spring-boot-starter-cache - - - javax.cache - cache-api - - - com.github.ben-manes.caffeine - caffeine - ${caffeine.version} - - - com.github.ben-manes.caffeine - jcache - ${caffeine.version} - + + com.github.vladimir-bukhtoyarov + bucket4j-core + ${bucket4j.version} + + + com.giffing.bucket4j.spring.boot.starter + bucket4j-spring-boot-starter + ${bucket4j-spring-boot-starter.version} + + + org.springframework.boot + spring-boot-starter-cache + + + javax.cache + cache-api + + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + + + com.github.ben-manes.caffeine + jcache + ${caffeine.version} + @@ -125,112 +125,112 @@ - - spring-boot-libraries - - - src/main/resources - true - - + + spring-boot-libraries + + + src/main/resources + true + + - + - - org.apache.maven.plugins - maven-war-plugin - + + org.apache.maven.plugins + maven-war-plugin + - - pl.project13.maven - git-commit-id-plugin - ${git-commit-id-plugin.version} - - - get-the-git-infos - - revision - - initialize - - - validate-the-git-infos - - validateRevision - - package - - - - true - ${project.build.outputDirectory}/git.properties - - + + pl.project13.maven + git-commit-id-plugin + ${git-commit-id-plugin.version} + + + get-the-git-infos + + revision + + initialize + + + validate-the-git-infos + + validateRevision + + package + + + + true + ${project.build.outputDirectory}/git.properties + + - - - autoconfiguration - - - - org.apache.maven.plugins - maven-surefire-plugin - - - integration-test - - test - - - - **/*LiveTest.java - **/*IntegrationTest.java - **/*IntTest.java - - - **/AutoconfigurationTest.java - - - - - - - json - - - - - - - + + + autoconfiguration + + + + org.apache.maven.plugins + maven-surefire-plugin + + + integration-test + + test + + + + **/*LiveTest.java + **/*IntegrationTest.java + **/*IntTest.java + + + **/AutoconfigurationTest.java + + + + + + + json + + + + + + + - - + + com.baeldung.intro.App 8.5.11 2.4.1.Final 1.9.0 2.0.0 - 5.0.2 - 5.0.2 - 5.2.4 - 18.0 - 2.2.4 - 2.3.2 - 0.23.0 - 1.4.200 - 2.1.0 - 1.5-beta1 - 2.1 - 2.6.0 - 3.3.0 - 4.10.0 - 0.2.0 - 2.8.2 - + 5.0.2 + 5.0.2 + 5.2.4 + 18.0 + 2.2.4 + 2.3.2 + 0.23.0 + 1.4.200 + 2.1.0 + 1.5-beta1 + 2.1 + 2.6.0 + 3.3.0 + 4.10.0 + 0.2.0 + 2.8.2 + - + \ No newline at end of file diff --git a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java index 2f225a83aa..27c30ba3a0 100644 --- a/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java +++ b/spring-boot-modules/spring-boot-libraries/src/main/java/com/baeldung/ratelimiting/bucket4japp/service/PricingPlan.java @@ -11,7 +11,7 @@ public enum PricingPlan { BASIC(40), - PROFESSIONAL(100);; + PROFESSIONAL(100); private int bucketCapacity; From b7c9e649825fee3377c07cf9cdbbec77e591e84c Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sun, 24 May 2020 16:55:06 +0530 Subject: [PATCH 13/14] review comments - indentation in pom --- spring-boot-modules/spring-boot-libraries/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-libraries/pom.xml b/spring-boot-modules/spring-boot-libraries/pom.xml index da4e979c17..4e7fd205d5 100644 --- a/spring-boot-modules/spring-boot-libraries/pom.xml +++ b/spring-boot-modules/spring-boot-libraries/pom.xml @@ -233,4 +233,4 @@ 2.8.2 - \ No newline at end of file + From e839798f0da0d2e598f64c5d20bba6973428ba41 Mon Sep 17 00:00:00 2001 From: priyank-sriv Date: Sun, 24 May 2020 16:56:57 +0530 Subject: [PATCH 14/14] review comments - indentation in pom --- spring-boot-modules/spring-boot-libraries/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-boot-modules/spring-boot-libraries/pom.xml b/spring-boot-modules/spring-boot-libraries/pom.xml index 4e7fd205d5..189eb4cf1a 100644 --- a/spring-boot-modules/spring-boot-libraries/pom.xml +++ b/spring-boot-modules/spring-boot-libraries/pom.xml @@ -87,7 +87,7 @@ javase ${zxing.version} - + com.github.vladimir-bukhtoyarov bucket4j-core