Cosa è Spring WebClient
Cosa è Spring WebClient
Efficient API Communication With Spring WebClient
Il WebClient è un client HTTP reattivo incluso in Spring WebFlux. Il suo principale vantaggio è la comunicazione asincrona e non bloccante tra microservizi o verso API esterne.
Nei moderni sistemi distribuiti, effettuare chiamate ad altri servizi da un’applicazione è un’operazione comunissima per gli sviluppatori. L’uso di WebClient si rivela ideale per questi scenari, grazie alla sua natura non bloccante.
Tutto in WebClient ruota attorno a eventi e flussi reattivi di dati, permettendoti di concentrarti sulla logica di business senza preoccuparti della gestione efficiente di thread, lock o stato.
Funzionalità chiave di WebClient – Cosa è Spring WebClient
- Non‑bloccante
Fornisce un modello di operazioni non bloccanti (non‑blocking) che scala notevolmente le performance di un’applicazione. - Client REST reattivo
Permette di interagire con il paradigma reattivo di Spring, sfruttando tutti i vantaggi tipici (backpressure, flussi di dati, ecc.). - Wrapper su Reactor Netty
Costruito su Reactor Netty, offre un’interfaccia di alto livello pur garantendo accesso diretto alle operazioni HTTP a basso livello. - Immutabile e thread‑safe
Può essere utilizzato in modo sicuro da più thread contemporaneamente, senza rischi di condizioni di race.
Come funziona WebClient
Alla base di WebClient c’è la programmazione reattiva, che contrasta fortemente con l’approccio classico basato su operazioni bloccanti. L’idea principale alla base della programmazione reattiva è quella di processare dati ed eventi man mano che si manifestano, senza bloccare i thread. Il meccanismo fondamentale che garantisce asincronia e non‑blocco in WebClient è l’Event Loop.
Immagina un sistema in cui esiste una coda di richieste in entrata (inbound queue) e un thread dedicato per ciascun core CPU, per tenere le cose semplici. Quel thread controlla costantemente la coda alla ricerca di compiti da eseguire. Quando non ci sono richieste, resta in stato di IDLE. Appena arriva una nuova richiesta nella coda, il thread la estrae per l’elaborazione, ma non resta in attesa del completamento dell’operazione: subito dopo aver inoltrato la richiesta, riparte per gestirne subito un’altra. In questo modo, la CPU non resta mai “ferma” in attesa di una risposta, ma continua a servire nuove richieste.
L’intero flusso reattivo di WebClient si basa su questa architettura a evento singolo che, grazie a Reactor Netty, mantiene aperte le connessioni e gestisce callback quando i dati arrivano o quando una risposta è pronta.
Cosa è Spring WebClient – Esempio di utilizzo di base
WebClient client = WebClient.create("https://api.example.com");
// Richiesta GET semplice
Mono<String> result = client.get()
.uri("/users/{id}", 42)
.retrieve()
.bodyToMono(String.class);
result.subscribe(response -> {
System.out.println("Risposta: " + response);
});
// Richiesta POST con payload JSON
Mono<ResponseEntity<MyResponse>> response = client.post()
.uri("/orders")
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(new OrderRequest("prod‑123", 3))
.retrieve()
.toEntity(MyResponse.class);
response.subscribe(res -> {
if (res.getStatusCode() == HttpStatus.CREATED) {
System.out.println("Ordine creato: " + res.getBody());
}
});
Configurazione avanzata del builder – Cosa è Spring WebClient
Puoi personalizzare l’istanza di WebClient tramite il suo builder, ad esempio per impostare header globali, timeout, filtri di log, autenticazione, ecc.
WebClient client = WebClient.builder()
.baseUrl("https://api.example.com")
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
.defaultHeader("X‑API‑KEY", "la‑tua‑chiave‑segreta")
.filter(ExchangeFilterFunctions.logRequest())
.clientConnector(new ReactorClientHttpConnector(HttpClient.create()
.tcpConfiguration(tcp -> tcp.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000))
.responseTimeout(Duration.ofSeconds(5))))
.build();
In questo esempio:
defaultHeader(...)imposta header predefiniti per tutte le richieste (ad es.Content-Typee un’ipotetica API key).filter(ExchangeFilterFunctions.logRequest())aggiunge un filtro che logga ogni richiesta HTTP.clientConnector(...)consente di configurare direttamente Reactor Netty, qui con un timeout di connessione di 5 secondi e un timeout di risposta di 5 secondi.
Gestione delle risposte e mapping – Cosa è Spring WebClient
Estrazione del corpo come Mono o Flux
- Mono<T>
Rappresenta un flusso che emette al massimo un valore. Utilizzato quando ti aspetti una singola entità in risposta (ad esempio, GET per un singolo oggetto).Mono<User> userMono = client.get() .uri("/users/{id}", 42) .retrieve() .bodyToMono(User.class); - Flux<T>
Rappresenta un flusso potenzialmente illimitato di elementi. Utilizzato quando la risposta HTTP è una lista o un flusso di oggetti (ad esempio, GET su/usersche ritorna un array di utenti).Flux<User> usersFlux = client.get() .uri("/users") .retrieve() .bodyToFlux(User.class);
Error handling
Per gestire eccezioni o errori HTTP, puoi intercettare stati di errore direttamente dopo il metodo retrieve():
Mono<User> userMono = client.get()
.uri("/users/{id}", 42)
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response ->
Mono.error(new RuntimeException("Errore nel client: " + response.statusCode())))
.onStatus(HttpStatus::is5xxServerError, response ->
Mono.error(new RuntimeException("Errore interno server: " + response.statusCode())))
.bodyToMono(User.class);
In alternativa, puoi usare il metodo .exchangeToMono() se vuoi gestire in modo più granulare lo stato della risposta, i suoi header e il body:
Mono<User> userMono = client.get()
.uri("/users/{id}", 42)
.exchangeToMono(response -> {
if (response.statusCode().equals(HttpStatus.OK)) {
return response.bodyToMono(User.class);
} else if (response.statusCode().is4xxClientError()) {
return Mono.error(new ClientException("Errore 4xx: " + response.statusCode()));
} else {
return Mono.error(new ServerException("Errore 5xx: " + response.statusCode()));
}
});
Timeout e ritardi – Cosa è Spring WebClient
Puoi impostare timeout specifici sia a livello di richiesta che a livello di risposta, usando operatori dei flussi reattivi o configurando Reactor Netty direttamente.
Timeout lato client
Mono<User> userMono = client.get()
.uri("/users/{id}", 42)
.retrieve()
.bodyToMono(User.class)
.timeout(Duration.ofSeconds(2)) // Scatta un errore se non riceve risposta entro 2 secondi
.onErrorResume(TimeoutException.class, e ->
Mono.error(new RuntimeException("La richiesta ha superato il tempo massimo")));
Configurazione Reactor Netty
HttpClient httpClient = HttpClient.create()
.responseTimeout(Duration.ofSeconds(5))
.doOnConnected(conn ->
conn.addHandlerLast(new ReadTimeoutHandler(5))
.addHandlerLast(new WriteTimeoutHandler(5)));
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
In questo esempio, Reactor Netty chiude la connessione se non arriva alcuna risposta entro 5 secondi, oppure se la scrittura del corpo supera 5 secondi.
Aggiungere intestazioni e parametri dinamicamente
Intestazioni personalizzate sul singolo request
WebClient client = WebClient.create("https://api.example.com");
Mono<String> dataMono = client.get()
.uri(uriBuilder -> uriBuilder
.path("/search")
.queryParam("q", "spring")
.queryParam("page", 2)
.build())
.header("X‑Request‑ID", UUID.randomUUID().toString())
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.bodyToMono(String.class);
Parametri di query inline
Utilizzando uri(Function<UriBuilder, URI>) puoi costruire l’URI in modo fluido, aggiungendo parametri di query, path variables o persino cambiare dominio a runtime.
Autenticazione e token JWT
Per scenari in cui devi aggiungere un token JWT (o qualsiasi altro Bearer token) a ogni richiesta, puoi usare un filtro personalizzato:
ExchangeFilterFunction authFilter = (request, next) -> {
ClientRequest filtered = ClientRequest.from(request)
.header(HttpHeaders.AUTHORIZATION, "Bearer " + getJwtToken())
.build();
return next.exchange(filtered);
};
WebClient client = WebClient.builder()
.baseUrl("https://api.protected.com")
.filter(authFilter)
.build();
private String getJwtToken() {
// Logica per recuperare o rinnovare il token JWT
return "eyJhbGciOiJIUzI1NiIsInR5cCI...";
}
In questo modo, prima di ogni richiesta, viene eseguito il filtro authFilter, che aggiunge l’header Authorization con il token aggiornato.
Streaming di dati con Flux
Se l’API di destinazione supporta streaming (ad es. Server‑Sent Events, o risposte chunked di tipo JSON array), puoi consumare le parti di risposta man mano che arrivano:
Flux<StockPrice> priceStream = client.get()
.uri("/stocks/stream/{symbol}", "AAPL")
.accept(MediaType.TEXT_EVENT_STREAM)
.retrieve()
.bodyToFlux(StockPrice.class);
priceStream.subscribe(price -> {
System.out.println("Prezzo di AAPL: " + price.getValue() + " a " + price.getTimestamp());
});
In questo scenario, non aspettiamo che l’intera risposta sia stata inviata: appena arriva un nuovo evento (un singolo oggetto StockPrice), viene emesso nel flusso Flux<StockPrice> e consumato in tempo reale.
Debug e logging
Per eseguire il debug delle chiamate HTTP e visualizzare effettivamente la richiesta e la risposta, puoi aggiungere filtri di log sia a livello di WebClient che a livello di Reactor Netty.
Filtri di log WebClient
ExchangeFilterFunction logRequest = ExchangeFilterFunctions.logRequest();
ExchangeFilterFunction logResponse = ExchangeFilterFunctions.logResponse();
WebClient client = WebClient.builder()
.filter(logRequest)
.filter(logResponse)
.build();
logRequest: logga il metodo HTTP, l’URI, header e corpo in uscita.logResponse: logga lo status code, header e corpo in arrivo.
Livello Reactor Netty
HttpClient httpClient = HttpClient.create()
.wiretap("reactor.netty.http.client.HttpClient", LogLevel.DEBUG, AdvancedByteBufFormat.TEXTUAL);
WebClient client = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
In questo caso, ogni byte in ingresso e uscita viene stampato nel log in formato testuale, il che aiuta a “vedere” esattamente quali dati passano sulla rete.
Gestione degli errori avanzata
Fallback e Retry
Se vuoi implementare meccanismi di retry in caso di errori transitori (ad es. 5xx o timeouts), puoi usare gli operatori di Reactor:
Mono<String> retryMono = client.get()
.uri("/unstable-endpoint")
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
.filter(throwable -> is5xxServerError(throwable)));
Qui:
retryWhen(...)effettua al massimo 3 tentativi, con backoff esponenziale (1 secondo → 2 secondi → 4 secondi) se l’errore rientra nella categoria server (500–599).- La funzione
is5xxServerError(throwable)è un predicato che verifica che l’eccezione sia dovuta a uno stato HTTP 5xx.
Circuit Breaker con Resilience4j
Per scenari più complessi, puoi integrare WebClient con Resilience4j per applicare circuit breaker:
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("myService");
WebClient client = WebClient.builder()
.filter(ExchangeFilterFunction.ofRequestProcessor(request ->
Mono.just(request).transformDeferred(CircuitBreakerOperator.of(circuitBreaker))))
.build();
Mono<String> protectedCall = client.get()
.uri("/critical-resource")
.retrieve()
.bodyToMono(String.class)
.onErrorResume(CircuitBreakerOpenException.class, e ->
Mono.just("Risposta di fallback"));
In questo modo, se il circuito è aperto (ossia troppi fallimenti consecutivi), tutte le richieste successive “cadono” immediatamente sul fallback senza tentare la chiamata HTTP.
Testing di WebClient – Cosa è Spring WebClient
Per testare componenti che usano WebClient, puoi utilizzare WebClientTest in Spring Boot o simulare il server con WebTestClient.
Esempio con WebTestClient
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class MyServiceTest {
@Autowired
private WebClient.Builder webClientBuilder;
private WebTestClient mockServer;
@BeforeEach
void setUp() {
this.mockServer = WebTestClient.bindToServer()
.baseUrl("http://localhost:8080")
.build();
}
@Test
void testGetUser() {
mockServer.get()
.uri("/users/42")
.exchange()
.expectStatus().isOk()
.expectBody(User.class)
.consumeWith(response -> {
User user = response.getResponseBody();
assertEquals("Mario Rossi", user.getName());
});
}
@Test
void testServiceClient() {
MyService myService = new MyService(webClientBuilder.baseUrl("http://localhost:8080").build());
User user = myService.getUserById(42).block();
assertEquals("Mario Rossi", user.getName());
}
}
In questo test:
- WebTestClient simula un server HTTP in ascolto su
localhost:8080. - Con
mockServer.get().uri("/users/42")definiamo cosa deve restituire il server “finto” in caso di GET su/users/42. - Nel test di integrazione, viene creato un’istanza di
MyServiceche usa lo stessoWebClientpuntato suhttp://localhost:8080. ChiamandogetUserById(42).block()otteniamo l’oggettoUsermappato dalla risposta JSON.
Best practice e consigli – Cosa è Spring WebClient
- Riutilizza le istanze di WebClient: Il builder di WebClient crea oggetti immutabili, quindi è consigliato definirne uno singleton per l’intera applicazione, evitando di ricreare istanze in continuazione.
- Limitare il buffer della risposta: Se prevedi risposte molto grandi, valuta di usare
bodyToFlux(DataBuffer.class)con operazioni su blocchi, per non saturare la memoria. - Rivedi i payload: Quando il corpo della risposta è un enum, una stringa o piccoli oggetti, usa direttamente
bodyToMono(String.class)obodyToMono(Integer.class)per semplicità. - Gestione dei thread e dei Scheduler: Ricorda che il flusso reattivo di WebClient, di default, utilizza il thread pool di Reactor Netty. Se devi processare dati CPU‑intensivi, sposta l’elaborazione su un scheduler dedicato (
publishOn(Schedulers.boundedElastic())oSchedulers.parallel()) per non bloccare il thread di I/O. - Timeout coerenti: Non imposti solo il timeout lato client: verifica sempre anche la configurazione di timeout nel server di destinazione, per evitare che la richiesta venga lasciata “in sospeso” troppo a lungo.
- Circuit Breaker e Retry: Analizza i pattern di errore per decidere se applicare retry o circuit breaker; attivarli indiscriminatamente può causare sovraccarichi su servizi già in crisi.
Conclusioni – Cosa è Spring WebClient
Spring WebClient, parte integrante di Spring WebFlux, offre un modo moderno e reattivo per comunicare con API esterne o tra microservizi. I punti di forza principali sono:
- Modello non‑bloccante che massimizza l’utilizzo delle risorse e riduce la latenza globale.
- Integrazione nativa con Reactor che permette di trattare le risposte come stream di dati, sfruttando tutte le operazioni tipiche di Reactor (map, flatMap, filter, retry, timeout, ecc.).
- Flessibilità nell’impostazione di timeout, filtri, autenticazione e circuit breaker, grazie all’architettura a builder e all’integrazione con librerie quali Resilience4j.
Per chi sviluppa microservizi in Spring, l’adozione di WebClient consente di costruire applicazioni più scalabili, reattive e resilienti, specialmente in un contesto in cui il numero di chiamate HTTP tra servizi tende a crescere rapidamente.
Innovaformazione, scuola informatica specialistica segue costantemente il mercato IT e promuove la formazione continua dei team di sviluppatori. Trovate nell’offerta formativa il corso Spring framework rivolto alle aziende.
Per altri articoli tecnici consigliamo di navigare sul nostro blog QUI.
INFO: info@innovaformazione.net – tel. 3471012275 (Dario Carrassi)
Vuoi essere ricontattato? Lasciaci il tuo numero telefonico e la tua email, ti richiameremo nelle 24h:
Articoli correlati
Lavoro Contabilità Milano
Protocollo SRTP ed implementazione
Le novità C# 14
Cosa è Laminas
Strumenti AI per sistemisti
