In this post we'll see how to configure API Gateway in Microservices using Spring Cloud Gateway with Eureka as service registry and Resilience4J as circuit breaker.
Spring Cloud Gateway
Spring Cloud Gateway project provides an API Gateway which acts as a front for routing to different microservices.
Spring Cloud Gateway is built on Spring Boot, Spring WebFlux, and Project Reactor. Spring Cloud Gateway requires the Netty runtime provided by Spring Boot and Spring Webflux. It does not work in a traditional Servlet Container or when built as a WAR. Therefore, you'll see the Gateway service starting on a Netty server rather than on a Tomcat server which is used for Spring Boot web applications by default.
- Route: It 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 the aggregate predicate is true and forwards the request to the destination URI.
- Predicate: This is a Java 8 Function Predicate. The input type is a Spring Framework ServerWebExchange. This lets you match on anything from the HTTP request, such as headers or parameters.
- Filter: These are instances of GatewayFilter that have been constructed with a specific factory. Used to modify requests and responses and apply features such as rate limiting, circuit breaker before or after sending the downstream request.
Advantages of using API gateway
- API gateway sits between an external client and multiple microservices and acts as a single entry & exit point. That makes gateway as a single implementation point to provide cross cutting concerns to all the microservices such as security (Initial authentication), monitoring/metrics, and resiliency (circuit breaker, rate limiting).
- Client doesn’t need to know about all the backend services. They need to communicate only with API gateway.
Spring Boot Microservice with API Gateway example
In this example we'll configure an API Gateway, register it as a service with Eureka and also configure Resilience4J as circuit breaker at the gateway level.
We'll use the example used in this post as a base example- Spring Boot Microservice - Service Registration and Discovery With Eureka with that you should have CustomerService and AccountService ready and registered with Eureka.
Creating Gateway Service
Gateway service is created as a separate microservice. If you are using Spring Tool Suite then create a new Spring Starter Project and name it gateway-service.
Select "Gateway" as dependency under "Spring Cloud Routing". Select "Resilience4J" under "Spring Cloud Circuit Breaker" and "Eureka Discovery Client" under "Spring Cloud Discovery".
With that the created pom.xml should look like this-
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.2</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.netjstech</groupId> <artifactId>gatewayservice</artifactId> <version>0.0.1-SNAPSHOT</version> <name>gateway-service</name> <description>Cloud Gateway Service</description> <properties> <java.version>17</java.version> <spring-cloud.version>2022.0.4</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </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> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
- Eureka client is needed because gateway-service will be registered with Eureka server.
- Resilience4J is needed to implement circuit breaker functionality as gateway level.
- Actuator is needed to see metrics for circuit breaker.
- Of course, cloud-starter-gateway is needed since we are implementing an API gateway.
Application class with @EnableDiscoveryClient annotation
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; @SpringBootApplication @EnableDiscoveryClient public class GatewayServiceApplication { public static void main(String[] args) { SpringApplication.run(GatewayServiceApplication.class, args); } }
API Gateway configuration
You can configure gateway using config or code. Here it is done using config in application.yml file.
server: port: 8181 eureka: client: serviceUrl: defaultZone: http://localhost:8761/eureka management: endpoint: health: show-details: always endpoints: web: exposure: include: health health: circuitbreakers: enabled: true resilience4j.circuitbreaker: configs: default: registerHealthIndicator: true spring: application: name: gateway-service cloud: gateway: discovery: locator: enabled: false lower-case-service-id: true routes: - id: customer-service uri: http://localhost:8081 #lb://customer-service predicates: - Path=/customer/** filters: - name: CircuitBreaker args: name: CusCircuitBreaker fallbackUri: forward:/fallback/customer - id: account-service uri: lb://account-service predicates: - Path=/account/** filters: - name: CircuitBreaker args: name: AcctCircuitBreaker fallbackUri: forward:/fallback/account
If we go over the configuration properties from the beginning of the file.
- Netty server, this service starts on, listens on the port 8181.
- Next there is configuration for actuator so that metrics for circuit breaker is also accessible when http://localhost:8181/actuator/health is accessed.
- Properties for gateway starts from routes which has a id, a destination URI to which the request is forwarded when the predicate matches.
- Predicate has a wildcard which means URLs having anything after /customer/ or /account/ should be matched with these predicates.
- Here note that URI for customer-service is a direct URL (a static route) where as for account-service it is lb://account-service which means a load balanced instance.
- Since we are also configuring circuit breaker that configuration is provided using filters. A fallback URI is configured here and that path is mapped to a method in Controller. This method is called as a fallback when service is not available and circuit is open.
Gateway Fallback Controller
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/fallback") public class GatewayFallbackController { @RequestMapping("/account") public ResponseEntity<String> getAccount() { System.out.println("In account fallback"); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("There is a problem in AccountService, please try after some time"); } @RequestMapping("/customer") public ResponseEntity<String> getCustomer() { System.out.println("In customer fallback"); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("There is a problem in CustomerService, please try after some time"); } }
Note that @RequestMapping is used with methods too rather than using @GetMapping, @PostMapping etc. as that makes more sense with fallback methods which are neither used to fetch data nor to insert or update or delete.
Resilience4J Configuration
Circuit breaker configuration can also be prvided using code or config in this example it is provided using code.
import java.time.Duration; import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory; import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder; import org.springframework.cloud.client.circuitbreaker.Customizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.SlidingWindowType; import io.github.resilience4j.timelimiter.TimeLimiterConfig; @Configuration public class ResilienceConfig { @Bean public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer() { return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id) .timeLimiterConfig(TimeLimiterConfig.custom() .timeoutDuration(Duration.ofMillis(2000)) .build()) .circuitBreakerConfig(CircuitBreakerConfig.custom() .slidingWindowSize(10) .slidingWindowType(SlidingWindowType.COUNT_BASED) .permittedNumberOfCallsInHalfOpenState(3) .failureRateThreshold(50.0F) .waitDurationInOpenState(Duration.ofSeconds(5)) .slowCallDurationThreshold(Duration.ofMillis(200)) .slowCallRateThreshold(50.0F) .automaticTransitionFromOpenToHalfOpenEnabled(true) .minimumNumberOfCalls(5) .build()) .build()); } }
To get more information about the configured properties please refer this post- Spring Boot Microservice Circuit Breaker Using Resilience4j
Testing API gateway
To test API gateway please start all the services- eureka-discovery-service, customer-service, account-service and gateway-service.
Now all the interaction with the services is done using gateway-service. If we want to get customer information we'll call http://localhost:8181/customer/1 not http://localhost:8081/customer/1 same way to access account-service http://localhost:8181/account/.
Using the predicate which was configured in application.yml it will match it to the correct destination URL.
Inserting new Account
Accessing Customer information along with associated accounts.
If Customer Service is not reachable (goes to fallback)-
If Account Service is not reachable (goes to fallback)-
You can access the circuit breaker metrics using the URL- http://localhost:8181/actuator/health Following image shows the metrics when the circuit is open because of the consecutive failed calls.
That's all for this topic Spring Boot Microservice + API Gateway + Resilience4J. If you have any doubt or any suggestions to make please drop a comment. Thanks!
>>>Return to Spring Tutorial Page
Related Topics
You may also like-
No comments:
Post a Comment