Spring Cloud Gateway is a tool provided by Spring Cloud that helps in routing incoming requests to different microservices. It acts as a gateway (or proxy) that routes client requests to various services based on certain conditions. In this tutorial we will continue our Spring Cloud Journey
Why a Spring Cloud Ecosystem Needs Spring Cloud Gateway
In a microservices architecture, Spring Cloud Gateway is essential for managing and routing requests effectively. It provides a centralized entry point, simplifying routing by directing traffic based on dynamic criteria like paths, headers, and methods. Its integration with service registries like Eureka enables automatic load balancing, distributing traffic across multiple service instances to ensure reliability and availability.
The gateway enhances security by enforcing authentication, authorization, and rate-limiting policies consistently across microservices. It also manages cross-cutting concerns like logging, metrics, and tracing centrally, reducing the complexity for individual services.
Spring Cloud Gateway supports the API Gateway pattern, allowing it to aggregate responses from multiple services and simplify client interactions. Additionally, it offers performance optimizations by handling tasks like SSL termination and request transformations, which improve the efficiency of the microservices ecosystem.
Spring Gateway Requirements
Read first the following article, which contains a description of the initial microservices architecture: Getting started with Spring Cloud: Service Discovery
Next, we will add to the initial project a new Spring Boot project “gateway-proxy
” with the following key dependencies:
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <!-- Other dependencies --> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
Here is a Sequence Diagram which shows the interaction between the various microservices in this project:
Next, let’s code our Gateway project.
Coding the Gateway Project
The key part of a Gateway project is the mapping of incoming calls to the microservices. You can either code it statically in your configuration (application.yml
) or define a RouteLocator in your @Configuration Bean.
For example:
import org.springframework.cloud.gateway.route.RouteLocator; import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @Configuration class ProxyConfig { @Bean RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("users_service_route", route -> route.path("/user-service/**") .and() .method(HttpMethod.POST) .filters(filter -> filter.stripPrefix(1) ) .uri("lb://user-service")).build(); } }
- The Route Locator defines a route with the ID
"users_service_route"
. This ID is simply a name to identify the route. route.path("/user-service/**")
: Specifies that this route maps the incoming request path starting with/user-service/
and().method(HttpMethod.POST)
: Adds a condition that the route will only match if the HTTP method isPOST
.filters(filter -> filter.stripPrefix(1))
: Applies a filter to the route. ThestripPrefix(1)
filter removes the first segment of the path before forwarding the request to the downstream service.uri("lb://user-service")
: Specifies the destination URI for the route. Thelb://
prefix indicates that this URI should be resolved using Spring Cloud’s load balancer, meaning it will forward the request to an instance of theuser-service
microservice.
Modify microservices to use the Gateway
Within your B2B microservices call, you need to modify your caller services to use the proxy instead of calling directly the target microservices. In our case, the card-service
will go through the proxy gateway to reach the user-service
.
To do that, modify the UserServiceClient Class in your card-service
project:
@Component public class UserServiceClient { private final RestTemplate restTemplate; private final DiscoveryClient discoveryClient; UserServiceClient(@Qualifier("restTemplate") RestTemplate restTemplate, DiscoveryClient discoveryClient) { this.restTemplate = restTemplate; this.discoveryClient = discoveryClient; } public ResponseEntity<User> registerUser(CardApplicationDto.User userDto) { ServiceInstance instance = discoveryClient.getInstances("proxy") .stream().findAny() .orElseThrow(() -> new IllegalStateException("Proxy unavailable")); return restTemplate.postForEntity(instance.getUri().toString() + "/user-service/registration", userDto, User.class); } }
- As you can see, we will discover the “proxy” Service through the Eureka Server first.
- Then, we will call the “
/proxy/user-service/registration
” URI.
If you go back to the RouteLocator you will see that it uses the stripPrefix
to remove the “/proxy
” section and forward the call to the “/user-service/registration
“
Test the Application Stack with the Gateway
Next, we will restart our Microservices stack. When all services are up and running, you should be able to see in your Eureka Console the user-service, the card-service and the gateway-proxy:
You can call the front-end microservice and verify that the call hits the proxy before going to the user-service:
curl -X POST -H "Content-Type: application/json" http://localhost:9080/application -d @cardApplication.json
You should be able to see in your proxy Console a similar output:
2024-08-27T16:21:24.080+02:00 DEBUG 2596 --- [proxy] [ctor-http-nio-2] [ ] .f.h.o.ObservedResponseHttpHeadersFilter : The response was handled for observation {name=http.client.requests(null), error=null, context=name='http.client.requests', contextualName='null', error='null', lowCardinalityKeyValues=[http.method='POST', http.status_code='UNKNOWN', spring.cloud.gateway.route.id='users_service_route', spring.cloud.gateway.route.uri='lb://user-service'], highCardinalityKeyValues=[http.uri='http://10.10.11.32:9083/registration']}}
Load Balancing Calls with the Gateway
If you pay attention to the RouteLocator definition, you will see it specifies the destination URI with the lb://
prefix . This indicates that this URI will forward and load balance the request to an instance of the user-service
microservice.
You can easily test load balancing by creating a clone of the user-service project. For example, copy and paste the folder into user-service2 .
Within your user-service2 project, choose a different port to avoid conflicts with the existing user-service:
spring: application: name: user-service server: address: 0.0.0.0 port: 9088
Upon start of the user-service2 application, you should see two instances of the user-service in the Eureka Console:
Finally, by calling the card-service front-end, you should see from the Console logs that each call is load balanced across the user-service
instances:
curl -X POST -H "Content-Type: application/json" http://localhost:9080/application -d @cardApplication.json
Source code: https://github.com/fmarchioni/masterspringboot/tree/master/cloud/cloud-gateway
Conclusion
Spring Cloud Gateway is a critical component in the Spring Cloud ecosystem. It provides centralized control over routing, security, and cross-cutting concerns, thereby simplifying the management of microservices and enhancing the overall robustness and scalability of the system. As microservices architectures continue to grow in complexity, a robust gateway solution like Spring Cloud Gateway becomes indispensable for maintaining a reliable, scalable, and secure system.
Found the article helpful? if so please follow us on Socials