Inter-Service Communication in Spring Boot Microservices using OpenFeign

Inter-Service Communication in Spring Boot Microservices using OpenFeign

Inter-Service Communication in Spring Boot Microservices using OpenFeign

In a microservices architecture, effective communication between services is crucial for building robust and scalable applications. OpenFeign, a part of the Spring Cloud ecosystem, is used in the context of microservices development. It simplifies the process of creating HTTP clients to call RESTful services and APIs by allowing developers to write interface definitions with annotations that specify how the HTTP requests should be constructed.

1. Introduction

In this tutorial, we’ll explore how to set up and use OpenFeign in a Spring Boot application to enable seamless inter-service communication.

Prerequisites:

A working order microservice.

2. Setting Up the Development Environment

Creating a new Spring Boot project becomes straightforward using the Spring Initializr. Visit the Spring Initializr website, choose the necessary dependencies like Spring Web, Spring Data JPA, and MySQL Driver, and then click ‘Generate’ to obtain the project template.

2.1 Adding Necessary Dependencies

In the order microservice pom.xml file, make sure we have the following dependencies:

...

<!-- Spring Cloud Starter OpenFeign -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>3.1.8</version>
</dependency>

<!-- Spring Cloud Dependencies (BOM) -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2020.0.3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

...

2.2 Configuring product microservice’s URL

Create a property to store product microservice endpoint in the application.properties file:

products.url=http://localhost:8081/api/products
product-service.name=product-service

3. Enable Feign Clients

In our main Spring Boot application class (annotated with @SpringBootApplication), add the @EnableFeignClients annotation to enable Feign client creation:

@SpringBootApplication
@EnableFeignClients
public class OrdermgmtApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrdermgmtApplication.class, args);
    }
}

This annotation enables the discovery and creation of Feign clients.

4. Define Feign Client

Next, define a Feign client interface that represents the service we want to communicate with. Use Spring MVC annotations to specify the HTTP requests:

@FeignClient(name = "${product-service.name}", url = "${products.url}")
public interface ProductClient {

    @GetMapping("/{id}")
    ResponseEntity<Map<String, Object>> getProductById(@PathVariable("id") Long id);
}

The @FeignClient denotes the remote service. The method inside the interface represents the endpoint we want to call.

When we specify a URL in the @FeignClient annotation, we are providing the exact location of the target service we want to communicate with. Feign will bypass service discovery like Netflix Eureka and make direct HTTP requests to the provided URL.

In this case, we are calling http://localhost:8081/api/products/{id} to get the product details by product id.

5. Use the Feign Client

Now, we can inject and use the Feign client in the order service to make HTTP calls to the product service:

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private ProductClient productClient;
    
    @Autowired
    private ObjectMapper objectMapper;
    ...
 
    @Override
    public OrderDto getOrderById(Long id) {
        logger.info("Fetching order by ID: {}", id);
        Order order = orderRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Order not found"));
        OrderDto orderResponse = new ModelMapper().map(order, OrderDto.class);
        ResponseEntity<Map<String, Object>> response = productClient.getProductById(order.getProductId());
        if (response != null && response.getBody() != null) {
            // Extract the ProductDto list from the response
            ProductDto productDto = objectMapper.convertValue(response.getBody().get("data"), ProductDto.class);
            orderResponse.setProductDto(productDto);
        }

        return orderResponse;
    }
}

6. Customize Feign Client

We have the flexibility to customize various aspects of the Feign client to suit our specific requirements. Here are some common customizations:

6.1. Timeouts

We can set the connection and read timeouts for our Feign client. These timeouts define how long the client will wait for specific events.

feign.client.config.default.connectTimeout=5000 # (ms)
feign.client.config.default.readTimeout=10000

6.2. Retry Mechanism

Create a custom retryer class, like CustomRetryer, implementing the feign.Retryer interface. This allows us to define our own retry logic, such as the maximum number of retries and the backoff period between retries.

public class CustomRetryer implements Retryer {
    private final int maxAttempts;
    private final long backoff;
    private int attempt;

    public CustomRetryer() {
        this(2000, 3); // Default backoff and maxAttempts
    }

    public CustomRetryer(long backoff, int maxAttempts) {
        this.backoff = backoff;
        this.maxAttempts = maxAttempts;
        this.attempt = 1;
    }

    @Override
    public void continueOrPropagate(RetryableException e) {
        if (attempt++ >= maxAttempts) {
            throw e; // Stop retrying after maxAttempts
        }
        try {
            Thread.sleep(backoff); // Wait before the next retry
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
        }
    }

    @Override
    public Retryer clone() {
        return new CustomRetryer(backoff, maxAttempts);
    }
}

Configure our Feign client to use this custom retryer:

@Configuration
public class CustomFeignConfig {
    @Bean
    public Retryer retryer() {
        return new CustomRetryer();
    }
}

If we encounter specific HTTP status codes (e.g., 503 Service Unavailable), we can throw a RetryableException to trigger retries.

We can also use the @Retryable annotation from Spring Retry to specify the number of maxAttempts and the backoff settings. In this approach, we would handle retries explicitly within the methods of our Feign client interface.

@FeignClient(name = "${product-service.name}", url = "${products.url}")
public interface ProductClient {
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 1000))
    @GetMapping("/{id}")
    ResponseEntity<Map<String, Object>> getProductById(@PathVariable("id") Long id);
}

6.3. Logger Level

We can configure the level of detail for logging Feign requests. Options include:

  • NONE: No logging (default).
  • BASIC: Log only essential request and response information.
  • HEADERS: Log request and response headers.
  • FULL: Log detailed information, including headers and request/response bodies.

Set the preferred logger level in the application’s properties:

feign.client.config.default.loggerLevel=FULL

6.4. Request Interceptors:

Request interceptors allow us to modify every request made by a Feign client. We can use them to add headers, authentication tokens, or any other modifications to every request. Implement the RequestInterceptor interface and define our interceptor logic.

@Component
public class CustomRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        requestTemplate.header("Authorization", "Bearer YOUR_TOKEN_HERE");
    }
}

Spring will automatically apply this interceptor to all Feign clients in the application.

6.5 Custom Error Decoder

An error decoder allows us to handle specific HTTP error responses and customize the behaviour when exceptions occur. By default, Feign throws a FeignException for responses with HTTP status 400 or higher. Suppose we want to handle specific HTTP status codes (e.g., 400 Bad Request and 404 Not Found) with custom messages.

Create a custom error decoder class (e.g., CustomErrorDecoder) that implements feign.codec.ErrorDecoder. The ErrorDecoder interface has a single method, decode, which takes a FeignException as its input and returns an object of type Exception.

public class CustomErrorDecoder implements ErrorDecoder {
    private final ErrorDecoder defaultErrorDecoder = new Default();

    @Override
    public Exception decode(String methodKey, Response response) {
        try {
            // Read the response body and parse it as a custom exception message
            ObjectMapper objectMapper = new ObjectMapper();
            ExceptionMessage exceptionMessage = objectMapper.readValue(
                response.body().asInputStream(), ExceptionMessage.class
            );

            // Handle specific status codes
            switch (response.status()) {
                case 400:
                    return new BadRequestException(
                        exceptionMessage.getMessage() != null ?
                        exceptionMessage.getMessage() : "Bad Request"
                    );
                case 404:
                    return new NotFoundException(
                        exceptionMessage.getMessage() != null ?
                        exceptionMessage.getMessage() : "Not found"
                    );
                default:
                    // Fallback to the default error decoder
                    return defaultErrorDecoder.decode(methodKey, response);
            }
        } catch (IOException e) {
            return new Exception(e.getMessage());
        }
    }
}

Configure our Feign client to use this custom error decoder:

@Configuration
public class CustomFeignConfig {
    @Bean
    public ErrorDecoder errorDecoder() {
        return new CustomErrorDecoder();
    }
}

In this example, we extract the custom message from the response body and map it to specific exceptions (BadRequestException and NotFoundException).

7. Testing with JUnit and Postman

Testing is essential to ensure the quality of our microservices. We’ll write unit tests using JUnit and Mockito to test our order microservices thoroughly.

First, create a new test class for OrderServiceImpl. Name it something like OderServiceImplTest. In the test class, we use Mockito to mock the dependencies that OderServiceImpl relies on. For example, if OrderServiceImpl depends on a repository or another service, mock those components using @MockBean or @Mock. The @InjectMocks annotation is then used to inject these mock objects into the fields of our class under test.

@SpringBootTest
public class OrderServiceImplTest {

    @InjectMocks
    private OrderServiceImpl orderService;

    @Mock
    private ProductClient productClient;

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private ObjectMapper objectMapper;

    private OrderDto orderDto;
    private Long orderID = 1L;

    @BeforeEach
    public void setUp() {
        orderDto = new OrderDto(new Date(), 1L, 2, "CREATED");
    }

    @Test
    public void testGetOrderById() {
        Order order = new Order();
        when(orderRepository.findById(anyLong())).thenReturn(Optional.of(order));

        ProductDto mockProduct = new ProductDto();
        mockProduct.setTitle("Sample Product");
        mockProduct.setPrice(29.90);
        mockProduct.setDescription("This is a sample product description.");
        Mockito.when(productClient.getProductById(orderID))
            .thenReturn(new ResponseEntity<>(Map.of("data", mockProduct), HttpStatus.OK));
        Mockito.when(objectMapper.convertValue(Mockito.any(), Mockito.eq(ProductDto.class)))
            .thenReturn(new ProductDto("Sample Product", "This is a sample product description.", 29.90));
  
        OrderDto result = orderService.getOrderById(orderID);
        assertNotNull(order);

        Mockito.verify(productClient, Mockito.times(1)).getProductById(1L);
        assertEquals("Sample Product", result.getProductDto().getTitle());
    }

    ...
}

Next, we can try to invoke the API via Postman. To do so, we need to start both product and order microservices.

We then insert one product record.

image Inter-Service Communication in Spring Boot Microservices using OpenFeign

Follow by inserting one order record with the productId set to 1.

image-2 Inter-Service Communication in Spring Boot Microservices using OpenFeign

Now, send a GET request to the http://localhost:8082/api/orders/1 endpoint to retrieve the order record.

image-3 Inter-Service Communication in Spring Boot Microservices using OpenFeign

We should see the product details returned in the productDto object. This proves that our inter-communication using Feign is working correctly.

8. Conclusion

In this tutorial, we explored Feign, a declarative HTTP client developed by Netflix. Feign simplifies the creation of HTTP API clients by allowing developers to declare and annotate an interface, while the actual implementation is provisioned at runtime. We covered setting up dependencies, creating a client interface, and understanding Feign’s capabilities. 

Share this content:

Leave a Comment

Discover more from nnyw@tech

Subscribe now to keep reading and get access to the full archive.

Continue reading