In the last article, we looked into Spring Cloud Gateway and discussed its core concepts and capabilities as a non-blocking API Gateway. In this article, we will use those concepts to develop an end to end microservice architecture based application using spring cloud. In the process, we will use spring cloud gateway as a gateway provider, Netflix Eureka as a discovery server with circuit breaker pattern using Netflix Hystrix.
In this implementation, we will have 2 different spring boot based microservices as first-service and second-service. These services will register themselves to the discovery server. Spring cloud gateway will use Netflix client API to discover the service and route the request to load balanced downstream services.
Here is an example of spring cloud Netflix with Zuul as Gateway Provider.
Discovery Server Implementation
In a microservice architecture, service discovery is one of the key tenets. Service discovery automates the process of multiple instance creations on demand and provides high availability of our microservices.
Below is the pom.xml and application.yml file required for this integration. You can visit my previous article for the complete integration of Netflix Eureka with Spring Cloud
pom.xml
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </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> <repositories> <repository> <id>spring-milestones</id> <name>Spring Milestones</name> <url>https://repo.spring.io/milestone</url> </repository> </repositories> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>application.yml
spring: application: name: discovery-service eureka: client: eureka-server-connect-timeout-seconds: 5 enabled: true fetch-registry: false register-with-eureka: false server: port: 8761
Spring Cloud Gateway Implementation
Project Setup
First, we will generate a sample spring boot project from https://start.spring.io and import into workspace. The selected dependencies are Gateway, Hystrix and Actuator.
We will also add spring-cloud-starter-netflix-eureka-client dependency in our pom.
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
Spring Cloud Route Configuration
Route is the basic building block of the gateway. It is defined by an ID, a destination URI, a collection of predicates and a collection of filters. A route is matched if aggregate predicate is true.
Spring Cloud Gateway provides many built-in Route Predicate Factories such as Path, Host, Date/Time, Method, Header etc. We can use these built-in route with conjuction with and() or or() to define our routes. 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 and the request is routed to matched route.
You can follow my another article for the complete spring cloud gateway route predicates..
Below is our route configuration. We have 2 different routes defined for our 2 microservices - first-service and second-service.
Here, we have defined functional routes with Spring WebFlux but you are free to use traditional Spring MVC style routes. You can visit this article for both functional and traditional style API definition.
BeanConfig.javapackage com.devglan.gatewayservice; 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; @Configuration public class BeanConfig { @Bean public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) { return builder.routes() .route(r -> r.path("/api/v1/first/**") .filters(f -> f.rewritePath("/api/v1/first/(?.*)", "/${remains}") .addRequestHeader("X-first-Header", "first-service-header") .hystrix(c -> c.setName("hystrix") .setFallbackUri("forward:/fallback/first"))) .uri("lb://FIRST-SERVICE/") .id("first-service")) .route(r -> r.path("/api/v1/second/**") .filters(f -> f.rewritePath("/api/v1/second/(? .*)", "/${remains}") .hystrix(c -> c.setName("hystrix") .setFallbackUri("forward:/fallback/second"))) .uri("lb://SECOND-SERVICE/") .id("second-service")) .build(); } }
In the above configuration, the first route is matched for a path that has a predicate defined to match all the request URL with /api/v1/first/**
. After these multiple filters such as rewritePath, addRequestHeader etc is applied. These are in-built filters provided by the gateway out of the box.
rewritePath filter takes a path regexp parameter and a replacement parameter.
The AddRequestHeader GatewayFilter Factory takes a name and value parameter and adds the configured header param in the request.
The Hystrix GatewayFilter Factory requires a single name parameter, which is the name of the HystrixCommand. The request will be forwarded to the controller matched by the fallbackUri parameter.
Below is an equivalent application.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 - name: Hystrix args: name: hystrix fallbackUri: forward:/fallback/first
Spring Cloud Gateway Application Config
Below is our application.yaml file.
application.yamlhystrix.command.fallbackcmd.execution.isolation.thread.timeoutInMilliseconds: 2000 spring: application: name: api-gateway server: port: 8088 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka register-with-eureka: false instance: preferIpAddress: true management: endpoints: web: exposure: include: hystrix.stream
Hystrix Fallback Command
Below is our controller implementation whose endpoints will be called on failure of our microservices.
HystrixController.javapackage com.devglan.gatewayservice; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/fallback") public class HystrixController { @GetMapping("/first") public String firstServiceFallback(){ return "This is a fallback for first service."; } @GetMapping("/second") public String secondServiceFallback(){ return "Second Server overloaded! Please try after some time."; } }
Now, let us define our spring boot main class.
GatewayServiceApplication.javapackage com.devglan.gatewayservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class GatewayServiceApplication { public static void main(String[] args) { SpringApplication.run(GatewayServiceApplication.class, args); } }
Microservices Implementation
First Service
These services are very simple implementation with only one controller defined for demo purpose.
FirstController.javapackage com.devglan.gatewayservice.controller; import org.springframework.web.bind.annotation.*; @RestController public class FirstController { @GetMapping("/test") public String test(@RequestHeader("X-first-Header") String headerValue){ return headerValue; } }application.yaml
spring: application: name: first-service server: port: 8086 eureka: client: service-url: defaultZone: http://localhost:8761/eurekaFirstServiceApplication.java
package com.devglan.gatewayservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class FirstServiceApplication { public static void main(String[] args) { SpringApplication.run(FirstServiceApplication.class, args); } }
Second Service Implementation
SecondController.java@RestController public class SecondController { @GetMapping("/second") public String test(@RequestHeader("X-second-Header") String headerValue){ return headerValue; } }application.yaml
spring: application: name: second-service server: port: 8087 eureka: client: service-url: defaultZone: http://localhost:8761/eureka
Adding CORS Configuration in Spring Cloud Gateway
While making API calls from a browser app with cross-origin, we frequently get errors as Cross-Origin Request Blocked and the API call fails. If you have spring cloud gateway as a Gateway provider, then you can define below filter to allow such Cross-origin request.
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpHeaders; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource; import org.springframework.web.reactive.config.CorsRegistry; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.config.WebFluxConfigurer; /** * @author Dhiraj Ray * */ @Configuration @EnableWebFlux public class CORSFilter implements WebFluxConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowCredentials(true) .allowedOrigins("*") .allowedHeaders("*") .allowedMethods("*"); } @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.setAllowCredentials(true); corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); corsConfiguration.addAllowedOrigin("*"); UrlBasedCorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource(); corsConfigurationSource.registerCorsConfiguration("/**", corsConfiguration); return new CorsWebFilter(corsConfigurationSource); } }
Serving Static Content Spring Cloud Gateway
The best practice to host your images or other static contents is the CDN but it is a very common practice to host static files at the Gateway level. Static files can be any image or .html file. As spring cloud gateway is developed upon spring web flux, we can use the spring web flux way to serve our static content from spring cloud gateway.
By default, Spring Boot serves static content from /public
, /static
, /resources
, /META-INF/resources
. Hence, without any extra configuration, you can place your static content in any one of these folders and it can be directly accessed with a HTTP request.
There are other different ways too to access these static resources and the complete implementation can be found in my another article here.
Conclusion
In this article, we discussed about spring cloud gateway and created a demo application using it. The source code can be downloaded from github here.