refactor to move pricing plan logic out into a separate class
This commit is contained in:
parent
1b5a64c4a4
commit
34bbce7995
|
@ -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);
|
||||||
}
|
}
|
|
@ -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);
|
||||||
}
|
}
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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)));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue