Tuesday, February 27, 2024

Spring Boot Microservice + API Gateway + Resilience4J

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

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.

  1. 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.
  2. 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.
  3. 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

  1. 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).
  2. 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

API Gateway Example

Accessing Customer information along with associated accounts.

If Customer Service is not reachable (goes to fallback)-

API Gateway with circuit breaker

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

  1. Spring Boot Microservice - Externalized Configuration With Spring Cloud Config
  2. Spring Boot Microservice - Load-Balancing With Spring Cloud LoadBalancer
  3. Spring Boot Microservice - Eureka + LoadBalancer + Feign
  4. Spring Boot Observability - Distributed Tracing, Metrics
  5. Spring Boot Event Driven Microservice With Kafka

You may also like-

  1. Spring Bean Life Cycle
  2. Connection Pooling Using C3P0 Spring Example
  3. Spring MVC - Binding List of Objects Example
  4. Spring Batch Processing With List of Objects in batchUpdate() Method
  5. Package in Java
  6. Java Lambda Expression Callable Example
  7. Abstract Class in Python
  8. Angular Event Binding With Examples

No comments:

Post a Comment