Chapitre 5: Communication Inter-Services
đŻ Objectifs du Chapitreâ
- Comprendre les modes de communication synchrone vs asynchrone
- Implémenter des appels REST avec OpenFeign
- Appliquer les patterns de résilience avec Resilience4j
- MaĂźtriser le Circuit Breaker, Retry, Rate Limiter
- Implémenter des fallbacks efficaces
1. Modes de Communicationâ
1.1 Communication Synchroneâ
Caractéristiques:
- Client attend la réponse
- Simple à implémenter
- Couplage temporel (dépendance sur la disponibilité)
- Latence cumulée
1.2 Communication Asynchroneâ
Caractéristiques:
- Fire and forget
- Découplage temporel
- Plus résilient
- Plus complexe (eventual consistency)
1.3 Quand Utiliser Quoi?â
| Cas d'Usage | Synchrone | Asynchrone |
|---|---|---|
| Validation utilisateur | â | |
| Obtenir dĂ©tails produit | â | |
| CrĂ©er une commande | â | |
| Envoyer notification | â | |
| Mettre Ă jour analytics | â | |
| Synchroniser donnĂ©es | â |
2. OpenFeign - Client HTTP DĂ©claratifâ
2.1 Qu'est-ce que Feign?â
Feign (maintenant OpenFeign) est un client HTTP déclaratif qui permet de définir des appels REST via des interfaces Java.
// Sans Feign (RestTemplate)
String url = "http://user-service/api/users/" + userId;
User user = restTemplate.getForObject(url, User.class);
// Avec Feign (déclaratif)
User user = userClient.getUserById(userId);
2.2 DĂ©pendances Mavenâ
<dependencies>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Eureka Client (pour la découverte) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- Resilience4j -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
</dependencies>
2.3 Activer Feignâ
package com.example.orderservice;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableFeignClients // Active Feign
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
2.4 DĂ©finir un Feign Clientâ
package com.example.orderservice.client;
import com.example.orderservice.dto.UserDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
@FeignClient(name = "user-service") // Nom du service dans Eureka
public interface UserClient {
@GetMapping("/api/users/{id}")
UserDto getUserById(@PathVariable("id") Long id);
@GetMapping("/api/users")
List<UserDto> getAllUsers();
@GetMapping("/api/users/{id}/exists")
Boolean userExists(@PathVariable("id") Long id);
}
2.5 DTOâ
package com.example.orderservice.dto;
public class UserDto {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;
// Constructeurs
public UserDto() {}
public UserDto(Long id, String username, String email,
String firstName, String lastName) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}
// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
2.6 Utilisationâ
package com.example.orderservice.service;
import com.example.orderservice.client.UserClient;
import com.example.orderservice.client.ProductClient;
import com.example.orderservice.dto.UserDto;
import com.example.orderservice.dto.ProductDto;
import com.example.orderservice.model.Order;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
private final UserClient userClient;
private final ProductClient productClient;
private final OrderRepository orderRepository;
public OrderService(UserClient userClient,
ProductClient productClient,
OrderRepository orderRepository) {
this.userClient = userClient;
this.productClient = productClient;
this.orderRepository = orderRepository;
}
public Order createOrder(Long userId, Long productId, int quantity) {
// Appel synchrone au User Service
UserDto user = userClient.getUserById(userId);
if (user == null) {
throw new RuntimeException("User not found: " + userId);
}
// Appel synchrone au Product Service
ProductDto product = productClient.getProductById(productId);
if (product == null) {
throw new RuntimeException("Product not found: " + productId);
}
// Créer la commande
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setQuantity(quantity);
order.setTotalPrice(product.getPrice() * quantity);
order.setUserEmail(user.getEmail());
return orderRepository.save(order);
}
}
2.7 Configuration Feignâ
spring:
cloud:
openfeign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 10000
loggerLevel: BASIC
user-service:
connectTimeout: 3000
readTimeout: 5000
loggerLevel: FULL
# Logging Feign
logging:
level:
com.example.orderservice.client: DEBUG
2.8 Niveaux de Logging Feignâ
| Niveau | Description |
|---|---|
NONE | Aucun log |
BASIC | Méthode, URL, status, temps |
HEADERS | + Headers |
FULL | + Body request/response |
3. Resilience4j - Patterns de RĂ©silienceâ
3.1 Pourquoi la RĂ©silience?â
3.2 DĂ©pendancesâ
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- Si vous voulez les annotations @CircuitBreaker, @Retry, etc. -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>
<!-- AOP pour les annotations -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
4. Circuit Breaker Patternâ
4.1 Ătats du Circuitâ
4.2 Configuration YAMLâ
resilience4j:
circuitbreaker:
configs:
default:
# Taille de la fenĂȘtre glissante
slidingWindowSize: 10
# Type de fenĂȘtre (COUNT_BASED ou TIME_BASED)
slidingWindowType: COUNT_BASED
# Nombre minimum d'appels avant calcul
minimumNumberOfCalls: 5
# Seuil d'échec pour ouvrir (%)
failureRateThreshold: 50
# Durée en état OPEN (ms)
waitDurationInOpenState: 10000
# Appels permis en HALF_OPEN
permittedNumberOfCallsInHalfOpenState: 3
# Transition auto OPEN -> HALF_OPEN
automaticTransitionFromOpenToHalfOpenEnabled: true
# Exceptions considérées comme échecs
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.FeignException
# Exceptions ignorées
ignoreExceptions:
- com.example.BusinessException
instances:
userService:
baseConfig: default
productService:
baseConfig: default
failureRateThreshold: 30
4.3 Avec Annotationsâ
package com.example.orderservice.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;
@Service
public class UserServiceClient {
private final UserClient userClient;
public UserServiceClient(UserClient userClient) {
this.userClient = userClient;
}
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}
// Fallback - DOIT avoir la mĂȘme signature + Exception
public UserDto getUserFallback(Long userId, Exception ex) {
// Log l'erreur
log.warn("Circuit breaker triggered for user {}. Error: {}",
userId, ex.getMessage());
// Retourner une valeur par défaut ou cached
return new UserDto(userId, "unknown", "unknown@example.com",
"Unknown", "User");
}
}
4.4 Avec Feign (Fallback Factory)â
package com.example.orderservice.client;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@FeignClient(
name = "user-service",
fallbackFactory = UserClientFallbackFactory.class
)
public interface UserClient {
@GetMapping("/api/users/{id}")
UserDto getUserById(@PathVariable("id") Long id);
@GetMapping("/api/users/{id}/exists")
Boolean userExists(@PathVariable("id") Long id);
}
package com.example.orderservice.client;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
private static final Logger log = LoggerFactory.getLogger(UserClientFallbackFactory.class);
@Override
public UserClient create(Throwable cause) {
log.error("UserClient fallback triggered. Cause: {}", cause.getMessage());
return new UserClient() {
@Override
public UserDto getUserById(Long id) {
return new UserDto(id, "fallback-user", "fallback@example.com",
"Fallback", "User");
}
@Override
public Boolean userExists(Long id) {
return false; // Pessimiste en cas d'erreur
}
};
}
}
Activer les fallbacks Feign:
spring:
cloud:
openfeign:
circuitbreaker:
enabled: true
5. Retry Patternâ
5.1 Configurationâ
resilience4j:
retry:
configs:
default:
maxAttempts: 3
waitDuration: 1000
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.RetryableException
ignoreExceptions:
- com.example.BusinessException
instances:
userService:
baseConfig: default
maxAttempts: 5
5.2 Avec Annotationsâ
@Service
public class ProductServiceClient {
@Retry(name = "productService", fallbackMethod = "getProductFallback")
public ProductDto getProduct(Long productId) {
return productClient.getProductById(productId);
}
public ProductDto getProductFallback(Long productId, Exception ex) {
log.warn("Retry exhausted for product {}. Error: {}",
productId, ex.getMessage());
throw new ServiceUnavailableException("Product service unavailable");
}
}
5.3 Combiner Retry et Circuit Breakerâ
@Service
public class UserServiceClient {
// L'ordre est important! Retry PUIS CircuitBreaker
@Retry(name = "userService")
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}
public UserDto getUserFallback(Long userId, Exception ex) {
return getCachedUser(userId);
}
}
Ordre d'exécution:
- Appel initial
- Si Ă©chec â Retry (jusqu'Ă maxAttempts)
- Si tous les retries Ă©chouent â Circuit Breaker
- Si Circuit OPEN â Fallback immĂ©diat
6. Rate Limiter Patternâ
6.1 Configurationâ
resilience4j:
ratelimiter:
configs:
default:
# Nombre de permissions par période
limitForPeriod: 10
# Durée de la période
limitRefreshPeriod: 1s
# Timeout d'attente pour une permission
timeoutDuration: 500ms
instances:
userService:
baseConfig: default
limitForPeriod: 100
limitRefreshPeriod: 1s
6.2 Avec Annotationsâ
@Service
public class ExternalApiClient {
@RateLimiter(name = "externalApi", fallbackMethod = "rateLimitFallback")
public Response callExternalApi(Request request) {
return externalClient.call(request);
}
public Response rateLimitFallback(Request request, RequestNotPermitted ex) {
log.warn("Rate limit exceeded");
throw new TooManyRequestsException("Rate limit exceeded. Try again later.");
}
}
7. Bulkhead Patternâ
7.1 Conceptâ
Le Bulkhead isole les ressources pour éviter qu'un service lent ne bloque tout.
7.2 Configurationâ
resilience4j:
bulkhead:
configs:
default:
maxConcurrentCalls: 10
maxWaitDuration: 500ms
instances:
userService:
baseConfig: default
maxConcurrentCalls: 5
thread-pool-bulkhead:
configs:
default:
maxThreadPoolSize: 10
coreThreadPoolSize: 5
queueCapacity: 20
instances:
productService:
baseConfig: default
7.3 Avec Annotationsâ
@Service
public class OrderService {
@Bulkhead(name = "userService", type = Bulkhead.Type.SEMAPHORE)
public UserDto getUser(Long userId) {
return userClient.getUserById(userId);
}
@Bulkhead(name = "productService", type = Bulkhead.Type.THREADPOOL)
public CompletableFuture<ProductDto> getProductAsync(Long productId) {
return CompletableFuture.supplyAsync(() ->
productClient.getProductById(productId)
);
}
}
8. Time Limiter Patternâ
8.1 Configurationâ
resilience4j:
timelimiter:
configs:
default:
timeoutDuration: 5s
cancelRunningFuture: true
instances:
userService:
baseConfig: default
timeoutDuration: 3s
8.2 Avec Annotationsâ
@Service
public class SlowServiceClient {
@TimeLimiter(name = "slowService", fallbackMethod = "timeoutFallback")
public CompletableFuture<Response> callSlowService() {
return CompletableFuture.supplyAsync(() -> {
// Appel potentiellement lent
return slowClient.call();
});
}
public CompletableFuture<Response> timeoutFallback(TimeoutException ex) {
return CompletableFuture.completedFuture(Response.timeout());
}
}
9. Combiner Tous les Patternsâ
9.1 Configuration ComplĂšteâ
resilience4j:
circuitbreaker:
instances:
userService:
slidingWindowSize: 10
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
retry:
instances:
userService:
maxAttempts: 3
waitDuration: 500ms
enableExponentialBackoff: true
ratelimiter:
instances:
userService:
limitForPeriod: 100
limitRefreshPeriod: 1s
bulkhead:
instances:
userService:
maxConcurrentCalls: 10
timelimiter:
instances:
userService:
timeoutDuration: 3s
9.2 Service avec Tous les Patternsâ
@Service
@Slf4j
public class ResilientUserService {
private final UserClient userClient;
public ResilientUserService(UserClient userClient) {
this.userClient = userClient;
}
// Ordre d'exĂ©cution: Bulkhead â TimeLimiter â RateLimiter â CircuitBreaker â Retry
@Bulkhead(name = "userService")
@TimeLimiter(name = "userService")
@RateLimiter(name = "userService")
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
@Retry(name = "userService")
public CompletableFuture<UserDto> getUser(Long userId) {
return CompletableFuture.supplyAsync(() -> {
log.info("Calling user service for user: {}", userId);
return userClient.getUserById(userId);
});
}
public CompletableFuture<UserDto> getUserFallback(Long userId, Exception ex) {
log.warn("Fallback triggered for user: {}. Error: {}",
userId, ex.getMessage());
return CompletableFuture.completedFuture(
new UserDto(userId, "fallback", "fallback@example.com", "Fallback", "User")
);
}
}
10. Monitoring Resilience4jâ
10.1 Actuator Endpointsâ
management:
endpoints:
web:
exposure:
include: health,circuitbreakers,circuitbreakerevents,retries,ratelimiters
endpoint:
health:
show-details: always
health:
circuitbreakers:
enabled: true
ratelimiters:
enabled: true
10.2 Endpoints Disponiblesâ
# Ătat des circuit breakers
curl http://localhost:8083/actuator/circuitbreakers
# ĂvĂ©nements des circuit breakers
curl http://localhost:8083/actuator/circuitbreakerevents
# Métriques Prometheus
curl http://localhost:8083/actuator/prometheus | grep resilience4j
10.3 RĂ©ponse Exempleâ
{
"circuitBreakers": {
"userService": {
"failureRate": "20.0%",
"slowCallRate": "0.0%",
"state": "CLOSED",
"bufferedCalls": 10,
"failedCalls": 2,
"slowCalls": 0,
"notPermittedCalls": 0
}
}
}
11. RĂ©sumĂ©â
Tableau RĂ©capitulatifâ
| Pattern | ProblÚme Résolu | Quand l'Utiliser |
|---|---|---|
| Circuit Breaker | Cascade de pannes | Appels externes instables |
| Retry | Erreurs transitoires | Timeouts réseau, 503 |
| Rate Limiter | Surcharge | API externes avec quotas |
| Bulkhead | Ăpuisement ressources | Isoler les services lents |
| Time Limiter | Appels bloquants | Services potentiellement lents |
Ordre des Patternsâ
Request â Bulkhead â TimeLimiter â RateLimiter â CircuitBreaker â Retry â Service
Configuration RecommandĂ©e Productionâ
resilience4j:
circuitbreaker:
instances:
default:
slidingWindowSize: 100
failureRateThreshold: 50
waitDurationInOpenState: 30000
permittedNumberOfCallsInHalfOpenState: 10
retry:
instances:
default:
maxAttempts: 3
waitDuration: 1000ms
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
ratelimiter:
instances:
default:
limitForPeriod: 1000
limitRefreshPeriod: 1s
12. Quiz de Validationâ
Question 1â
Quelle est la différence entre @FeignClient(fallback) et @FeignClient(fallbackFactory)?
Voir la réponse
- fallback: Classe statique, pas accĂšs Ă l'exception
- fallbackFactory: Reçoit l'exception en paramÚtre, permet de logger ou de varier le fallback selon l'erreur
Question 2â
Dans quel ordre s'exécutent Retry et CircuitBreaker?
Voir la réponse
Retry s'exécute EN PREMIER (plus proche du service), puis CircuitBreaker.
Si tous les retries Ă©chouent, alors le CircuitBreaker compte un Ă©chec. Si le CircuitBreaker est OPEN, les retries ne sont mĂȘme pas tentĂ©s.