Sommaire
- Introduction 📚
- Qu'est-ce que HttpExchange ? 🤔
- Qu'est-ce que HttpServiceProxyFactory ? 🏭
- Mise en place d'un projet Spring 🛠️
- Créer un client HTTP déclaratif 📝
- Cas d'usage avancés 🔬
- Gestion des erreurs et exceptions 🚨
- Tests unitaires et d'intégration 🧪
- Bonnes pratiques, FAQ et limitations 💡
- Aller plus loin / Pour experts 🏅
- Exemple de configuration avancée
- Utilisation avec WebFlux (réactif)
- Résilience avec Resilience4j
- Tableau récapitulatif des annotations principales
- Différences avec Feign, Retrofit, RestTemplate
- Migration rapide RestTemplate -> HttpExchange
- Checklist avant mise en production
- Astuces de debug
- Ressources complémentaires
- Conclusion 🎯
- Glossaire express (vulgarisé)
Introduction 📚
Cet article s'adresse aux développeurs Java/Spring de niveau intermédiaire qui souhaitent consommer des API REST externes de façon moderne et typée. Avec Spring 6 et Spring Boot 3, l'interface HttpExchange propose une nouvelle façon, simple et déclarative, de déclarer des clients HTTP, à la manière de Feign ou Retrofit, mais 100% Spring. Ce guide vous explique pas à pas comment l'utiliser efficacement, avec des exemples concrets et des conseils pratiques. ✨
À la fin de cet article, vous saurez comment intégrer, tester et fiabiliser vos appels HTTP dans vos applications Spring modernes. 💪
Version vulgarisée en 20 secondes
HttpExchange= vous écrivez le "contrat" (interface Java), Spring fait le "travail technique".HttpServiceProxyFactory= la "machine" qui transforme ce contrat en vrai client HTTP utilisable.- Résultat : moins de code technique, plus de code métier, moins d'erreurs.
1. Qu'est-ce que HttpExchange ? 🤔
HttpExchange permet de définir des interfaces Java annotées qui décrivent les appels HTTP à effectuer. Spring se charge de générer l'implémentation à l'exécution. Cela permet :
- D'écrire du code client HTTP lisible, typé et testable ✅
- De s'intégrer nativement à l'écosystème Spring (sécurité, configuration, etc.) 🌱
- De réduire le code « boilerplate » (infrastructure) 🧹
Image mentale (vulgarisation)
Imaginez une télécommande :
- les boutons = les méthodes de votre interface (
getUserById,createUser, etc.) - la télécommande ne contient pas la TV, mais sait quoi déclencher
- Spring joue le rôle de l'électronique interne qui envoie le bon signal HTTP
Vous appuyez sur un bouton (méthode Java), Spring envoie l'appel HTTP réel.
Schéma de choix rapide (débutant) :
flowchart TD
A["Je dois appeler une API HTTP"] --> B{"Je suis en mode reactif ?"}
B -->|"Non"| C["RestClient + HttpExchange"]
B -->|"Oui"| D["WebClient + HttpExchange"]
C --> E["Code simple, synchrone"]
D --> F["Flux reactifs Mono/Flux"]
Schéma du fonctionnement général:
flowchart LR A["Interface annotée (UserClient)"] -->|"Décrit les endpoints"| B["HttpExchange"] B -->|"Analyse à l'exécution"| C["Spring"] C -->|"Génère"| D["Implémentation dynamique du client"] D -->|"Appels HTTP typés"| E["API distante"]
Ce schéma illustre comment une interface annotée avec HttpExchange est analysée par Spring pour générer dynamiquement un client HTTP typé.
Comparaison rapide:
| Approche | Déclaratif | Typé | Intégration Spring | Modernité |
|---|---|---|---|---|
| RestTemplate | Non | Oui | Oui | Moyen |
| WebClient | Non | Oui | Oui | Oui |
| Feign | Oui | Oui | Partiel (lib) | Oui |
| HttpExchange | Oui | Oui | Oui | Oui |
Pourquoi la déclarativité ?
Déclarer ses appels HTTP sous forme d'interfaces permet d'écrire moins de code, d'améliorer la lisibilité, de faciliter les tests et de réduire les erreurs liées au mapping manuel. 🧑💻
Qu'est-ce que HttpServiceProxyFactory ? 🏭
HttpServiceProxyFactory est une classe utilitaire de Spring qui permet de générer dynamiquement une implémentation d'une interface Java annotée avec @HttpExchange, @GetExchange, etc.
Concrètement, elle prend une interface qui décrit des appels HTTP (par exemple UserClient) et crée automatiquement, à l'exécution, un "proxy" qui réalise les requêtes HTTP correspondantes.
Fonctionnement:
- Vous déclarez une interface Java annotée pour décrire vos endpoints HTTP.
- Vous configurez un client HTTP (RestClient ou WebClient) avec l'URL de base, les timeouts, etc.
- Vous utilisez
HttpServiceProxyFactorypour lier ce client à votre interface. - Spring génère alors un bean prêt à l'emploi, typé, testable et intégré à l'écosystème Spring.
Exemple:
// caption: Création d'un bean UserClient
@Bean
UserClient userClient(RestClient.Builder builder) {
RestClient restClient = builder.baseUrl("https://jsonplaceholder.typicode.com").build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build();
return factory.createClient(UserClient.class);
}
Résumé:
HttpServiceProxyFactory automatise la création de clients HTTP typés à partir d'interfaces Java, sans avoir à écrire le code d'appel HTTP manuellement. Cela favorise la lisibilité, la testabilité et la maintenance du code. 🤖
Schéma du rôle de HttpServiceProxyFactory:
flowchart LR A["RestClient/WebClient configuré"] --> B["HttpServiceProxyFactory"] B -->|"createClient"| C["Proxy dynamique de l'interface"] C -->|"Utilisation dans Spring"| D["Service métier"]
Ce schéma montre comment la factory relie un client HTTP configuré à une interface pour produire un proxy prêt à l'emploi dans Spring.
2. Mise en place d'un projet Spring avec HttpExchange 🛠️
Prérequis
- Spring Boot 3.x ou Spring Framework 6.x
- Dépendance Maven:
// caption: Ajout du starter web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Pour WebFlux:
// caption: Ajout du starter web flux
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Exemple Gradle:
// caption: Ajout du starter web/webflux
implementation 'org.springframework.boot:spring-boot-starter-web'
// Pour WebFlux :
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Exemple de configuration de timeout dans application.yml:
// caption: Configuration de l'api
clients:
user-api:
base-url: https://jsonplaceholder.typicode.com
connect-timeout: 3s
read-timeout: 5s
Vous pouvez ensuite lire ces propriétés via @ConfigurationProperties et les appliquer dans la construction du RestClient.
3. Créer un client HTTP déclaratif 📝
Supposons que vous souhaitez consommer l'API publique JSONPlaceholder pour manipuler des utilisateurs.
a) Déclaration de l'interface
// caption: Déclaration des endpoints
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
// L'URL de base sera définie dans la configuration du bean (voir plus bas)
// Vous pouvez aussi versionner l'API ici : @HttpExchange("/v1/users")
@HttpExchange("/users")
public interface UserClient {
@GetExchange
List<User> getAllUsers();
@GetExchange("/{id}")
User getUserById(@PathVariable Long id);
@PostExchange
User createUser(@RequestBody User user);
}
b) Exemple de DTO (Data Transfer Object)
// caption: Création du model User
// Le DTO doit correspondre à la structure JSON de l'API cible
public record User(Long id, String name, String username, String email) {
}
c) Configuration du bean client
// caption: Configuration du Bean Client
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
@Bean
UserClient userClient(RestClient.Builder builder) {
// Définir ici l'URL de base de l'API
RestClient restClient = builder.baseUrl("https://jsonplaceholder.typicode.com").build();
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build();
return factory.createClient(UserClient.class);
}
// Pensez à externaliser l'URL de base dans vos fichiers de configuration pour plus de flexibilité
d) Utilisation dans un service Spring
// caption: Utilisation du client dans le service
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserClient userClient;
public UserService(UserClient userClient) {
this.userClient = userClient;
}
public List<User> fetchAllUsers() {
return userClient.getAllUsers();
}
public User fetchUser(Long id) {
return userClient.getUserById(id);
}
}
Cycle de vie d’un appel client :
sequenceDiagram participant Service as Service Spring participant Client as Interface annotée (UserClient) participant Proxy as Proxy dynamique participant API as API distante Service->>Client: Appel méthode (ex: getAllUsers) Client->>Proxy: Délégation automatique Proxy->>API: Requête HTTP (GET /users) API-->>Proxy: Réponse JSON Proxy-->>Client: Mapping vers DTO (User) Client-->>Service: Retourne le résultat typé
Ce diagramme séquentiel illustre le cheminement d’un appel HTTP depuis le service Spring jusqu’à la réponse typée.
e) Mini scénario concret (ultra simple)
Cas concret: "Je dois afficher la fiche d'un utilisateur dans mon app".
- Le contrôleur appelle
userService.fetchUser(42). UserServiceappelleuserClient.getUserById(42).- Le proxy Spring envoie
GET /users/42. - La réponse JSON est convertie en
User. - Votre code récupère un objet Java prêt à l'emploi.
Pourquoi c'est plus simple ?
- Vous raisonnez en Java métier (
User) et non en infrastructure technique HTTP. - Le mapping JSON -> objet est centralisé.
- Les tests sont plus faciles à écrire.
4. Cas d'usage avancés 🔬
a) Appel POST avec payload
// caption: Appel de l'endpoint avec un payload
User newUser = new User(null, "Jean Dupont", "jdupont", "jean@exemple.com");
User created = userClient.createUser(newUser);
b) Passage de headers personnalisés
// caption: Ajout du header dans l'appel Client
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
// La clé API peut venir d'une propriété de configuration (ex: @Value)
@GetExchange("/{id}")
User getUserWithApiKey(@PathVariable Long id, @RequestHeader("X-API-KEY") String apiKey);
c) Paramètres de requête
// caption: Paramètrage des appels (mettre en query params)
import org.springframework.web.bind.annotation.RequestParam;
@GetExchange()
List<User> searchUsers(@RequestParam String name);
// Exemple d'appel : userClient.searchUsers("Lea");
// Générera une requête GET /users?name=Lea
5. Gestion des erreurs et exceptions 🚨
Spring lève automatiquement des exceptions pour les codes HTTP d’erreur (4xx, 5xx), selon le client HTTP sous-jacent (RestClient, WebClient). Vous pouvez les intercepter:
// caption: Gestion des erreurs
import org.springframework.web.client.HttpClientErrorException;
public User getUserSafe(Long id) {
try {
return userClient.getUserById(id);
} catch (HttpClientErrorException.NotFound e) {
// Gérer l'absence d'utilisateur
throw new UserNotFoundException("Utilisateur non trouvé");
}
}
// Exemple d'exception personnalisée
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
Pour une gestion globale, utilisez un @ControllerAdvice ou personnalisez la gestion dans vos services.
Exemple minimaliste:
// caption: Jeter une erreur de type ProblemDetail (RFC 9457)
import java.net.URI;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(UserNotFoundException.class)
public ProblemDetail handleUserNotFound(UserNotFoundException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
problem.setTitle("Utilisateur non trouvé");
problem.setType(URI.create("https://example.com/errors/user-not-found"));
problem.setDetail(ex.getMessage());
return problem;
}
}
6. Tests unitaires et d'intégration 🧪
a) Test unitaire avec MockWebServer
Pour simuler des réponses HTTP sans dépendre d'un serveur externe, utilisez MockWebServer:
// caption: Test Unitaire du Client
import java.util.List;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.Test;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import static org.assertj.core.api.Assertions.assertThat;
@Test
void testGetAllUsers_withMockWebServer() throws Exception {
try (MockWebServer server = new MockWebServer()) {
server.enqueue(new MockResponse().setBody("[{\"id\":1,\"name\":\"Lea\"}]").setResponseCode(200));
server.start();
RestClient restClient = RestClient.builder()
.baseUrl(server.url("/").toString())
.build();
UserClient userClient = HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build()
.createClient(UserClient.class);
List<User> users = userClient.getAllUsers();
assertThat(users).isNotEmpty();
}
}
b) Test d’intégration
Pour tester l'intégration réelle du client HTTP, privilégiez @SpringBootTest avec un serveur de test comme
WireMock:
// caption: Test Intégration du Client
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.assertFalse;
@SpringBootTest
class UserClientIntegrationTest {
@Autowired
UserClient userClient;
@Test
void testGetAllUsers() {
List<User> users = userClient.getAllUsers();
assertFalse(users.isEmpty());
}
}
7. Bonnes pratiques, FAQ et limitations 💡
- Utilisez des DTOs pour le typage fort
- Gérez les erreurs explicitement (ne masquez pas les exceptions)
- Privilégiez l’injection via constructeur pour la testabilité
- Externalisez l'URL de base et les timeouts dans la configuration (
application.ymlouapplication.properties) - Activez les logs HTTP pour faciliter le debug (ex: niveau DEBUG sur org.springframework.web)
- Attention: pas de support pour les WebSockets ou le streaming avancé
FAQ et limitations courantes
1. Peut-on utiliser HttpExchange avec WebFlux (réactif) ?
Oui, il suffit de configurer un WebClient au lieu d’un RestClient dans la factory. Les méthodes de l’interface peuvent alors retourner des types réactifs (Mono<T>, Flux<T>).
2. Comment gérer l’authentification ?
Configurez le RestClient ou WebClient avec un filtre ou un interceptor pour ajouter les headers d’authentification (Bearer, Basic, etc.).
3. Peut-on personnaliser la sérialisation JSON ?
Oui, en passant un RestClient/WebClient configuré avec un ObjectMapper personnalisé.
4. Quelles sont les limitations ?
- Pas de support WebSocket/streaming avancé.
- Pas de gestion automatique du circuit breaker/retry (utilisez Resilience4j ou Spring Retry).
- Les interfaces doivent être publiques et annotées correctement.
5. Où trouver la documentation officielle ?
Aller plus loin / Pour experts 🏅
Exemple de configuration avancée
// caption: Configuration avancée
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
// Exemple: token récupéré depuis une propriété ou un provider dédié.
@Bean
UserClient userClient(RestClient.Builder builder) {
String token = "<token>";
RestClient restClient = builder
.baseUrl("https://api.exemple.com")
.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + token)
.requestInterceptor((request, body, execution) -> {
// Log ou enrichissement personnalisé
return execution.execute(request, body);
})
.build();
return HttpServiceProxyFactory
.builderFor(RestClientAdapter.create(restClient))
.build()
.createClient(UserClient.class);
}
Utilisation avec WebFlux (réactif)
// caption: Utilisation du client en Réactive
import java.util.List;
import reactor.core.publisher.Mono;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
@HttpExchange("/users")
public interface ReactiveUserClient {
@GetExchange
Mono<List<User>> getAllUsers();
}
Résilience avec Resilience4j
Intégrez un décorateur autour du client pour ajouter du retry/circuit breaker:
Tableau récapitulatif des annotations principales
| Annotation | Méthode HTTP | Usage principal |
|---|---|---|
| @GetExchange | GET | Lecture de ressources |
| @PostExchange | POST | Création de ressources |
| @PutExchange | PUT | Mise à jour de ressources |
| @DeleteExchange | DELETE | Suppression de ressources |
| @PatchExchange | PATCH | Modification partielle |
Différences avec Feign, Retrofit, RestTemplate
- Feign: nécessite une dépendance externe, moins intégré à Spring Boot 3+.
- Retrofit: orienté Android/Java, non natif Spring.
- RestTemplate: impératif, verbeux, obsolète à terme.
- HttpExchange: natif Spring, moderne, typé, supporte RestClient/WebClient.
Migration rapide RestTemplate -> HttpExchange
- Identifiez les appels
restTemplate.getForObject(...),postForEntity(...), etc. - Créez une interface annotée
@HttpExchangeavec les méthodes équivalentes. - Remplacez les appels impératifs par l'injection du client généré.
- Conservez la logique métier dans le service, pas dans le client.
- Ajoutez des tests de contrat (MockWebServer/WireMock) pour valider la migration.
Checklist avant mise en production
- URL de base externalisée (
application.yml) et versionnée. - Timeouts configurés (connexion + lecture).
- Gestion explicite des erreurs 4xx/5xx (ProblemDetail + mapping métier).
- Logs HTTP activables en debug, désactivés en production par défaut.
- Stratégie de résilience en place (retry/circuit breaker) selon le contexte.
- Tests unitaires et d'intégration automatisés dans la CI.
Astuces de debug
- Activez les logs HTTP:
logging.level.org.springframework.web=DEBUGdansapplication.yml. - Utilisez Postman/Insomnia pour tester vos endpoints indépendamment du code.
Ressources complémentaires
Conclusion 🎯
L’interface HTTP de Spring via HttpExchange modernise la consommation d’API REST: code plus lisible, typé, testable et parfaitement intégré à Spring. Pour tout nouveau projet Spring Boot 3+, c’est la solution recommandée.
N’hésitez pas à explorer la documentation officielle et à expérimenter sur vos propres projets pour aller plus loin.
Ressources de base:
Glossaire express (vulgarisé)
- 🌐
API REST: un service distant que vous appelez via HTTP. Lien : https://www.sfeir.dev/rest-definition/ - 📦
DTO: un objet Java simple qui transporte les données. - 🪞
Proxy: un objet généré qui exécute l'appel HTTP à votre place. - 🧱
Boilerplate: du code répétitif, peu utile côté métier. - 🚗
RestClient: client HTTP Spring pour un usage synchrone. - 🚀
WebClient: client HTTP Spring pour un usage réactif (Mono,Flux).