Aller au contenu principal

TP13.4: Communication avec Feign et Resilience4j

🎯 Objectifs du TP​

  • ImplĂ©menter des appels inter-services avec OpenFeign
  • Configurer Circuit Breaker avec Resilience4j
  • ImplĂ©menter des fallbacks efficaces
  • Ajouter des retries avec backoff exponentiel
  • Monitorer l'Ă©tat des circuits

Durée estimée: 2h


📋 Architecture​


Partie 1: CrĂ©er Order Service (30 min)​

1.1 CrĂ©er le Projet​

pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>

<groupId>com.example</groupId>
<artifactId>order-service</artifactId>
<version>1.0.0</version>

<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>

<dependencies>
<!-- Spring Boot Web -->
<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>

<!-- Eureka Client -->
<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>

<!-- Resilience4j annotations -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
</dependency>

<!-- AOP pour annotations -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

1.2 Application Principale​

package com.example.orderservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {

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

1.3 Configuration​

# src/main/resources/application.yml
server:
port: 8084

spring:
application:
name: order-service
cloud:
openfeign:
circuitbreaker:
enabled: true
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
loggerLevel: FULL

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/

# Resilience4j Configuration
resilience4j:
circuitbreaker:
configs:
default:
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 10000
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
recordExceptions:
- java.io.IOException
- java.util.concurrent.TimeoutException
- feign.FeignException
instances:
userService:
baseConfig: default
productService:
baseConfig: default
failureRateThreshold: 30

retry:
configs:
default:
maxAttempts: 3
waitDuration: 1000
enableExponentialBackoff: true
exponentialBackoffMultiplier: 2
retryExceptions:
- java.io.IOException
- feign.RetryableException
instances:
userService:
baseConfig: default
productService:
baseConfig: default
maxAttempts: 5

timelimiter:
configs:
default:
timeoutDuration: 3s
instances:
userService:
baseConfig: default
productService:
baseConfig: default

# Actuator
management:
endpoints:
web:
exposure:
include: health,info,circuitbreakers,circuitbreakerevents
health:
circuitbreakers:
enabled: true
endpoint:
health:
show-details: always

# Logging
logging:
level:
com.example.orderservice.client: DEBUG
io.github.resilience4j: DEBUG

Partie 2: Feign Clients avec Fallbacks (30 min)​

2.1 DTOs​

package com.example.orderservice.dto;

public class UserDto {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;

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; }

// Pour identifier les fallbacks
public boolean isFallback() {
return "FALLBACK".equals(username);
}
}
package com.example.orderservice.dto;

public class ProductDto {
private Long id;
private String name;
private Double price;
private Long ownerId;

public ProductDto() {}

public ProductDto(Long id, String name, Double price, Long ownerId) {
this.id = id;
this.name = name;
this.price = price;
this.ownerId = ownerId;
}

// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getName() { return name; }
public void setName(String name) { this.name = name; }

public Double getPrice() { return price; }
public void setPrice(Double price) { this.price = price; }

public Long getOwnerId() { return ownerId; }
public void setOwnerId(Long ownerId) { this.ownerId = ownerId; }

public boolean isFallback() {
return "FALLBACK".equals(name);
}
}

2.2 User Client avec FallbackFactory​

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",
fallbackFactory = UserClientFallbackFactory.class
)
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);
}
package com.example.orderservice.client;

import com.example.orderservice.dto.UserDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

@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.getClass().getSimpleName(),
cause.getMessage()
);

return new UserClient() {
@Override
public UserDto getUserById(Long id) {
log.warn("Returning fallback user for id: {}", id);
return new UserDto(
id,
"FALLBACK",
"fallback@example.com",
"Service",
"Unavailable"
);
}

@Override
public List<UserDto> getAllUsers() {
log.warn("Returning empty list for getAllUsers");
return Collections.emptyList();
}

@Override
public Boolean userExists(Long id) {
log.warn("Returning false for userExists: {}", id);
return false;
}
};
}
}

2.3 Product Client avec FallbackFactory​

package com.example.orderservice.client;

import com.example.orderservice.dto.ProductDto;
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 = "product-service",
fallbackFactory = ProductClientFallbackFactory.class
)
public interface ProductClient {

@GetMapping("/api/products/{id}")
ProductDto getProductById(@PathVariable("id") Long id);

@GetMapping("/api/products")
List<ProductDto> getAllProducts();
}
package com.example.orderservice.client;

import com.example.orderservice.dto.ProductDto;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.List;

@Component
public class ProductClientFallbackFactory implements FallbackFactory<ProductClient> {

private static final Logger log = LoggerFactory.getLogger(ProductClientFallbackFactory.class);

@Override
public ProductClient create(Throwable cause) {
log.error("ProductClient fallback triggered. Cause: {} - {}",
cause.getClass().getSimpleName(),
cause.getMessage()
);

return new ProductClient() {
@Override
public ProductDto getProductById(Long id) {
log.warn("Returning fallback product for id: {}", id);
return new ProductDto(id, "FALLBACK", 0.0, 0L);
}

@Override
public List<ProductDto> getAllProducts() {
log.warn("Returning empty list for getAllProducts");
return Collections.emptyList();
}
};
}
}

Partie 3: Order Service Logic (20 min)​

3.1 Order Model​

package com.example.orderservice.model;

import java.time.LocalDateTime;

public class Order {
private Long id;
private Long userId;
private Long productId;
private Integer quantity;
private Double totalPrice;
private String status;
private LocalDateTime createdAt;
private String userEmail;
private String productName;
private boolean userFallback;
private boolean productFallback;

public Order() {
this.createdAt = LocalDateTime.now();
this.status = "PENDING";
}

// Getters et Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }

public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }

public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }

public Double getTotalPrice() { return totalPrice; }
public void setTotalPrice(Double totalPrice) { this.totalPrice = totalPrice; }

public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }

public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }

public String getUserEmail() { return userEmail; }
public void setUserEmail(String userEmail) { this.userEmail = userEmail; }

public String getProductName() { return productName; }
public void setProductName(String productName) { this.productName = productName; }

public boolean isUserFallback() { return userFallback; }
public void setUserFallback(boolean userFallback) { this.userFallback = userFallback; }

public boolean isProductFallback() { return productFallback; }
public void setProductFallback(boolean productFallback) { this.productFallback = productFallback; }
}

3.2 Order Request DTO​

package com.example.orderservice.dto;

public class OrderRequest {
private Long userId;
private Long productId;
private Integer quantity;

// Getters et Setters
public Long getUserId() { return userId; }
public void setUserId(Long userId) { this.userId = userId; }

public Long getProductId() { return productId; }
public void setProductId(Long productId) { this.productId = productId; }

public Integer getQuantity() { return quantity; }
public void setQuantity(Integer quantity) { this.quantity = quantity; }
}

3.3 Order Service​

package com.example.orderservice.service;

import com.example.orderservice.client.ProductClient;
import com.example.orderservice.client.UserClient;
import com.example.orderservice.dto.OrderRequest;
import com.example.orderservice.dto.ProductDto;
import com.example.orderservice.dto.UserDto;
import com.example.orderservice.model.Order;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

@Service
public class OrderService {

private static final Logger log = LoggerFactory.getLogger(OrderService.class);

private final UserClient userClient;
private final ProductClient productClient;
private final Map<Long, Order> orders = new HashMap<>();
private final AtomicLong idGenerator = new AtomicLong(1);

public OrderService(UserClient userClient, ProductClient productClient) {
this.userClient = userClient;
this.productClient = productClient;
}

public Order createOrder(OrderRequest request) {
log.info("Creating order for user: {}, product: {}",
request.getUserId(), request.getProductId());

// Appel Ă  User Service
UserDto user = userClient.getUserById(request.getUserId());
log.info("User response: {} (fallback: {})", user.getUsername(), user.isFallback());

// Appel Ă  Product Service
ProductDto product = productClient.getProductById(request.getProductId());
log.info("Product response: {} (fallback: {})", product.getName(), product.isFallback());

// Créer la commande
Order order = new Order();
order.setId(idGenerator.getAndIncrement());
order.setUserId(request.getUserId());
order.setProductId(request.getProductId());
order.setQuantity(request.getQuantity());
order.setUserEmail(user.getEmail());
order.setProductName(product.getName());
order.setUserFallback(user.isFallback());
order.setProductFallback(product.isFallback());

// Calculer le prix (0 si fallback)
if (product.isFallback()) {
order.setTotalPrice(0.0);
order.setStatus("PENDING_PRODUCT_VERIFICATION");
} else {
order.setTotalPrice(product.getPrice() * request.getQuantity());
order.setStatus("CONFIRMED");
}

orders.put(order.getId(), order);
log.info("Order created: {} with status: {}", order.getId(), order.getStatus());

return order;
}

public List<Order> getAllOrders() {
return new ArrayList<>(orders.values());
}

public Optional<Order> getOrderById(Long id) {
return Optional.ofNullable(orders.get(id));
}
}

3.4 Order Controller​

package com.example.orderservice.controller;

import com.example.orderservice.dto.OrderRequest;
import com.example.orderservice.model.Order;
import com.example.orderservice.service.OrderService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/orders")
public class OrderController {

private final OrderService orderService;

public OrderController(OrderService orderService) {
this.orderService = orderService;
}

@PostMapping
public ResponseEntity<Order> createOrder(@RequestBody OrderRequest request) {
Order order = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED).body(order);
}

@GetMapping
public ResponseEntity<List<Order>> getAllOrders() {
return ResponseEntity.ok(orderService.getAllOrders());
}

@GetMapping("/{id}")
public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
return orderService.getOrderById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}

Partie 4: Circuit Breaker Status Endpoint (10 min)​

4.1 Circuit Breaker Controller​

package com.example.orderservice.controller;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/api/circuit-breaker")
public class CircuitBreakerController {

private final CircuitBreakerRegistry circuitBreakerRegistry;

public CircuitBreakerController(CircuitBreakerRegistry circuitBreakerRegistry) {
this.circuitBreakerRegistry = circuitBreakerRegistry;
}

@GetMapping("/status")
public ResponseEntity<Map<String, Object>> getStatus() {
Map<String, Object> status = new HashMap<>();

circuitBreakerRegistry.getAllCircuitBreakers().forEach(cb -> {
Map<String, Object> cbStatus = new HashMap<>();
cbStatus.put("state", cb.getState().name());
cbStatus.put("failureRate", cb.getMetrics().getFailureRate() + "%");
cbStatus.put("slowCallRate", cb.getMetrics().getSlowCallRate() + "%");
cbStatus.put("bufferedCalls", cb.getMetrics().getNumberOfBufferedCalls());
cbStatus.put("failedCalls", cb.getMetrics().getNumberOfFailedCalls());
cbStatus.put("successfulCalls", cb.getMetrics().getNumberOfSuccessfulCalls());
cbStatus.put("notPermittedCalls", cb.getMetrics().getNumberOfNotPermittedCalls());

status.put(cb.getName(), cbStatus);
});

return ResponseEntity.ok(status);
}
}

Partie 5: Tests (30 min)​

5.1 Ordre de DĂ©marrage​

  1. Eureka Server (8761)
  2. User Service (8081)
  3. Product Service (8083)
  4. API Gateway (8080) - si configuré
  5. Order Service (8084)

5.2 Test: CrĂ©ation de Commande (Tout OK)​

# Créer une commande
curl -X POST http://localhost:8084/api/orders \
-H "Content-Type: application/json" \
-d '{"userId": 1, "productId": 1, "quantity": 2}'

Résultat attendu:

{
"id": 1,
"userId": 1,
"productId": 1,
"quantity": 2,
"totalPrice": 1999.98,
"status": "CONFIRMED",
"userEmail": "john@example.com",
"productName": "Laptop",
"userFallback": false,
"productFallback": false
}

5.3 Test: Circuit Breaker (User Service Down)​

# 1. ArrĂȘter User Service

# 2. Créer une commande
curl -X POST http://localhost:8084/api/orders \
-H "Content-Type: application/json" \
-d '{"userId": 1, "productId": 1, "quantity": 2}'

Résultat attendu (avec fallback):

{
"id": 2,
"userId": 1,
"productId": 1,
"quantity": 2,
"totalPrice": 1999.98,
"status": "CONFIRMED",
"userEmail": "fallback@example.com",
"productName": "Laptop",
"userFallback": true,
"productFallback": false
}

5.4 Test: VĂ©rifier l'État du Circuit​

curl http://localhost:8084/api/circuit-breaker/status

Résultat:

{
"feign-user-service": {
"state": "OPEN",
"failureRate": "100.0%",
"bufferedCalls": 5,
"failedCalls": 5,
"successfulCalls": 0
},
"feign-product-service": {
"state": "CLOSED",
"failureRate": "0.0%",
"bufferedCalls": 3,
"failedCalls": 0,
"successfulCalls": 3
}
}

5.5 Test: Actuator Endpoints​

# Circuit breakers status
curl http://localhost:8084/actuator/circuitbreakers

# Circuit breaker events
curl http://localhost:8084/actuator/circuitbreakerevents

# Health avec détails
curl http://localhost:8084/actuator/health

5.6 Script de Test de Charge​

#!/bin/bash
# test-resilience.sh

echo "=== Testing Order Service Resilience ==="

for i in {1..20}; do
echo "Request $i:"
curl -s -X POST http://localhost:8084/api/orders \
-H "Content-Type: application/json" \
-d "{\"userId\": 1, \"productId\": 1, \"quantity\": $i}" | jq '.status, .userFallback'
echo ""
sleep 0.5
done

echo "=== Circuit Breaker Status ==="
curl -s http://localhost:8084/api/circuit-breaker/status | jq .

✅ Checklist de Validation​

  • Order Service dĂ©marre et s'enregistre dans Eureka
  • CrĂ©ation de commande fonctionne (tous services up)
  • Fallback User Client activĂ© quand User Service down
  • Fallback Product Client activĂ© quand Product Service down
  • Circuit passe en OPEN aprĂšs plusieurs Ă©checs
  • Circuit passe en HALF_OPEN aprĂšs le dĂ©lai d'attente
  • /api/circuit-breaker/status affiche l'Ă©tat des circuits
  • /actuator/circuitbreakers fonctionne

🎯 Exercices Bonus​

Exercice 1: Ajouter un Cache Fallback​

Stockez les derniÚres réponses en cache et utilisez-les comme fallback:

private final Map<Long, UserDto> userCache = new ConcurrentHashMap<>();

public UserDto getUserById(Long id) {
try {
UserDto user = userClient.getUserById(id);
userCache.put(id, user); // Mise en cache
return user;
} catch (Exception e) {
return userCache.getOrDefault(id, createFallbackUser(id));
}
}

Exercice 2: Retry avec Annotations​

Ajoutez @Retry sur une méthode de service:

@Retry(name = "userService", fallbackMethod = "getUserRetryFallback")
@CircuitBreaker(name = "userService", fallbackMethod = "getUserCircuitFallback")
public UserDto getUser(Long id) {
return userClient.getUserById(id);
}

Exercice 3: Bulkhead​

Limitez le nombre d'appels concurrents:

resilience4j:
bulkhead:
instances:
userService:
maxConcurrentCalls: 5
maxWaitDuration: 500ms

📊 RĂ©sumĂ© du Module 13​

Félicitations! Vous avez maintenant une architecture microservices complÚte:

ComposantPortRĂŽle
Eureka Server8761Service Discovery
Config Server8888Configuration centralisée
API Gateway8080Point d'entrée unique
User Service8081Gestion utilisateurs
Product Service8083Catalogue produits
Order Service8084Gestion commandes

Patterns implémentés:

  • ✅ Service Discovery (Eureka)
  • ✅ Centralized Configuration (Config Server)
  • ✅ API Gateway (Spring Cloud Gateway)
  • ✅ Circuit Breaker (Resilience4j)
  • ✅ Retry Pattern
  • ✅ Fallback Pattern
  • ✅ Load Balancing (LoadBalancer)

Retour au Module 13

Retour Ă  l'accueil