Enterprise rate limiter for java web apps, based on rate-limiter-annotation.
We believe that rate limiting should be as simple as:
@Rate(10) // 10 permits per second for all methods in this class
@RequestMapping("/api/v1")
public class GreetingResource {
@Rate(permits=10, when="web.request.user.role = GUEST")
@GetMapping("/smile")
public String smile() {
return ":)";
}
@Rate(permits=1, when="jvm.memory.available < 1gb")
@GetMapping("/greet")
public String greet(@RequestParam("who") String who) {
return "Hello " + who;
}
}
Please first read the rate-limiter-annotation documentation.
Some custom implementations:
To add a dependency on rate-limiter-web-core
using Maven, use the following:
<dependency>
<groupId>io.github.poshjosh</groupId>
<artifactId>rate-limiter-web-core</artifactId>
<version>0.8.0</version>
</dependency>
Annotate the resource you want to rate limit
@Controller
@RequestMapping("/api")
class GreetingResource {
// 2 calls per second, if the header X-Rate-Limited has a value
@Rate(2)
@RateCondition("web.request.header = X-Rate-Limited")
@GetMapping("/smile")
String smile() {
return ":)";
}
}
(Optional) Configure rate limiting
Limiters, matchers, caches and listeners, could be configured by implementing and
exposing a RateLimiterConfigurer
as shown below:
import io.github.poshjosh.ratelimiter.store.BandwidthsStore;
@Configuration
public class Configurer implements RateLimiterConfigurer {
@Override
public void configure(Registries registries) {
// Register request matchers
// -------------------------
// Identify resources to rate-limit by session id
registries.matchers().register("limitBySession", request -> request.getSession().getId());
// Identify resources to rate-limit by the presence of request parameter "utm_source"
registries.matchers()
.register("limitByUtmSource", request -> request.getParameter("utm_source"));
// Rate limit users from a specific utm_source e.g facebook
registries.matchers().register("limitByUtmSourceIsFacebook",
request -> "facebook".equals(request.getParameter("utm_source")));
}
}
Please first read the annotation specs. It is concise.
The @Rate
and @RateGroup
annotations are bound to paths.
For the specification of these annotations, please read the rate-limiter documentation. In addition, the following applies:
-
The
@Rate
annotation must be placed together with path related annotations e.g: Springframeworks's@RequestMapping
,@Get
etc or JAX-RS@Path
etc -
The
@Rate
annotation is bound to the path with which it is co-located, not the resource. This means that the@Rate
annotation below will match all request paths beginning with/api/v1
even those paths specified on other resources and methods.
@Path("/api/v1")
@Rate(limit = 20, timeUnit = TimeUnit.MINUTES)
class RateLimitedResource{
}
Limiters, matchers, caches or listeners could be registered using either a class ID, a method ID, or a string name. When using a string name, it should match one of the following:
- A group - The name of a
@RateGroup
annotation. - A class - The fully qualified name of a class e.g:
com.example.web.resources.GreetingResource
- A method - The identifier of a method eg:
com.example.web.resources.GreetingResource.greet(java.lang.String)
- A property - One of the keys in the
Map
returned byRateLimitProperties#getRateLimitConfigs()
When rate limits are specified via annotations, then the corresponding matcher for the annotated class
or method is automatically created. However, this automatic creation does not happen when rate limits are
specified via properties. This means you need to explicitly register a matcher for each key in the Map
returned by RateLimitProperties#getRateLimitConfigs()
.
The following code will not lead to any rate limiting. Unless, we explicitly register a matcher for each key in the returned map.
public class RateLimitPropertiesImpl implements RateLimitProperties {
// other code
@Override
public Map<String, Rates> getRateLimitConfigs() {
return Collections.singletonMap("default", Rates.of(Rate.ofMinutes(10)));
}
}
You could bind rate limits from properties to a class or method. For example to bind to
class MyRateLimitedResource.class
:
public class RateLimitPropertiesImpl implements RateLimitProperties, RateLimiterConfigurer {
private final Object resource = MyRateLimitedResource.class;
@Override
public void configure(Registries registries) {
registries.matchers().register(resource, request -> request.getRequestURI());
}
@Override
public List<String> getResourcePackages() {
return Collections.singletonList(this.getClass().getPackage().getName());
}
@Override
public Map<String, Rates> getRateLimitConfigs() {
return Collections.singletonMap(RateId.of(resource), Rates.of(Rate.ofMinutes(10)));
}
}
Matchers are invoked in this order:
- Path pattern matcher - Created by default - (Matches path patterns e.g
@GetMapping("/api/v1")
) - Custom expression matcher (see expression language)
- System expression matcher - Created by default - (see expression language)
- Custom registered matcher
There are 2 ways to rate limit a web application (You could use both):
Example using Springframework
@Controller
@RequestMapping("/api")
class GreetingResource {
// Only 99 calls to this path is allowed per second
@Rate(99)
@GetMapping("/greet")
String greet() {
return "Hello World!";
}
}
Example using JAX-RS
@Path("/api")
class GreetingResource {
// Only 99 calls to this path is allowed per second
@Rate(99)
@GET
@Path("/greet")
@Produces("text/plan")
String greet() {
return "Hello World!";
}
}
Example class that implements the required properties.
package com.example.web;
import RateLimitProperties;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class RateLimitPropertiesImpl implements RateLimitProperties {
@Override
public List<String> getResourcePackages() {
return Collections.singletonList("com.example.web.resources");
}
@Override
public Map<String, Rates> getRateLimitConfigs() {
return Collections.singletonMap("limitBySession", Rates.of(getRates()));
}
private Rate[] getRates() {
return new Rate[] { Rate.of(1, Duration.ofMinutes(1)) };
}
}
Make sure this class is available for injection into other resources/beans.
The properties the user defines should be used to create a rate limiter which will be automatically applied to every request the web application handles.
The following depend on this library:
Enjoy! 😉