Cosa è Spring WebClient

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-Type e 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 /users che 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:

  1. WebTestClient simula un server HTTP in ascolto su localhost:8080.
  2. Con mockServer.get().uri("/users/42") definiamo cosa deve restituire il server “finto” in caso di GET su /users/42.
  3. Nel test di integrazione, viene creato un’istanza di MyService che usa lo stesso WebClient puntato su http://localhost:8080. Chiamando getUserById(42).block() otteniamo l’oggetto User mappato dalla risposta JSON.

Best practice e consigli – Cosa è Spring WebClient

  1. 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.
  2. 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.
  3. Rivedi i payload: Quando il corpo della risposta è un enum, una stringa o piccoli oggetti, usa direttamente bodyToMono(String.class) o bodyToMono(Integer.class) per semplicità.
  4. 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()) o Schedulers.parallel()) per non bloccare il thread di I/O.
  5. 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.
  6. 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.

(fonte) (fonte)

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:

    Ti potrebbe interessare

    Articoli correlati