Tutoriel Spring OpenFeign - Communication Déclarative

Spring OpenFeign

Communication déclarative entre microservices avec OpenFeign

Introduction à OpenFeign

Qu'est-ce que OpenFeign ?

OpenFeign est un client HTTP déclaratif qui simplifie la communication entre microservices. Il permet de définir des clients HTTP en utilisant des annotations, sans écrire de code d'implémentation.

graph LR A[Microservice A
Client Feign] --> B[@FeignClient] B --> C[Service B
Users API] B --> D[Service C
Products 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

Avantages de OpenFeign :

  • Déclaratif : Définition des clients avec des annotations
  • Intégration Spring : Fonctionne parfaitement avec Spring Cloud
  • Sérialisation automatique : Conversion JSON ↔ Objets Java automatique
  • Gestion d'erreurs : Gestion intégrée des exceptions HTTP
  • Load Balancing : Intégration native avec Ribbon/Eureka
  • Retry Mechanism : Support natif du retry

Architecture des Microservices avec OpenFeign

Structure de l'application

graph TD A[Eureka Server
Port: 8761] --> B[Microservice A
Client Feign - Port: 8080] A --> C[Microservice B
Users - Port: 8081] A --> D[Microservice C
Products - Port: 8082] B -- @FeignClient --> C B -- @FeignClient --> 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

Rôles des microservices :

  • Microservice A : Client qui utilise OpenFeign pour communiquer
  • Microservice B : Service de gestion des utilisateurs
  • Microservice C : Service de gestion des produits
  • Eureka Server : Serveur de découverte de services

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;
    }
    
    /**
     * Recherche par nom (contient)
     */
    @GetMapping(params = "name")
    public List<User> getUsersByNameContains(@RequestParam String name) {
        List<User> result = new ArrayList<>();
        for (User user : users.values()) {
            if (user.getName().toLowerCase().contains(name.toLowerCase())) {
                result.add(user);
            }
        }
        return result;
    }
}

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 C - Service Produits (Simple)

1 Structure du projet

Arborescence du projet :

microserviceC/
├── pom.xml
└── src/main/
    ├── java/com/example/products/
    │   ├── ProductsMicroserviceApplication.java
    │   ├── controller/
    │   │   └── ProductController.java
    │   ├── model/
    │   │   └── Product.java
    └── resources/
        └── application.properties

2 Fichier pom.xml

Dépendances Maven :

microserviceC/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 :

microserviceC/src/main/resources/application.properties
# === CONFIGURATION SERVEUR ===
server.port=8082
spring.application.name=service-c

# === 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 Product

Modèle Product :

microserviceC/src/main/java/com/example/products/model/Product.java
package com.example.products.model;

/**
 * Modèle représentant un produit
 */
public class Product {
    private Long id;
    private String name;
    private String description;
    private Double price;
    private String category;
    
    // Constructeurs
 
    // Getters et Setters
   

5 Contrôleur REST

Contrôleur REST :

microserviceC/src/main/java/com/example/products/controller/ProductController.java
package com.example.products.controller;

import com.example.products.model.Product;
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 produits
 */
@RestController
@RequestMapping("/api/products")
@CrossOrigin(origins = "*")
public class ProductController {
    
    // Stockage en mémoire pour la démonstration
    private final ConcurrentHashMap<Long, Product> products = new ConcurrentHashMap<>();
    private final AtomicLong counter = new AtomicLong();
    
    // Initialisation avec quelques produits
    public ProductController() {
        products.put(counter.incrementAndGet(), new Product(1L, "Laptop", "High-performance laptop", 999.99, "Electronics"));
        products.put(counter.incrementAndGet(), new Product(2L, "Smartphone", "Latest smartphone model", 699.99, "Electronics"));
        products.put(counter.incrementAndGet(), new Product(3L, "Coffee Mug", "Ceramic coffee mug", 12.99, "Kitchen"));
        counter.set(3);
    }
    
    /**
     * Récupère tous les produits
     * @return Liste de produits
     */
    @GetMapping
    public List<Product> getAllProducts() {
        return new ArrayList<>(products.values());
    }
    
    /**
     * Récupère un produit par son ID
     * @param id ID du produit
     * @return Produit trouvé
     */
    @GetMapping("/{id}")
    public Product getProductById(@PathVariable Long id) {
        return products.get(id);
    }
    
    /**
     * Crée un nouveau produit
     * @param product Produit à créer
     * @return Produit créé
     */
    @PostMapping
    public Product createProduct(@RequestBody Product product) {
        Long id = counter.incrementAndGet();
        product.setId(id);
        products.put(id, product);
        return product;
    }
    
    /**
     * Met à jour un produit existant
     * @param id ID du produit
     * @param product Données de mise à jour
     * @return Produit mis à jour
     */
    @PutMapping("/{id}")
    public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
        product.setId(id);
        products.put(id, product);
        return product;
    }
    
    /**
     * Supprime un produit
     * @param id ID du produit à supprimer
     */
    @DeleteMapping("/{id}")
    public void deleteProduct(@PathVariable Long id) {
        products.remove(id);
    }
    
    /**
     * Recherche par catégorie
     */
    @GetMapping(params = "category")
    public List<Product> getProductsByCategory(@RequestParam String category) {
        List<Product> result = new ArrayList<>();
        for (Product product : products.values()) {
            if (product.getCategory().equals(category)) {
                result.add(product);
            }
        }
        return result;
    }
    
    /**
     * Recherche par prix maximum
     */
    @GetMapping(params = "maxPrice")
    public List<Product> getProductsByMaxPrice(@RequestParam Double maxPrice) {
        List<Product> result = new ArrayList<>();
        for (Product product : products.values()) {
            if (product.getPrice() <= maxPrice) {
                result.add(product);
            }
        }
        return result;
    }
    
    /**
     * Recherche par nom (contient)
     */
    @GetMapping(params = "name")
    public List<Product> getProductsByNameContains(@RequestParam String name) {
        List<Product> result = new ArrayList<>();
        for (Product product : products.values()) {
            if (product.getName().toLowerCase().contains(name.toLowerCase())) {
                result.add(product);
            }
        }
        return result;
    }
}

6 Classe principale

Application Spring Boot :

microserviceC/src/main/java/com/example/products/ProductsMicroserviceApplication.java
package com.example.products;

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 produits
 */
@SpringBootApplication
@EnableDiscoveryClient
public class ProductsMicroserviceApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ProductsMicroserviceApplication.class, args);
    }
}

Microservice A - Client OpenFeign

1 Configuration de Base d'OpenFeign

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>
    
    <!-- OpenFeign -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
</dependencies>

Activation d'OpenFeign :

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 OpenFeign
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients  // Active OpenFeign dans l'application
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
 
    
    // Getters et Setters
  
microserviceA/src/main/java/com/example/client/model/Product.java
package com.example.client.model;

// Même structure que dans le microservice C
public class Product {
    private Long id;
    private String name;
    private String description;
    private Double price;
    private String category;
    
    // Constructeurs, getters et setters identiques
   
   

3 Clients Feign - Fonctionnalités de Base

Client Feign pour le service utilisateurs :

microserviceA/src/main/java/com/example/client/feign/UserServiceClient.java
package com.example.client.feign;

import com.example.client.model.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * Client Feign pour communiquer avec le service utilisateurs
 * 
 * @FeignClient - Annotation principale pour définir un client Feign
 * name - Nom du service (doit correspondre à spring.application.name du service cible)
 * url - Optionnel, utilisé pour le développement local sans Eureka
 */
@FeignClient(
    name = "service-b",  // Nom du service cible
    url = "http://localhost:8081"  // Optionnel - pour développement sans Eureka
)
public interface UserServiceClient {
    
    // === MÉTHODES GET - Récupération de données ===
    
    /**
     * @GetMapping - Définit une requête HTTP GET
     * value - Chemin de l'endpoint
     */
    @GetMapping("/api/users")
    List<User> getAllUsers();
    
    /**
     * @PathVariable - Variable dans le chemin de l'URL
     * {id} dans l'URL correspond au paramètre id
     */
    @GetMapping("/api/users/{id}")
    User getUserById(@PathVariable("id") Long id);
    
    /**
     * @RequestParam - Paramètre de requête
     * ?department=IT
     */
    @GetMapping("/api/users")
    List<User> getUsersByDepartment(@RequestParam("department") String department);
    
    /**
     * Recherche avec paramètre optionnel
     */
    @GetMapping("/api/users")
    List<User> getUsersByNameContains(@RequestParam(value = "name", required = false) String name);
    
    // === MÉTHODES POST - Création de ressources ===
    
    /**
     * @PostMapping - Définit une requête HTTP POST
     * @RequestBody - Corps de la requête
     */
    @PostMapping("/api/users")
    User createUser(@RequestBody User user);
    
    /**
     * Retourne ResponseEntity pour accéder aux headers et status code
     */
    @PostMapping("/api/users")
    ResponseEntity<User> createUserWithResponse(@RequestBody User user);
    
    // === MÉTHODES PUT - Mise à jour de ressources ===
    
    /**
     * @PutMapping - Définit une requête HTTP PUT
     */
    @PutMapping("/api/users/{id}")
    User updateUser(@PathVariable("id") Long id, @RequestBody User user);
    
    // === MÉTHODES DELETE - Suppression de ressources ===
    
    /**
     * @DeleteMapping - Définit une requête HTTP DELETE
     */
    @DeleteMapping("/api/users/{id}")
    void deleteUser(@PathVariable("id") Long id);
}
microserviceA/src/main/java/com/example/client/feign/ProductServiceClient.java
package com.example.client.feign;

import com.example.client.model.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * Client Feign pour communiquer avec le service produits
 */
@FeignClient(
    name = "service-c",
    url = "http://localhost:8082"
)
public interface ProductServiceClient {
    
    // === MÉTHODES GET - Récupération de données ===
    
    @GetMapping("/api/products")
    List<Product> getAllProducts();
    
    @GetMapping("/api/products/{id}")
    Product getProductById(@PathVariable("id") Long id);
    
    @GetMapping("/api/products")
    List<Product> getProductsByCategory(@RequestParam("category") String category);
    
    @GetMapping("/api/products")
    List<Product> getProductsByMaxPrice(@RequestParam("maxPrice") Double maxPrice);
    
    @GetMapping("/api/products")
    List<Product> getProductsByNameContains(@RequestParam(value = "name", required = false) String name);
    
    // === MÉTHODES POST - Création de ressources ===
    
    @PostMapping("/api/products")
    Product createProduct(@RequestBody Product product);
    
    // === MÉTHODES PUT - Mise à jour de ressources ===
    
    @PutMapping("/api/products/{id}")
    Product updateProduct(@PathVariable("id") Long id, @RequestBody Product product);
    
    // === MÉTHODES DELETE - Suppression de ressources ===
    
    @DeleteMapping("/api/products/{id}")
    void deleteProduct(@PathVariable("id") Long id);
}

Explication des annotations de base :

@FeignClient : Annotation principale pour définir un client Feign. Le paramètre name doit correspondre au spring.application.name du service cible. Le paramètre url est optionnel et utilisé pour le développement local sans Eureka.

@GetMapping : Définit une requête HTTP GET. La valeur correspond au chemin de l'endpoint.

@PostMapping : Définit une requête HTTP POST. Utilisé pour créer des ressources.

@PutMapping : Définit une requête HTTP PUT. Utilisé pour mettre à jour des ressources.

@DeleteMapping : Définit une requête HTTP DELETE. Utilisé pour supprimer des ressources.

@PathVariable : Lie une variable dans le chemin de l'URL à un paramètre de méthode.

@RequestParam : Lie un paramètre de requête à un paramètre de méthode.

@RequestBody : Indique que le paramètre doit être utilisé comme corps de la requête.

4 Clients Feign - Fonctionnalités Avancées

Client Feign avec fonctionnalités avancées :

microserviceA/src/main/java/com/example/client/feign/AdvancedUserServiceClient.java
package com.example.client.feign;

import com.example.client.model.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * Client Feign avancé avec fonctionnalités supplémentaires
 */
@FeignClient(
    name = "service-b",
    url = "http://localhost:8081",
    configuration = FeignConfig.class  // Configuration personnalisée
)
public interface AdvancedUserServiceClient {
    
    // === HEADERS PERSONNALISÉS ===
    
    /**
     * Headers statiques définis avec @RequestMapping
     */
    @RequestMapping(
        method = RequestMethod.GET,
        value = "/api/users",
        headers = "X-API-Version=1.0"
    )
    List<User> getAllUsersWithVersionHeader();
    
    /**
     * Headers dynamiques passés en paramètre
     */
    @GetMapping("/api/users")
    List<User> getAllUsersWithCustomHeader(@RequestHeader("Authorization") String token);
    
    /**
     * Plusieurs headers dynamiques
     */
    @PostMapping("/api/users")
    User createUserWithHeaders(
        @RequestBody User user,
        @RequestHeader("X-API-Key") String apiKey,
        @RequestHeader("User-Agent") String userAgent
    );
    
    // === GESTION DES ERREURS ===
    
    /**
     * Méthode qui peut retourner différents types de réponse
     */
    @GetMapping("/api/users/{id}")
    ResponseEntity<User> getUserByIdWithResponse(@PathVariable("id") Long id);
    
    // === REQUÊTES COMPLEXES ===
    
    /**
     * Requête avec plusieurs paramètres
     */
    @GetMapping("/api/users/search")
    List<User> searchUsers(
        @RequestParam(value = "name", required = false) String name,
        @RequestParam(value = "department", required = false) String department,
        @RequestParam(value = "page", defaultValue = "0") int page,
        @RequestParam(value = "size", defaultValue = "10") int size
    );
    
    /**
     * Requête avec tableau de paramètres
     */
    @GetMapping("/api/users")
    List<User> getUsersByDepartments(@RequestParam("department") List<String> departments);
    
    // === MÉTHODES HTTP PERSONNALISÉES ===
    
    /**
     * Méthode PATCH (moins courante mais supportée)
     */
    @RequestMapping(method = RequestMethod.PATCH, value = "/api/users/{id}")
    User updateUserPartially(@PathVariable("id") Long id, @RequestBody User user);
    
    /**
     * Méthode HEAD
     */
    @RequestMapping(method = RequestMethod.HEAD, value = "/api/users")
    void checkUsersEndpoint();
}
microserviceA/src/main/java/com/example/client/config/FeignConfig.java
package com.example.client.config;

import feign.Logger;
import feign.Request;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Configuration personnalisée pour OpenFeign
 */
@Configuration
public class FeignConfig {
    
    /**
     * Configuration du niveau de logging
     */
    @Bean
    public Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL; // FULL, BASIC, HEADERS, NONE
    }
    
    /**
     * Configuration des timeouts
     */
    @Bean
    public Request.Options options() {
        return new Request.Options(
            5000,  // connect timeout (5 secondes)
            10000  // read timeout (10 secondes)
        );
    }
    
    /**
     * Configuration du retry mechanism
     */
    @Bean
    public Retryer retryer() {
        return new Retryer.Default(
            100,   // période initiale (ms)
            1000,  // période maximale (ms)
            3      // nombre maximum de tentatives
        );
    }
    
    /**
     * Encoder personnalisé (optionnel)
     */
    // @Bean
    // public Encoder customEncoder() {
    //     return new CustomEncoder();
    // }
    
    /**
     * Decoder personnalisé (optionnel)
     */
    // @Bean
    // public Decoder customDecoder() {
    //     return new CustomDecoder();
    // }
}

5 Gestion des Erreurs avec ErrorDecoder

Gestion personnalisée des erreurs :

microserviceA/src/main/java/com/example/client/exception/UserNotFoundException.java
package com.example.client.exception;

/**
 * Exception personnalisée pour utilisateur non trouvé
 */
public class UserNotFoundException extends RuntimeException {
    private final Long userId;
    
    public UserNotFoundException(Long userId) {
        super("Utilisateur avec ID " + userId + " non trouvé");
        this.userId = userId;
    }
    
    public Long getUserId() {
        return userId;
    }
}
microserviceA/src/main/java/com/example/client/exception/ServiceUnavailableException.java
package com.example.client.exception;

/**
 * Exception pour service indisponible
 */
public class ServiceUnavailableException extends RuntimeException {
    public ServiceUnavailableException(String message) {
        super(message);
    }
}
microserviceA/src/main/java/com/example/client/feign/CustomErrorDecoder.java
package com.example.client.feign;

import com.example.client.exception.UserNotFoundException;
import com.example.client.exception.ServiceUnavailableException;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.springframework.stereotype.Component;

/**
 * Décodeur d'erreurs personnalisé pour OpenFeign
 */
@Component
public class CustomErrorDecoder implements ErrorDecoder {
    
    @Override
    public Exception decode(String methodKey, Response response) {
        switch (response.status()) {
            case 404:
                // Erreur 404 - Not Found
                if (methodKey.contains("getUserById")) {
                    // Extraction de l'ID de l'utilisateur depuis la requête
                    String[] parts = methodKey.split("\\?");
                    if (parts.length > 0) {
                        String path = parts[0];
                        String[] pathParts = path.split("/");
                        if (pathParts.length > 0) {
                            try {
                                Long userId = Long.parseLong(pathParts[pathParts.length - 1]);
                                return new UserNotFoundException(userId);
                            } catch (NumberFormatException e) {
                                // Ignorer et retourner l'exception par défaut
                            }
                        }
                    }
                }
                return new UserNotFoundException(null);
                
            case 503:
                // Erreur 503 - Service Unavailable
                return new ServiceUnavailableException("Service temporairement indisponible");
                
            case 500:
                // Erreur 500 - Internal Server Error
                return new ServiceUnavailableException("Erreur interne du service");
                
            default:
                // Pour les autres erreurs, utiliser le décodeur par défaut
                return new ErrorDecoder.Default().decode(methodKey, response);
        }
    }
}
microserviceA/src/main/java/com/example/client/config/FeignErrorConfig.java
package com.example.client.config;

import com.example.client.feign.CustomErrorDecoder;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Configuration de l'ErrorDecoder personnalisé
 */
@Configuration
public class FeignErrorConfig {
    
    @Bean
    public ErrorDecoder errorDecoder() {
        return new CustomErrorDecoder();
    }
}

6 Intercepteurs et Logging

Intercepteurs pour le logging et l'authentification :

microserviceA/src/main/java/com/example/client/feign/FeignRequestInterceptor.java
package com.example.client.feign;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

/**
 * Intercepteur pour ajouter des headers à toutes les requêtes Feign
 */
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
    
    @Override
    public void apply(RequestTemplate template) {
        // Ajout d'headers communs à toutes les requêtes
        template.header("User-Agent", "MyApplication/1.0");
        template.header("Accept", "application/json");
        template.header("Content-Type", "application/json");
        
        // Ajout d'un token d'authentification (exemple)
        // template.header("Authorization", "Bearer " + getAuthToken());
        
        // Logging des requêtes (optionnel)
        System.out.println("Feign Request: " + template.method() + " " + template.url());
    }
    
    // Méthode pour obtenir un token d'authentification
    private String getAuthToken() {
        // Implémentation de récupération de token
        return "dummy-token";
    }
}
microserviceA/src/main/java/com/example/client/feign/LoggingRequestInterceptor.java
package com.example.client.feign;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

/**
 * Intercepteur de logging (activé uniquement en développement)
 */
@Component
@Profile("development")
public class LoggingRequestInterceptor implements RequestInterceptor {
    
    @Override
    public void apply(RequestTemplate template) {
        System.out.println("=== FEIGN REQUEST ===");
        System.out.println("Method: " + template.method());
        System.out.println("URL: " + template.url());
        System.out.println("Headers: " + template.headers());
        System.out.println("Body: " + template.body());
    }
}

7 Service Utilisant les Clients Feign

Service métier utilisant les clients Feign :

microserviceA/src/main/java/com/example/client/service/UserService.java
package com.example.client.service;

import com.example.client.feign.UserServiceClient;
import com.example.client.feign.AdvancedUserServiceClient;
import com.example.client.model.User;
import com.example.client.exception.UserNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * Service métier utilisant les clients Feign
 */
@Service
public class UserService {
    
    @Autowired
    private UserServiceClient userServiceClient;
    
    @Autowired
    private AdvancedUserServiceClient advancedUserServiceClient;
    
    // === MÉTHODES DE BASE ===
    
    public List<User> getAllUsers() {
        return userServiceClient.getAllUsers();
    }
    
    public User getUserById(Long id) {
        try {
            return userServiceClient.getUserById(id);
        } catch (UserNotFoundException e) {
            throw e; // Relance l'exception personnalisée
        } catch (Exception e) {
            throw new RuntimeException("Erreur lors de la récupération de l'utilisateur", e);
        }
    }
    
    public User createUser(User user) {
        return userServiceClient.createUser(user);
    }
    
    public User updateUser(Long id, User user) {
        return userServiceClient.updateUser(id, user);
    }
    
    public void deleteUser(Long id) {
        userServiceClient.deleteUser(id);
    }
    
    // === MÉTHODES AVANCÉES ===
    
    public List<User> getUsersByDepartment(String department) {
        return userServiceClient.getUsersByDepartment(department);
    }
    
    public List<User> searchUsers(String name, String department) {
        return advancedUserServiceClient.searchUsers(name, department, 0, 10);
    }
    
    public User createUserWithApiKey(User user, String apiKey) {
        return advancedUserServiceClient.createUserWithHeaders(
            user, 
            apiKey, 
            "MyApplication/1.0"
        );
    }
    
    public ResponseEntity<User> getUserWithResponse(Long id) {
        return advancedUserServiceClient.getUserByIdWithResponse(id);
    }
}
microserviceA/src/main/java/com/example/client/service/ProductService.java
package com.example.client.service;

import com.example.client.feign.ProductServiceClient;
import com.example.client.model.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * Service métier pour les produits utilisant Feign
 */
@Service
public class ProductService {
    
    @Autowired
    private ProductServiceClient productServiceClient;
    
    public List<Product> getAllProducts() {
        return productServiceClient.getAllProducts();
    }
    
    public Product getProductById(Long id) {
        return productServiceClient.getProductById(id);
    }
    
    public Product createProduct(Product product) {
        return productServiceClient.createProduct(product);
    }
    
    public Product updateProduct(Long id, Product product) {
        return productServiceClient.updateProduct(id, product);
    }
    
    public void deleteProduct(Long id) {
        productServiceClient.deleteProduct(id);
    }
    
    public List<Product> getProductsByCategory(String category) {
        return productServiceClient.getProductsByCategory(category);
    }
    
    public List<Product> getProductsByMaxPrice(Double maxPrice) {
        return productServiceClient.getProductsByMaxPrice(maxPrice);
    }
}

8 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 com.example.client.service.ProductService;
import com.example.client.exception.UserNotFoundException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * Contrôleur REST pour exposer les fonctionnalités du client OpenFeign
 */
@RestController
@RequestMapping("/api/client")
@CrossOrigin(origins = "*")
public class ClientController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private ProductService productService;
    
    // === ENDPOINTS UTILISATEURS ===
    
    @GetMapping("/users")
    public List<User> getAllUsers() {
        return userService.getAllUsers();
    }
    
    @GetMapping("/users/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.getUserById(id);
    }
    
    @PostMapping("/users")
    public User createUser(@RequestBody User user) {
        return userService.createUser(user);
    }
    
    @PutMapping("/users/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User user) {
        return userService.updateUser(id, user);
    }
    
    @DeleteMapping("/users/{id}")
    public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return ResponseEntity.ok().build();
    }
    
    @GetMapping("/users/department/{department}")
    public List<User> getUsersByDepartment(@PathVariable String department) {
        return userService.getUsersByDepartment(department);
    }
    
    @GetMapping("/users/search")
    public List<User> searchUsers(
            @RequestParam(required = false) String name,
            @RequestParam(required = false) String department) {
        return userService.searchUsers(name, department);
    }
    
    // === ENDPOINTS PRODUITS ===
    
    @GetMapping("/products")
    public List<Product> getAllProducts() {
        return productService.getAllProducts();
    }
    
    @GetMapping("/products/{id}")
    public Product getProductById(@PathVariable Long id) {
        return productService.getProductById(id);
    }
    
    @PostMapping("/products")
    public Product createProduct(@RequestBody Product product) {
        return productService.createProduct(product);
    }
    
    @PutMapping("/products/{id}")
    public Product updateProduct(@PathVariable Long id, @RequestBody Product product) {
        return productService.updateProduct(id, product);
    }
    
    @DeleteMapping("/products/{id}")
    public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
        productService.deleteProduct(id);
        return ResponseEntity.ok().build();
    }
    
    @GetMapping("/products/category/{category}")
    public List<Product> getProductsByCategory(@PathVariable String category) {
        return productService.getProductsByCategory(category);
    }
    
    @GetMapping("/products/max-price/{maxPrice}")
    public List<Product> getProductsByMaxPrice(@PathVariable Double maxPrice) {
        return productService.getProductsByMaxPrice(maxPrice);
    }
    
    // === GESTION DES ERREURS ===
    
    @GetMapping("/users/{id}/safe")
    public ResponseEntity<?> getUserByIdSafe(@PathVariable Long id) {
        try {
            User user = userService.getUserById(id);
            return ResponseEntity.ok(user);
        } catch (UserNotFoundException e) {
            return ResponseEntity.notFound().build();
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Erreur interne: " + e.getMessage());
        }
    }
}

Configuration Avancée d'OpenFeign

1 Configuration Globale

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 OPENFEIGN ===
# Activation d'OpenFeign
feign.client.config.default.connectTimeout=5000
feign.client.config.default.readTimeout=10000
feign.client.config.default.loggerLevel=basic
feign.client.config.default.errorDecoder=com.example.client.feign.CustomErrorDecoder
feign.client.config.default.requestInterceptors=com.example.client.feign.FeignRequestInterceptor

# Logging
logging.level.com.example.client.feign=DEBUG

# Retry configuration
feign.client.config.default.retryer=feign.Retryer.Default

# Compression
feign.compression.request.enabled=true
feign.compression.response.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

# Actuator endpoints
management.endpoints.web.exposure.include=health,info,metrics,httptrace

2 Configuration par Client

Configuration spécifique à un client :

microserviceA/src/main/resources/application.properties (suite)
# Configuration spécifique au client service-b
feign.client.config.service-b.connectTimeout=3000
feign.client.config.service-b.readTimeout=8000
feign.client.config.service-b.loggerLevel=full
feign.client.config.service-b.requestInterceptors=com.example.client.feign.LoggingRequestInterceptor

# Configuration spécifique au client service-c
feign.client.config.service-c.connectTimeout=4000
feign.client.config.service-c.readTimeout=12000
feign.client.config.service-c.loggerLevel=headers

Tableau Récapitulatif des Fonctionnalités

Fonctionnalité Annotation OpenFeign Utilisation Avantages
Définition du client @FeignClient Création d'un client HTTP déclaratif Simplicité, intégration Spring
Requêtes GET @GetMapping Récupération de données Mapping intuitif, paramètres supportés
Requêtes POST @PostMapping Création de ressources Sérialisation automatique
Requêtes PUT @PutMapping Mise à jour de ressources Mapping REST naturel
Requêtes DELETE @DeleteMapping Suppression de ressources Simplicité d'utilisation
Variables de chemin @PathVariable Paramètres dans l'URL Sécurité, flexibilité
Paramètres de requête @RequestParam Paramètres dans l'URL Facilité de filtrage
Corps de requête @RequestBody Données dans le corps Sérialisation automatique
Headers personnalisés @RequestHeader Headers HTTP personnalisés Authentification, versioning
Gestion d'erreurs ErrorDecoder Mapping erreurs HTTP → exceptions Contrôle total sur les erreurs

Bonnes Pratiques et Conseils

1 Organisation du Code

Structure recommandée :

src/main/java/com/example/client/
├── ClientMicroserviceApplication.java
├── config/                    // Configurations
│   ├── FeignConfig.java
│   └── FeignErrorConfig.java
├── feign/                     // Clients Feign
│   ├── UserServiceClient.java
│   ├── ProductServiceClient.java
│   └── AdvancedUserServiceClient.java
├── model/                     // Modèles de données
│   ├── User.java
│   └── Product.java
├── service/                   // Services métier
│   ├── UserService.java
│   └── ProductService.java
├── controller/                // Contrôleurs REST
│   └── ClientController.java
└── exception/                 // Exceptions personnalisées
    ├── UserNotFoundException.java
    └── ServiceUnavailableException.java

2 Gestion des Erreurs Robuste

Gestion des erreurs complète :

@FeignClient(name = "service-b", configuration = FeignErrorConfig.class)
public interface UserServiceClient {
    
    @GetMapping("/api/users/{id}")
    User getUserById(@PathVariable("id") Long id);
    
    @PostMapping("/api/users")
    User createUser(@RequestBody User user);
}

@Service
public class UserService {
    
    @Autowired
    private UserServiceClient userServiceClient;
    
    public User getUserById(Long id) {
        try {
            return userServiceClient.getUserById(id);
        } catch (UserNotFoundException e) {
            // Gestion spécifique de l'erreur 404
            log.warn("Utilisateur {} non trouvé", id);
            throw e;
        } catch (ServiceUnavailableException e) {
            // Gestion des erreurs 5xx
            log.error("Service utilisateur indisponible", e);
            throw new RuntimeException("Service temporairement indisponible", e);
        } catch (Exception e) {
            // Gestion des autres erreurs
            log.error("Erreur inattendue lors de la récupération de l'utilisateur {}", id, e);
            throw new RuntimeException("Erreur de communication avec le service utilisateur", e);
        }
    }
}

3 Sécurité et Authentification

Authentification avec intercepteurs :

@Component
public class AuthRequestInterceptor implements RequestInterceptor {
    
    @Autowired
    private AuthService authService;
    
    @Override
    public void apply(RequestTemplate template) {
        // Ajout du token d'authentification
        String token = authService.getValidToken();
        template.header("Authorization", "Bearer " + token);
        
        // Ajout d'autres headers de sécurité
        template.header("X-Request-ID", UUID.randomUUID().toString());
        template.header("X-Client-Version", "1.0.0");
    }
}

@Configuration
public class FeignSecurityConfig {
    
    @Bean
    public RequestInterceptor authRequestInterceptor() {
        return new AuthRequestInterceptor();
    }
}

4 Retry Mechanism

Configuration du retry personnalisé :

@Configuration
public class FeignRetryConfig {
    
    @Bean
    public Retryer retryer() {
        return new Retryer.Default(
            1000,  // 1 seconde d'attente initiale
            3000,  // 3 secondes d'attente maximale
            3      // 3 tentatives maximum
        );
    }
}

// Ou retry personnalisé
public class CustomRetryer implements Retryer {
    private final int maxAttempts;
    private int attempt = 1;
    
    public CustomRetryer(int maxAttempts) {
        this.maxAttempts = maxAttempts;
    }
    
    @Override
    public void continueOrPropagate(RetryableException e) {
        if (attempt++ >= maxAttempts) {
            throw e;
        }
        
        try {
            Thread.sleep(1000 * attempt); // Attente progressive
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
            throw e;
        }
    }
    
    @Override
    public Retryer clone() {
        return new CustomRetryer(maxAttempts);
    }
}

Migration de RestTemplate vers OpenFeign

1 Comparaison des Approches

Avec RestTemplate (Impératif) :

@Service
public class UserService {
    @Autowired
    private RestTemplate restTemplate;
    
    public User getUserById(Long id) {
        String url = "http://service-b/api/users/" + id;
        return restTemplate.getForObject(url, User.class);
    }
    
    public User createUser(User user) {
        String url = "http://service-b/api/users";
        return restTemplate.postForObject(url, user, User.class);
    }
}

Avec OpenFeign (Déclaratif) :

@FeignClient(name = "service-b")
public interface UserServiceClient {
    @GetMapping("/api/users/{id}")
    User getUserById(@PathVariable("id") Long id);
    
    @PostMapping("/api/users")
    User createUser(@RequestBody User user);
}

@Service
public class UserService {
    @Autowired
    private UserServiceClient userServiceClient;
    
    public User getUserById(Long id) {
        return userServiceClient.getUserById(id);
    }
    
    public User createUser(User user) {
        return userServiceClient.createUser(user);
    }
}

Avantages de la migration :

  • Lisibilité : Code plus clair et expressif
  • Maintenance : Moins de code boilerplate
  • Type Safety : Vérification à la compilation
  • Intégration : Meilleure intégration avec Spring Cloud
  • Fonctionnalités : Retry, load balancing natifs

Conclusion

Points Clés à Retenir

graph TB A[OpenFeign] --> B[Fonctionnalités de Base] A --> C[Fonctionnalités Avancées] A --> D[Bonnes Pratiques] B --> B1[@FeignClient] B --> B2[@GetMapping/@PostMapping] B --> B3[@PathVariable/@RequestParam] C --> C1[Headers Personnalisés] C --> C2[Gestion d'Erreurs] C --> C3[Retry Mechanism] C --> C4[Intercepteurs] D --> D1[Organisation du Code] D --> D2[Sécurité] D --> D3[Configuration] D --> D4[Monitoring] style A fill:#4CAF50,stroke:#388E3C style B fill:#2196F3,stroke:#0D47A1 style C fill:#FF9800,stroke:#E65100 style D fill:#9C27B0,stroke:#4A148C

Résumé des concepts importants :

  • OpenFeign est un client HTTP déclaratif qui simplifie la communication entre microservices
  • Utilisez @FeignClient pour définir des clients HTTP avec des annotations
  • @GetMapping/@PostMapping permettent de mapper les méthodes HTTP de manière intuitive
  • Les paramètres sont liés avec @PathVariable, @RequestParam et @RequestBody
  • Implémentez une gestion d'erreurs personnalisée avec ErrorDecoder
  • Utilisez des intercepteurs pour ajouter des headers communs ou du logging
  • Configurez des timeouts et retry pour une meilleure résilience

⚠️ Points d'attention :

  • Charge : Chaque client Feign génère du code proxy à l'exécution
  • Dépendances : Nécessite Spring Cloud et des services de découverte
  • Debugging : Peut être plus complexe à déboguer que RestTemplate

🚀 Recommandations futures :

  • WebClient : Pour les applications réactives (Spring WebFlux)
  • Resilience4j : Pour ajouter du circuit breaker
  • Spring Cloud LoadBalancer : Pour le load balancing client-side
  • Spring Cloud Sleuth : Pour le tracing distribué