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.
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
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 :
<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 :
# === 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 :
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 :
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 :
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 :
<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 :
# === 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 :
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 :
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 :
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 :
<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 :
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 :
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
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 :
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);
}
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 :
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();
}
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 :
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;
}
}
package com.example.client.exception;
/**
* Exception pour service indisponible
*/
public class ServiceUnavailableException extends RuntimeException {
public ServiceUnavailableException(String message) {
super(message);
}
}
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);
}
}
}
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 :
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";
}
}
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 :
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);
}
}
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 :
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 :
# === 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 :
# 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
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é