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

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

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