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.
Follow by inserting one order record with the productId
set to 1.
Now, send a GET request to the http://localhost:8082/api/orders/1
endpoint to retrieve the order record.
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