refactor to move pricing plan logic out into a separate class

This commit is contained in:
priyank-sriv 2020-05-13 23:19:54 +05:30
parent 1b5a64c4a4
commit 34bbce7995
7 changed files with 67 additions and 34 deletions

View File

@ -11,10 +11,10 @@ import org.springframework.cache.annotation.EnableCaching;
SecurityAutoConfiguration.class, SecurityAutoConfiguration.class,
}) })
@EnableCaching @EnableCaching
public class Bucket4jRateLimitingApp { public class Bucket4jRateLimitApp {
public static void main(String[] args) { public static void main(String[] args) {
new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) new SpringApplicationBuilder(Bucket4jRateLimitApp.class)
.properties("spring.config.location=classpath:ratelimiting/application-bucket4j-starter.yml") .properties("spring.config.location=classpath:ratelimiting/application-bucket4j-starter.yml")
.run(args); .run(args);
} }

View File

@ -1,28 +1,34 @@
package com.baeldung.ratelimiting.bucket4japp; package com.baeldung.ratelimiting.bucket4japp;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder; 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.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 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 = { @SpringBootApplication(scanBasePackages = "com.baeldung.ratelimiting", exclude = {
DataSourceAutoConfiguration.class, DataSourceAutoConfiguration.class,
SecurityAutoConfiguration.class SecurityAutoConfiguration.class
}) })
public class Bucket4jRateLimitingApp implements WebMvcConfigurer { public class Bucket4jRateLimitApp implements WebMvcConfigurer {
@Autowired
@Lazy
private RateLimitInterceptor interceptor;
@Override @Override
public void addInterceptors(InterceptorRegistry registry) { public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RateLimitingInterceptor()) registry.addInterceptor(interceptor)
.addPathPatterns("/api/v1/area/**"); .addPathPatterns("/api/v1/area/**");
} }
public static void main(String[] args) { public static void main(String[] args) {
new SpringApplicationBuilder(Bucket4jRateLimitingApp.class) new SpringApplicationBuilder(Bucket4jRateLimitApp.class)
.properties("spring.config.location=classpath:ratelimiting/application-bucket4j.yml") .properties("spring.config.location=classpath:ratelimiting/application-bucket4j.yml")
.run(args); .run(args);
} }

View File

@ -1,38 +1,39 @@
package com.baeldung.ratelimiting.bucket4japp.interceptor; package com.baeldung.ratelimiting.bucket4japp.interceptor;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor; 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.Bucket;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.ConsumptionProbe; 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_API_KEY = "X-api-key";
private static final String HEADER_LIMIT_REMAINING = "X-Rate-Limit-Remaining"; 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-Milliseconds";
private final Map<String, Bucket> cache = new ConcurrentHashMap<>(); @Autowired
private PricingPlanService pricingPlanService;
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String apiKey = request.getHeader(HEADER_API_KEY); String apiKey = request.getHeader(HEADER_API_KEY);
if (apiKey == null || apiKey.isEmpty()) { if (apiKey == null || apiKey.isEmpty()) {
response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: " + HEADER_API_KEY); response.sendError(HttpStatus.BAD_REQUEST.value(), "Missing Header: " + HEADER_API_KEY);
return false; return false;
} }
Bucket tokenBucket = cache.computeIfAbsent(apiKey, this::resolveBucket); Bucket tokenBucket = pricingPlanService.resolveBucket(apiKey);
ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1); ConsumptionProbe probe = tokenBucket.tryConsumeAndReturnRemaining(1);
@ -51,15 +52,4 @@ public class RateLimitingInterceptor implements HandlerInterceptor {
return false; 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();
}
} }

View File

@ -1,4 +1,4 @@
package com.baeldung.ratelimiting.bucket4japp.interceptor; package com.baeldung.ratelimiting.bucket4japp.service;
import java.time.Duration; import java.time.Duration;
@ -11,7 +11,7 @@ enum PricingPlan {
@Override @Override
Bandwidth getLimit() { Bandwidth getLimit() {
return Bandwidth.classic(20, Refill.intervally(20, Duration.ofHours(1))); return Bandwidth.classic(2, Refill.intervally(2 , Duration.ofHours(1)));
} }
}, },

View File

@ -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<String, Bucket> 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();
}
}

View File

@ -3,21 +3,21 @@ server:
spring: spring:
application: application:
name: bucket4j-starter-api-rate-limiting-app name: bucket4j-starter-api-rate-limit-app
mvc: mvc:
throw-exception-if-no-handler-found: true throw-exception-if-no-handler-found: true
resources: resources:
add-mappings: false add-mappings: false
cache: cache:
cache-names: cache-names:
- rate-limiting-buckets - rate-limit-buckets
caffeine: caffeine:
spec: maximumSize=100000,expireAfterAccess=3600s spec: maximumSize=100000,expireAfterAccess=3600s
bucket4j: bucket4j:
enabled: true enabled: true
filters: filters:
- cache-name: rate-limiting-buckets - cache-name: rate-limit-buckets
url: /api/v1/area.* url: /api/v1/area.*
http-response-body: "{ \"status\": 429, \"error\": \"Too Many Requests\", \"message\": \"You have exhausted your API Request Quota\" }" http-response-body: "{ \"status\": 429, \"error\": \"Too Many Requests\", \"message\": \"You have exhausted your API Request Quota\" }"
rate-limits: rate-limits:
@ -38,4 +38,3 @@ bucket4j:
- capacity: 20 - capacity: 20
time: 1 time: 1
unit: hours unit: hours

View File

@ -3,8 +3,14 @@ server:
spring: spring:
application: application:
name: bucket4j-api-rate-limiting-app name: bucket4j-api-rate-limit-app
mvc: mvc:
throw-exception-if-no-handler-found: true throw-exception-if-no-handler-found: true
resources: resources:
add-mappings: false add-mappings: false
cache:
cache-names:
- rate-limit-buckets
caffeine:
spec: maximumSize=100000,expireAfterAccess=3600s