An API Gateway provides a single entry point for all the microservices running downstream. There are many gateway solutions available such as Zuul, Linkerd, Nginx, etc. but in this article, we will specifically discuss Spring Cloud Gateway - a reactive Gateway built upon Project Reactor, Spring WebFlux, and Spring Boot 2.0.
First, we will start with an introduction of Spring Cloud Gateway and then discuss the features of it that makes it the best fit as a cloud gateway. After that, we will look into the gateway flow with different examples of using predicates for routing, pre-filters, global filters to modify the request and response header and body along with Hystrix support. In the end, we will create a simple example project to demonstrate these capabilities with discovery server using Netflix, multiple microservices and Spring Cloud Gateway routing requests to these different microservices downstream.
Blocking vs Non-Blocking Gateway
A Blocking gateway such as Zuul1 requires as many threads as no of requests to handle requests coming to an API gateway and hence a lot of resources is required to handle these requests. Any extra requests simply sit in the queue and wait until a working thread completes its execution. Whereas in case of non-blocking, the main thread is always available to serve the request and other multiple threads process those requests asynchronously in the background and once the request is completely processed the response is returned. Hence non-blocking model requires a less no of resources to serve the same amount of requests as compared to blocking gateway.
Here is an example of spring cloud Netflix with Zuul as Gateway Provider.
Spring cloud gateway is a non-blocking reactive gateway similar to Zuul2 but spring cloud does not provide any out of the box integration with Zuul2.
Here is the performance benchmark of Spring Cloud gateway.
Spring Cloud Gateway Architecture
Below is a simple architecture diagram of Spring Cloud. In the later sections, we will discuss the different components involved in it.
Once a request reaches to the gateway, the first thing gateway does is to match the request with each of the available route based on the predicate defined. Once, the route has matched the request moves to web handler and the filters will be applied to the request. There are many out of the box filters provided by the gateway itself to modify the request header as well as the body. Pre-filters are applied specifically to a route whereas global filters can be applied to all the route request. Global filters can be applied to perform authentication and authorization of all the request at one place.
Once the response is generated by the downstream service, the post filter can be used to modify the response such as to apply checksum in the response header to all the responses to ensure the response is not tempered due to middle man attack.
Below is an example of a route that has a predicate defined to match all the request URL with /api/v1/first/**
and apply a pre-filter to rewrite the path. There is another filter applied to modify the request header and then route the request to load balanced FIRST-SERVICE.
builder.routes() .route(r -> r.path("/api/v1/first/**") .filters(f -> f.rewritePath("/api/v1/first/(?.*)", "/${remains}") .addRequestHeader("X-first-Header", "first-service-header") ) .uri("lb://FIRST-SERVICE/") //downstream endpoint lb - load balanced .id("queue-service")) .build();
Below is the equivalent .yaml configuration.
spring: cloud: gateway: routes: - id: first-service uri: lb://FIRST-SERVICE predicates: - Path=/api/v1/first/** filters: - RewritePath=/api/v1/first/(?.*), /$\{remains} - AddRequestHeader=X-first-Header, first-service-header
Handler Mapping
This is the first component where all the requests hit. Handler mapping looks into each route that is defined and tries to match with each predicate defined in the route. After the match, handler mapping routes the request to that matched route.
In the above example, we have only defined a path predicate to match the request. There are a couple of other predicates out of the box to define our route such as Host, Date/Time, Method, Headers, Query Params, Cookies, body etc.
Path Roue Predicate
With path predicate, we can match the request with URI path. For example, below route is matched against the URl - localhost:8080/api/v1/first/users and routes to load balanced first-service service.
.route(r -> r.path("/api/v1/first/**") .uri("lb://FIRST-SERVICE/") .id("first-service")) .build();
Host Route Predicate
With the Host route predicate, we can match Host header with any given pattern.
.route(r -> r.host("**.devglan.com") .uri("lb://FIRST-SERVICE/") //downstream endpoint lb - load balanced .id("first-service")) .build();
Date/Time Route Predicate
This predicate can be used to match the route request with any given date/time. We can use Before, After or Between to match with the date/time.
.route(r -> r.before(ZonedDateTime.now()) .uri("lb://FIRST-SERVICE/") //downstream endpoint lb - load balanced .id("first-service")) .build();
Method Route Predicate
With this predicate, we match the HTTP request method type such as GET, POST, etc.
.route(r -> r.method(HttpMethod.POST) .uri("lb://FIRST-SERVICE/") //downstream endpoint lb - load balanced .id("first-service")) .build();
Header Route Predicate
Header route predicate is used to match the request with the header params.
.route(r -> r.header("X-first-Header", "first-service-header") .uri("lb://FIRST-SERVICE/") //downstream endpoint lb - load balanced .id("first-service")) .build();
Query Param Route Predicate
This route predicate is used to match the request with query parameter passed in the request URI.
.route(r -> r.query("refer", "email") .uri("lb://FIRST-SERVICE/") //downstream endpoint lb - load balanced .id("first-service")) .build();
Cookies Route Predicate
The Cookie Route Predicate Factory takes two parameters, the cookie name and a regular expression. This predicate matches cookies that have the given name and the value matches the regular expression.
.route(r -> r.cookie("chocolate", "ch.p") .uri("lb://FIRST-SERVICE/") .id("first-service")) .build();
Web Handler Filters
As discussed above, we can have pre-filters, global filters and post-flters. There are many filters that are provided by the gateway out of the box and it also provides flexibility to define our custom filters that can be either applied to a specific route or to all the request coming to the gateway.
Pre-Filters
Pre-filters are route specific which can be used to modify all the elements of the incoming request such as Headers, Path, Rate Limiting, Cookies, Hystrix, Moddify Body etc.
AddRequestHeader WebFilter
This filter is used to add extra header element in the incoming request. It takes a name and value parameter.
.route(r -> r.path("/api/v1/first/**") .filters(f -> f.addRequestHeader("X-first-Header", "first-service-header")) .uri("lb://FIRST-SERVICE/") .id("first-service")) .build();
RewritePath WebFilter
It is used to rewrite the incoming request URI path. The RewritePath GatewayFilter Factory takes a path regexp parameter and a replacement parameter. This uses Java regular expressions for a flexible way to rewrite the request path. Below is the Java config.
.route(r -> r.path("/api/v1/first/**") .filters(f -> f.rewritePath("/api/v1/first/(?.*)", "/${remains}")) .uri("lb://FIRST-SERVICE/") .id("first-service")) .build();
Rate Limiting WebFilter
It is mainly used to control the amount of incoming request to the server.This filter uses a RateLimiter implementation to determine if the current request is allowed to proceed. If it is not, a status of HTTP 429 - Too Many Requests (by default) is returned.This filter takes an optional keyResolver parameter and parameters specific to the rate limiter - replenishRate, capacity, and keyResolverName
ReplenishRate - Defines number of requests per second allowed.
Capacity - Defines bursting capacity that is allowed. 2 consecutive bursts will result in dropped requests (HTTP 429 - Too Many Requests)
KeyResolverName - Custom implementation of KeyResolver interface.
.route(r -> r.path("/api/v1/first/**") .filters(f -> f.requestRateLimiter().configure(c -> c.setRateLimiter(customRateLimiter))) .uri("lb://FIRST-SERVICE/") .id("first-service")) .build();
Here, customRateLimiter is the implementation of KeyResolver interface.
Hystrix WebFilter
The Hystrix GatewayFilter allows you to introduce circuit breakers to your gateway routes, protecting your services from cascading failures and allowing you to provide fallback responses in the event of downstream failures. The Hystrix GatewayFilter Factory requires a single name parameter, which is the name of the HystrixCommand.
.route(r -> r.path("/api/v1/first/**") .filters(f -> f.hystrix(h -> h.setName("Hystrix").setFallbackUri("forward:/fallback"))) .uri("lb://FIRST-SERVICE/") .id("first-service")) .build();
@RestController @RequestMapping("/fallback") public class FallbackController { @GetMapping("/") public ResponseEntityfallback(){ return ResponseEntity.ok(Arrays.asList()); } }
Modify Request Body Filter
This filter can be used to modify the request body before it is sent downstream by the Gateway.
@Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes() .route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org") .filters(f -> f.prefixPath("/httpbin") .modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE, (exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri)) .build(); } static class Hello { String message; public Hello() { } public Hello(String message) { this.message = message; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
Global Filter
Global filters cana be applied conditionally to all routes. The different global filters are Netty Router, Web Sockets, Load Balancer, Metrics.
Post Filters
Post filters are used to add, modify, remove or rewrite response header or body.
Add Response Header Filter
.route(r -> r.path("/api/v1/first/**") .filters(f -> f.addResponseHeader("payload-size", "12")) .uri("lb://FIRST-SERVICE/") .id("first-service")) .build();
Remove Response Header Filter
.route(r -> r.path("/api/v1/first/**") .filters(f -> f.removeResponseHeader("payload-size")) .uri("lb://FIRST-SERVICE/") .id("first-service")) .build();
Conclusion
In this article, we discussed about the spring cloud gateway and its different components such as routes, predicates, filters with example. In the next tutorial of Spring Cloud Gateway Example we will implement these theories and build an application with microservice architecture using Spring Cloud Gateway and demonstrate working examples on web filters to modify request and response body.