Resilience4j
Tolérance aux pannes avec Circuit Breaker, Retry et Rate Limiter
Introduction à Resilience4j
Qu'est-ce que Resilience4j ?
Resilience4j est une bibliothèque de tolérance aux pannes légère et facile à utiliser conçue pour les applications Java 8 et fonctionnelles. Elle aide à construire des systèmes résilients en fournissant des patterns de tolérance aux pannes tels que Circuit Breaker, Retry, Rate Limiter, Bulkhead, Time Limiter et Cache.
graph LR
A[Microservice A] --> B[Circuit Breaker]
B --> C[Retry]
C --> D[Rate Limiter]
D --> E[Service B
Users API] style A fill:#FF9800,stroke:#E65100 style B fill:#4CAF50,stroke:#388E3C style C fill:#2196F3,stroke:#0D47A1 style D fill:#9C27B0,stroke:#4A148C style E fill:#FF5722,stroke:#E64A19
Users API] style A fill:#FF9800,stroke:#E65100 style B fill:#4CAF50,stroke:#388E3C style C fill:#2196F3,stroke:#0D47A1 style D fill:#9C27B0,stroke:#4A148C style E fill:#FF5722,stroke:#E64A19
Patterns de tolérance aux pannes fournis :
- Circuit Breaker : Empêche les appels répétés vers un service défaillant
- Retry : Réessaie automatiquement les opérations échouées
- Rate Limiter : Limite le nombre d'appels par période de temps
- Bulkhead : Isole les ressources pour éviter la propagation des pannes
- Time Limiter : Limite la durée d'exécution des appels
- Cache : Met en cache les résultats pour améliorer les performances
Architecture des Microservices avec Resilience4j
Structure de l'application
graph TD
A[Eureka Server
Port: 8761] --> B[Microservice A
Client - Port: 8080] A --> C[Microservice B
Users - Port: 8081] A --> D[Microservice C
Products - Port: 8082] B -- RestTemplate/OpenFeign --> E[Resilience4j] E --> C E --> D style A fill:#2196F3,stroke:#0D47A1 style B fill:#FF9800,stroke:#E65100 style C fill:#4CAF50,stroke:#388E3C style D fill:#9C27B0,stroke:#4A148C style E fill:#FF5722,stroke:#E64A19
Port: 8761] --> B[Microservice A
Client - Port: 8080] A --> C[Microservice B
Users - Port: 8081] A --> D[Microservice C
Products - Port: 8082] B -- RestTemplate/OpenFeign --> E[Resilience4j] E --> C E --> D style A fill:#2196F3,stroke:#0D47A1 style B fill:#FF9800,stroke:#E65100 style C fill:#4CAF50,stroke:#388E3C style D fill:#9C27B0,stroke:#4A148C style E fill:#FF5722,stroke:#E64A19
Rôles des microservices :
- Microservice A : Client qui utilise Resilience4j pour communiquer
- Microservice B : Service de gestion des utilisateurs
- Microservice C : Service de gestion des produits
- Eureka Server : Serveur de découverte de services
- Resilience4j : Patterns de tolérance aux pannes
Microservice B - Service Utilisateurs (Simple)
1 Structure du projet
Arborescence du projet :
microserviceB/
├── pom.xml
└── src/main/
├── java/com/example/users/
│ ├── UsersMicroserviceApplication.java
│ ├── controller/
│ │ └── UserController.java
│ ├── model/
│ │ └── User.java
└── resources/
└── application.properties
2 Fichier pom.xml
Dépendances Maven :
microserviceB/pom.xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
3 Configuration application.properties
Fichier de configuration :
microserviceB/src/main/resources/application.properties
# === CONFIGURATION SERVEUR ===
server.port=8081
spring.application.name=service-b
# === CONFIGURATION EUREKA ===
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.instance.prefer-ip-address=true
4 Modèle User
Modèle User :
microserviceB/src/main/java/com/example/users/model/User.java
package com.example.users.model;
/**
* Modèle représentant un utilisateur
*/
public class User {
private Long id;
private String name;
private String email;
private String department;
// Constructeurs
// Getters et Setters
5 Contrôleur REST
Contrôleur REST :
microserviceB/src/main/java/com/example/users/controller/UserController.java
package com.example.users.controller;
import com.example.users.model.User;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
/**
* Contrôleur REST pour les opérations sur les utilisateurs
*/
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "*")
public class UserController {
// Stockage en mémoire pour la démonstration
private final ConcurrentHashMap<Long, User> users = new ConcurrentHashMap<>();
private final AtomicLong counter = new AtomicLong();
// Initialisation avec quelques utilisateurs
public UserController() {
users.put(counter.incrementAndGet(), new User(1L, "John Doe", "john@example.com", "IT"));
users.put(counter.incrementAndGet(), new User(2L, "Jane Smith", "jane@example.com", "HR"));
users.put(counter.incrementAndGet(), new User(3L, "Bob Johnson", "bob@example.com", "Finance"));
counter.set(3);
}
/**
* Récupère tous les utilisateurs
* @return Liste d'utilisateurs
*/
@GetMapping
public List<User> getAllUsers() {
return new ArrayList<>(users.values());
}
/**
* Récupère un utilisateur par son ID
* @param id ID de l'utilisateur
* @return Utilisateur trouvé
*/
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return users.get(id);
}
/**
* Crée un nouvel utilisateur
* @param user Utilisateur à créer
* @return Utilisateur créé
*/
@PostMapping
public User createUser(@RequestBody User user) {
Long id = counter.incrementAndGet();
user.setId(id);
users.put(id, user);
return user;
}
/**
* Met à jour un utilisateur existant
* @param id ID de l'utilisateur
* @param user Données de mise à jour
* @return Utilisateur mis à jour
*/
@PutMapping("/{id}")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
user.setId(id);
users.put(id, user);
return user;
}
/**
* Supprime un utilisateur
* @param id ID de l'utilisateur à supprimer
*/
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
users.remove(id);
}
/**
* Recherche par département
*/
@GetMapping(params = "department")
public List<User> getUsersByDepartment(@RequestParam String department) {
List<User> result = new ArrayList<>();
for (User user : users.values()) {
if (user.getDepartment().equals(department)) {
result.add(user);
}
}
return result;
}
/**
* Endpoint qui simule une erreur pour tester Resilience4j
*/
@GetMapping("/error")
public User getErrorUser() {
// Simule une erreur 50% du temps
if (Math.random() > 0.5) {
throw new RuntimeException("Erreur simulée pour tester Resilience4j");
}
return new User(999L, "Error Test", "error@test.com", "TEST");
}
/**
* Endpoint qui simule un timeout pour tester Resilience4j
*/
@GetMapping("/slow")
public User getSlowUser() throws InterruptedException {
// Simule un traitement lent
Thread.sleep(3000); // 3 secondes
return new User(888L, "Slow Test", "slow@test.com", "TEST");
}
}
6 Classe principale
Application Spring Boot :
microserviceB/src/main/java/com/example/users/UsersMicroserviceApplication.java
package com.example.users;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* Classe principale du microservice de gestion des utilisateurs
*/
@SpringBootApplication
@EnableDiscoveryClient
public class UsersMicroserviceApplication {
public static void main(String[] args) {
SpringApplication.run(UsersMicroserviceApplication.class, args);
}
}
Microservice A - Client avec Resilience4j
1 Configuration de Base de Resilience4j
Dépendances Maven :
microserviceA/pom.xml
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Eureka Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- RestTemplate -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Resilience4j -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Resilience4j pour Circuit Breaker -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Resilience4j pour Retry -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-retry</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Resilience4j pour Rate Limiter -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-ratelimiter</artifactId>
<version>2.1.0</version>
</dependency>
<!-- Actuator pour monitoring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
Activation des fonctionnalités :
microserviceA/src/main/java/com/example/client/ClientMicroserviceApplication.java
package com.example.client;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* Classe principale du microservice client avec Resilience4j
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // Active OpenFeign
public class ClientMicroserviceApplication {
public static void main(String[] args) {
SpringApplication.run(ClientMicroserviceApplication.class, args);
}
}
2 Modèles de Données
Copies des modèles :
microserviceA/src/main/java/com/example/client/model/User.java
package com.example.client.model;
// Même structure que dans le microservice B
public class User {
private Long id;
private String name;
private String email;
private String department;
// Constructeurs, getters et setters identiques
3 Clients avec RestTemplate et Resilience4j
Configuration de RestTemplate :
microserviceA/src/main/java/com/example/client/config/RestTemplateConfig.java
package com.example.client.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import java.nio.charset.StandardCharsets;
/**
* Configuration de RestTemplate
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// Configuration des convertisseurs de messages
restTemplate.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
return restTemplate;
}
}
5 Configuration de Resilience4j
Configuration application.properties :
microserviceA/src/main/resources/application.properties
# === CONFIGURATION SERVEUR ===
server.port=8080
spring.application.name=client-service
# === CONFIGURATION EUREKA ===
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
eureka.client.register-with-eureka=true
eureka.client.fetch-registry=true
eureka.instance.prefer-ip-address=true
# ==========================================
# === CONFIGURATION RESILIENCE4J ===
# ==========================================
# ------------------------------------------
# Circuit Breaker pour RestTemplate (userService)
# Ce circuit breaker protège les appels HTTP effectués via RestTemplate
# ------------------------------------------
# Active un indicateur de santé exposé par Actuator pour ce circuit breaker.
resilience4j.circuitbreaker.instances.userService.registerHealthIndicator=true
# Taille du buffer utilisé pour stocker les événements (succès, échecs, transitions).
resilience4j.circuitbreaker.instances.userService.eventConsumerBufferSize=10
# Pourcentage d'échecs (exceptions) à partir duquel le circuit passe en état "Open".
# Ici : si 50% des appels échouent => ouverture du circuit.
resilience4j.circuitbreaker.instances.userService.failureRateThreshold=50
# Nombre minimum d’appels nécessaires avant que le seuil de défaillance soit évalué.
# Permet d’éviter des décisions prématurées.
resilience4j.circuitbreaker.instances.userService.minimumNumberOfCalls=5
# Permet au circuit breaker de passer automatiquement de "Open" à "Half-Open"
# après la période définie dans "waitDurationInOpenState".
resilience4j.circuitbreaker.instances.userService.automaticTransitionFromOpenToHalfOpenEnabled=true
# Durée pendant laquelle le circuit reste en état "Open" avant tentative de réouverture (Half-Open).
resilience4j.circuitbreaker.instances.userService.waitDurationInOpenState=5s
# Nombre d’appels autorisés en état "Half-Open" pour tester si le service est redevenu fiable.
resilience4j.circuitbreaker.instances.userService.permittedNumberOfCallsInHalfOpenState=3
# Taille de la fenêtre de mesure du circuit breaker (ici, fenêtre de 10 appels).
resilience4j.circuitbreaker.instances.userService.slidingWindowSize=10
# Type de fenêtre glissante basé sur le **nombre d’appels** (COUNT_BASED)
# contrairement à TIME_BASED (basé sur le temps).
resilience4j.circuitbreaker.instances.userService.slidingWindowType=COUNT_BASED
# ------------------------------------------
# Retry (réessayer automatiquement un appel en cas d’échec)
# ------------------------------------------
# Nombre maximum de tentatives avant de considérer l’appel comme un échec total.
resilience4j.retry.instances.userService.maxAttempts=3
# Temps d’attente entre les tentatives (1 seconde).
resilience4j.retry.instances.userService.waitDuration=1000
# Active l’utilisation d’un backoff exponentiel (temps d’attente qui augmente à chaque tentative).
resilience4j.retry.instances.userService.enableExponentialBackoff=true
# Facteur multiplicateur pour le backoff exponentiel (ex : 1s → 2s → 4s).
resilience4j.retry.instances.userService.exponentialBackoffMultiplier=2
# ------------------------------------------
# Rate Limiter (limitation du nombre d'appels autorisés)
# ------------------------------------------
# Autorise 10 appels dans chaque période de rafraîchissement.
resilience4j.ratelimiter.instances.userService.limitForPeriod=10
# Durée de rafraîchissement de la limite (1 seconde).
resilience4j.ratelimiter.instances.userService.limitRefreshPeriod=1s
# Durée maximum d’attente si la limite est atteinte (0 = ne jamais attendre, retourner immédiatement une erreur).
resilience4j.ratelimiter.instances.userService.timeoutDuration=0
# Permet à l’indicateur de santé d’échouer si le rate limiter est saturé.
resilience4j.ratelimiter.instances.userService.allowHealthIndicatorToFail=true
# Active la souscription aux événements du rate limiter (pratique pour monitoring).
resilience4j.ratelimiter.instances.userService.subscribeForEvents=true
# ------------------------------------------
# Time Limiter (timeout des appels asynchrones)
# ------------------------------------------
# Durée maximum d’exécution avant interruption de l’appel.
resilience4j.timelimiter.instances.userService.timeoutDuration=2s
# Annule la tâche en cours si elle dépasse le timeout.
resilience4j.timelimiter.instances.userService.cancelRunningFuture=true
# ==========================================
# === CONFIGURATION ACTUATOR ===
# ==========================================
# Expose les endpoints Actuator nécessaires au monitoring.
management.endpoints.web.exposure.include=health,info,metrics,circuitbreakerevents
# Affiche les détails complets de l’état de santé (utile pour debugging).
management.endpoint.health.show-details=always
Service utilisant RestTemplate avec Resilience4j :
microserviceA/src/main/java/com/example/client/service/RestTemplateUserService.java
package com.example.client.service;
import com.example.client.model.User;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.ratelimiter.annotation.RateLimiter;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Service utilisant RestTemplate avec Resilience4j
*/
@Service
public class RestTemplateUserService {
@Autowired
private RestTemplate restTemplate;
private static final String BASE_URL = "http://service-b/api/users";
// === CIRCUIT BREAKER ===
/**
* @CircuitBreaker - Active le Circuit Breaker
* name - Nom du circuit breaker (doit correspondre à la configuration)
* fallbackMethod - Méthode de fallback en cas de panne
*/
@CircuitBreaker(name = "userService", fallbackMethod = "getDefaultUser")
public User getUserById(Long id) {
String url = BASE_URL + "/" + id;
return restTemplate.getForObject(url, User.class);
}
/**
* Méthode de fallback pour getUserById
* Doit avoir la même signature que la méthode principale + Throwable
*/
public User getDefaultUser(Long id, Throwable throwable) {
System.err.println("Fallback appelé pour getUserById: " + throwable.getMessage());
return new User(id, "Utilisateur par défaut", "default@example.com", "DEFAULT");
}
// === RETRY ===
/**
* @Retry - Active le mécanisme de retry
* name - Nom du retry (doit correspondre à la configuration)
*/
@Retry(name = "userService")
public List<User> getAllUsers() {
String url = BASE_URL;
User[] usersArray = restTemplate.getForObject(url, User[].class);
return Arrays.asList(usersArray);
}
// === RATE LIMITER ===
/**
* @RateLimiter - Limite le nombre d'appels
* name - Nom du rate limiter (doit correspondre à la configuration)
*/
@RateLimiter(name = "userService")
public User createUser(User user) {
String url = BASE_URL;
return restTemplate.postForObject(url, user, User.class);
}
// === COMBINAISON DE PATTERNS ===
/**
* Combinaison de plusieurs patterns
*/
@CircuitBreaker(name = "userService", fallbackMethod = "getDefaultUsers")
@Retry(name = "userService")
@RateLimiter(name = "userService")
public List<User> getUsersWithAllPatterns() {
String url = BASE_URL;
User[] usersArray = restTemplate.getForObject(url, User[].class);
return Arrays.asList(usersArray);
}
public List<User> getDefaultUsers(Throwable throwable) {
System.err.println("Fallback appelé pour getUsersWithAllPatterns: " + throwable.getMessage());
return Arrays.asList(new User(0L, "Utilisateur par défaut", "default@example.com", "DEFAULT"));
}
// === TIME LIMITER ===
/**
* @TimeLimiter - Limite la durée d'exécution
* name - Nom du time limiter (doit correspondre à la configuration)
*/
@TimeLimiter(name = "userService")
@CircuitBreaker(name = "userService", fallbackMethod = "getDefaultUserAsync")
//CompletableFuture allows the method to run in another thread without blocking the main thread.
/*
--> call service in background
--> return immediately (a promise of future result)
instead of :
--> call service
--> wait...
--> return result
*/
public CompletableFuture<User> getUserByIdAsync(Long id) {
// Run the task asynchronously (non-blocking) in another thread.
return CompletableFuture.supplyAsync(() -> {
String url = BASE_URL + "/" + id;
// This call may fail if the service is down, slow, or unreachable.
return restTemplate.getForObject(url, User.class);
});
}
public CompletableFuture<User> getDefaultUserAsync(Long id, Throwable throwable) {
System.err.println("Fallback asynchrone appelé: " + throwable.getMessage());
return CompletableFuture.completedFuture(
new User(id, "Utilisateur par défaut", "default@example.com", "DEFAULT")
);
}
// === TESTS D'ERREURS ET TIMEOUTS ===
@CircuitBreaker(name = "userService", fallbackMethod = "getDefaultUser")
public User getErrorUser() {
String url = BASE_URL + "/error";
return restTemplate.getForObject(url, User.class);
}
@CircuitBreaker(name = "userService", fallbackMethod = "getDefaultUser")
@TimeLimiter(name = "userService")
public User getSlowUser() {
String url = BASE_URL + "/slow";
return restTemplate.getForObject(url, User.class);
}
}
6 Services Métier
Services utilisant les clients avec Resilience4j :
microserviceA/src/main/java/com/example/client/service/UserService.java
package com.example.client.service;
import com.example.client.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Service métier pour les utilisateurs
*/
@Service
public class UserService {
@Autowired
private RestTemplateUserService restTemplateUserService;
// === MÉTHODES AVEC RESTTEMPLATE ===
public User getUserByIdRest(Long id) {
return restTemplateUserService.getUserById(id);
}
public List<User> getAllUsersRest() {
return restTemplateUserService.getAllUsers();
}
public User createUserRest(User user) {
return restTemplateUserService.createUser(user);
}
public List<User> getUsersWithAllPatternsRest() {
return restTemplateUserService.getUsersWithAllPatterns();
}
public CompletableFuture<User> getUserByIdAsyncRest(Long id) {
return restTemplateUserService.getUserByIdAsync(id);
}
public User getErrorUserRest() {
return restTemplateUserService.getErrorUser();
}
public User getSlowUserRest() {
return restTemplateUserService.getSlowUser();
}
}
7 Contrôleur REST Client
Contrôleur exposant les fonctionnalités :
microserviceA/src/main/java/com/example/client/controller/ClientController.java
package com.example.client.controller;
import com.example.client.model.User;
import com.example.client.model.Product;
import com.example.client.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* Contrôleur REST pour exposer les fonctionnalités Resilience4j
*/
@RestController
@RequestMapping("/api/client")
@CrossOrigin(origins = "*")
public class ClientController {
@Autowired
private UserService userService;
// === ENDPOINTS UTILISATEURS AVEC RESTTEMPLATE ===
@GetMapping("/users/rest/{id}")
public User getUserByIdRest(@PathVariable Long id) {
return userService.getUserByIdRest(id);
}
@GetMapping("/users/rest")
public List<User> getAllUsersRest() {
return userService.getAllUsersRest();
}
@PostMapping("/users/rest")
public User createUserRest(@RequestBody User user) {
return userService.createUserRest(user);
}
@GetMapping("/users/rest/all-patterns")
public List<User> getUsersWithAllPatternsRest() {
return userService.getUsersWithAllPatternsRest();
}
@GetMapping("/users/rest/async/{id}")
public CompletableFuture<User> getUserByIdAsyncRest(@PathVariable Long id) {
return userService.getUserByIdAsyncRest(id);
}
@GetMapping("/users/rest/error")
public User getErrorUserRest() {
return userService.getErrorUserRest();
}
@GetMapping("/users/rest/slow")
public User getSlowUserRest() {
return userService.getSlowUserRest();
}
}
Configuration Avancée de Resilience4j
1 Configuration Programmatique
Configuration via code :
microserviceA/src/main/java/com/example/client/config/Resilience4jConfig.java
package com.example.client.config;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import io.github.resilience4j.ratelimiter.RateLimiter;
import io.github.resilience4j.ratelimiter.RateLimiterConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
/**
* Configuration programmatique de Resilience4j
*/
@Configuration
public class Resilience4jConfig {
@Bean
public CircuitBreaker userServiceCircuitBreaker() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50% d'erreurs pour ouvrir le circuit
.waitDurationInOpenState(Duration.ofSeconds(10)) // 10 secondes en état ouvert
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
.slidingWindowSize(10) // Fenêtre de 10 appels
.minimumNumberOfCalls(5) // Minimum 5 appels avant calcul du taux d'erreur
.permittedNumberOfCallsInHalfOpenState(3) // 3 appels en état semi-ouvert
.automaticTransitionFromOpenToHalfOpenEnabled(true) // Transition automatique
.recordExceptions(Exception.class) // Enregistrer toutes les exceptions
.build();
return CircuitBreaker.of("userServiceProgrammatic", config);
}
@Bean
public Retry userServiceRetry() {
RetryConfig config = RetryConfig.custom()
.maxAttempts(3) // 3 tentatives maximum
.waitDuration(Duration.ofSeconds(1)) // 1 seconde entre chaque tentative
.enableExponentialBackoff(true) // Backoff exponentiel
.exponentialBackoffMultiplier(2.0) // Multiplié par 2 à chaque tentative
.retryExceptions(Exception.class) // Réessayer pour toutes les exceptions
.build();
return Retry.of("userServiceProgrammatic", config);
}
@Bean
public RateLimiter userServiceRateLimiter() {
RateLimiterConfig config = RateLimiterConfig.custom()
.limitForPeriod(10) // 10 appels par période
.limitRefreshPeriod(Duration.ofSeconds(1)) // Période de 1 seconde
.timeoutDuration(Duration.ofMillis(500)) // Timeout de 500ms
.build();
return RateLimiter.of("userServiceProgrammatic", config);
}
}
2 Utilisation Programmatique
Utilisation des composants configurés :
microserviceA/src/main/java/com/example/client/service/ProgrammaticResilienceService.java
package com.example.client.service;
import com.example.client.model.User;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.ratelimiter.RateLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.function.Supplier;
/**
* Service utilisant Resilience4j de manière programmatique
*/
@Service
public class ProgrammaticResilienceService {
@Autowired
private RestTemplate restTemplate;
@Autowired
private CircuitBreaker userServiceCircuitBreaker;
@Autowired
private Retry userServiceRetry;
@Autowired
private RateLimiter userServiceRateLimiter;
private static final String BASE_URL = "http://service-b/api/users";
public User getUserById(Long id) {
// Création du supplier pour l'appel HTTP
Supplier<User> userSupplier = () -> {
String url = BASE_URL + "/" + id;
return restTemplate.getForObject(url, User.class);
};
// Application des patterns de résilience
Supplier<User> decoratedSupplier = RateLimiter
.decorateSupplier(userServiceRateLimiter, userSupplier);
decoratedSupplier = CircuitBreaker
.decorateSupplier(userServiceCircuitBreaker, decoratedSupplier);
decoratedSupplier = Retry
.decorateSupplier(userServiceRetry, decoratedSupplier);
// Exécution avec fallback
return decoratedSupplier.get();
}
public User getUserByIdWithErrorHandling(Long id) {
Supplier<User> userSupplier = () -> {
String url = BASE_URL + "/" + id;
return restTemplate.getForObject(url, User.class);
};
// Décoration avec tous les patterns
Supplier<User> decoratedSupplier = RateLimiter
.decorateSupplier(userServiceRateLimiter, userSupplier);
decoratedSupplier = CircuitBreaker
.decorateSupplier(userServiceCircuitBreaker, decoratedSupplier);
decoratedSupplier = Retry
.decorateSupplier(userServiceRetry, decoratedSupplier);
// Ajout d'un fallback personnalisé
Supplier<User> fallbackSupplier = () ->
new User(id, "Utilisateur par défaut", "default@example.com", "DEFAULT");
return CircuitBreaker
.decorateSupplier(userServiceCircuitBreaker, decoratedSupplier)
.recover(fallbackSupplier)
.get();
}
}
Monitoring et Métriques
1 Actuator Endpoints
Endpoints de monitoring :
microserviceA/src/main/java/com/example/client/config/ActuatorConfig.java
package com.example.client.config;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.web.*;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Configuration des endpoints Actuator pour Resilience4j
*/
@Configuration
public class ActuatorConfig {
@Bean
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier,
ServletEndpointsSupplier servletEndpointsSupplier,
ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes,
CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties,
Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.get();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.get());
allEndpoints.addAll(controllerEndpointsSupplier.get());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(
webEndpointProperties, environment, basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
shouldRegisterLinksMapping, null);
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties,
Environment environment, String basePath) {
return webEndpointProperties.getDiscovery().isEnabled() &&
(StringUtils.hasText(basePath) ||
ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}
}
Endpoints Actuator utiles :
- /actuator/health : Santé de l'application et des circuits
- /actuator/metrics : Métriques détaillées
- /actuator/circuitbreakerevents : Événements des circuit breakers
- /actuator/retryevents : Événements des retries
- /actuator/ratelimiterevents : Événements des rate limiters
2 Logging et Events
Gestion des événements :
microserviceA/src/main/java/com/example/client/listener/CircuitBreakerEventListener.java
package com.example.client.listener;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.event.CircuitBreakerEvent;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Listener pour les événements de Circuit Breaker
*/
@Configuration
public class CircuitBreakerEventListener {
@Bean
public CircuitBreaker.EventPublisher circuitBreakerEventPublisher() {
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("userService");
circuitBreaker.getEventPublisher()
.onStateTransition(event ->
System.out.println("Circuit Breaker state changed: " + event.getStateTransition())
)
.onCallNotPermitted(event ->
System.out.println("Call not permitted: " + event.getCircuitBreakerName())
)
.onError(event ->
System.err.println("Circuit Breaker error: " + event.getThrowable().getMessage())
)
.onSuccess(event ->
System.out.println("Circuit Breaker success: " + event.getCircuitBreakerName())
);
return circuitBreaker.getEventPublisher();
}
}
Tableau Récapitulatif des Patterns
| Pattern | Annotation | Utilisation | Configuration Principale |
|---|---|---|---|
| Circuit Breaker | @CircuitBreaker | Empêche les appels vers services défaillants | failureRateThreshold, waitDurationInOpenState |
| Retry | @Retry | Réessaie les opérations échouées | maxAttempts, waitDuration |
| Rate Limiter | @RateLimiter | Limite le nombre d'appels | limitForPeriod, limitRefreshPeriod |
| Time Limiter | @TimeLimiter | Limite la durée d'exécution | timeoutDuration |
| Bulkhead | @Bulkhead | Isole les ressources | maxConcurrentCalls |
Bonnes Pratiques et Conseils
1 Configuration Optimale
Configuration recommandée :
# Circuit Breaker - Configuration équilibrée
resilience4j.circuitbreaker.instances.default.failureRateThreshold=50
resilience4j.circuitbreaker.instances.default.waitDurationInOpenState=30s
resilience4j.circuitbreaker.instances.default.permittedNumberOfCallsInHalfOpenState=10
resilience4j.circuitbreaker.instances.default.slidingWindowSize=100
resilience4j.circuitbreaker.instances.default.minimumNumberOfCalls=10
# Retry - Backoff exponentiel
resilience4j.retry.instances.default.maxAttempts=3
resilience4j.retry.instances.default.waitDuration=1s
resilience4j.retry.instances.default.enableExponentialBackoff=true
resilience4j.retry.instances.default.exponentialBackoffMultiplier=2
# Rate Limiter - Configuration raisonnable
resilience4j.ratelimiter.instances.default.limitForPeriod=50
resilience4j.ratelimiter.instances.default.limitRefreshPeriod=10s
Conclusion
Points Clés à Retenir
graph TB
A[Resilience4j] --> B[Patterns de Base]
A --> C[Intégration]
A --> D[Configuration]
A --> E[Monitoring]
B --> B1[Circuit Breaker]
B --> B2[Retry]
B --> B3[Rate Limiter]
B --> B4[Time Limiter]
C --> C1[RestTemplate]
C --> C2[OpenFeign]
C --> C3[Annotations]
C --> C4[Fallbacks]
D --> D1[Properties]
D --> D2[Programmatique]
D --> D3[Événements]
E --> E1[Actuator]
E --> E2[Métriques]
E --> E3[Alertes]
style A fill:#4CAF50,stroke:#388E3C
style B fill:#2196F3,stroke:#0D47A1
style C fill:#FF9800,stroke:#E65100
style D fill:#9C27B0,stroke:#4A148C
style E fill:#FF5722,stroke:#E64A19
Résumé des concepts importants :
- Resilience4j fournit des patterns de tolérance aux pannes légers et modulaires
- Utilisez @CircuitBreaker pour protéger contre les services défaillants
- @Retry permet de réessayer automatiquement les opérations échouées
- @RateLimiter limite le nombre d'appels pour éviter la surcharge
- Les fallbacks fournissent des réponses alternatives en cas de panne
- Resilience4j s'intègre parfaitement avec RestTemplate et OpenFeign
- Le monitoring via Actuator permet de suivre l'état des circuits