WebFlux in Java Spring Boot

Spring WebFlux è il framework web reattivo introdotto in Spring 5 che consente di costruire applicazioni non bloccanti, asincrone e basate su un modello di programmazione funzionale o ad annotazioni. A differenza del tradizionale Spring MVC, che si basa su un'architettura sincrona e bloccante con un thread per richiesta, WebFlux sfrutta il paradigma reattivo della libreria Project Reactor per gestire un numero elevato di connessioni concorrenti con un numero limitato di thread.

Differenze tra Spring MVC e Spring WebFlux

Spring MVC è costruito sull'API Servlet e segue il modello "thread-per-request": ogni richiesta HTTP viene assegnata a un thread del pool che resta occupato fino al completamento dell'elaborazione, comprese eventuali operazioni di I/O bloccanti come query al database o chiamate HTTP esterne. Spring WebFlux, al contrario, utilizza un server reattivo come Netty (default) o un container Servlet 3.1+ in modalità non bloccante, permettendo a un singolo thread di gestire molte richieste tramite un event loop.

Il vantaggio principale di WebFlux emerge in scenari ad alta concorrenza con operazioni I/O-bound: streaming di dati, comunicazione con servizi esterni lenti, gateway API e sistemi che gestiscono migliaia di connessioni simultanee. Per applicazioni CPU-bound o con stack tecnologici interamente bloccanti, Spring MVC resta spesso la scelta più semplice ed efficace.

Project Reactor: Mono e Flux

WebFlux si basa su Project Reactor, che fornisce due tipi fondamentali per rappresentare flussi di dati asincroni. Mono<T> rappresenta un flusso che emette zero o un elemento, mentre Flux<T> rappresenta un flusso che emette da zero a N elementi. Entrambi sono pubblicatori conformi alla specifica Reactive Streams.

import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

public class ReactiveExample {

    // Restituisce un singolo valore in modo asincrono
    public Mono<String> getSingleValue() {
        return Mono.just("Hello WebFlux");
    }

    // Restituisce una sequenza di valori
    public Flux<Integer> getNumberSequence() {
        return Flux.range(1, 10)
                .map(number -> number * 2)
                .filter(number -> number > 5);
    }

    // Combina più sorgenti asincrone
    public Mono<String> combineResults() {
        Mono<String> first = Mono.just("Hello");
        Mono<String> second = Mono.just("World");
        return Mono.zip(first, second, (a, b) -> a + " " + b);
    }
}

I flussi reattivi sono lazy: nulla viene eseguito finché non c'è una sottoscrizione. Questo permette di costruire pipeline di trasformazioni dichiarative che vengono valutate solo al momento del consumo, abilitando ottimizzazioni come backpressure e scheduling esplicito.

Configurazione del progetto

Per iniziare con WebFlux è sufficiente includere la dipendenza spring-boot-starter-webflux nel file pom.xml di un progetto Maven. È importante non includere contemporaneamente spring-boot-starter-web, perché Spring Boot darebbe priorità a quest'ultimo configurando un server Servlet tradizionale.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-r2dbc</artifactId>
    </dependency>
    <dependency>
        <groupId>io.r2dbc</groupId>
        <artifactId>r2dbc-postgresql</artifactId>
    </dependency>
    <dependency>
        <groupId>io.projectreactor</groupId>
        <artifactId>reactor-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

La dipendenza spring-boot-starter-data-r2dbc abilita l'accesso reattivo ai database relazionali tramite il driver R2DBC, l'equivalente non bloccante di JDBC. Per MongoDB esiste invece spring-boot-starter-data-mongodb-reactive.

Modello ad annotazioni

WebFlux supporta lo stesso stile di programmazione di Spring MVC basato su annotazioni come @RestController, @GetMapping e @PostMapping. La differenza fondamentale è che i metodi del controller restituiscono tipi reattivi anziché valori sincroni.

import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    // Endpoint che restituisce un singolo prodotto
    @GetMapping("/{id}")
    public Mono<Product> getProductById(@PathVariable String id) {
        return productService.findById(id);
    }

    // Endpoint che restituisce una collezione di prodotti
    @GetMapping
    public Flux<Product> getAllProducts() {
        return productService.findAll();
    }

    // Endpoint per la creazione di un prodotto
    @PostMapping
    public Mono<Product> createProduct(@RequestBody Mono<Product> productMono) {
        return productMono.flatMap(productService::save);
    }

    // Endpoint per l'aggiornamento di un prodotto esistente
    @PutMapping("/{id}")
    public Mono<Product> updateProduct(
            @PathVariable String id,
            @RequestBody Mono<Product> productMono) {
        return productMono.flatMap(product -> productService.update(id, product));
    }

    // Endpoint per l'eliminazione di un prodotto
    @DeleteMapping("/{id}")
    public Mono<Void> deleteProduct(@PathVariable String id) {
        return productService.deleteById(id);
    }
}

Spring WebFlux gestisce automaticamente la sottoscrizione ai flussi restituiti dai metodi del controller. Il framework si occupa di scrivere i dati nella risposta HTTP non appena diventano disponibili, applicando la serializzazione JSON tramite Jackson per impostazione predefinita.

Modello funzionale con RouterFunction

Oltre allo stile ad annotazioni, WebFlux offre un modello di programmazione funzionale basato su RouterFunction e HandlerFunction. Questo approccio è particolarmente adatto per definire route in modo programmatico e componibile.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;

import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;

@Configuration
public class ProductRouter {

    @Bean
    public RouterFunction<ServerResponse> productRoutes(ProductHandler handler) {
        return route(GET("/api/v2/products"), handler::getAll)
                .andRoute(GET("/api/v2/products/{id}"), handler::getById)
                .andRoute(POST("/api/v2/products"), handler::create)
                .andRoute(PUT("/api/v2/products/{id}"), handler::update)
                .andRoute(DELETE("/api/v2/products/{id}"), handler::delete);
    }
}

Gli handler corrispondenti sono classi separate che implementano la logica di gestione delle richieste, accedendo direttamente all'oggetto ServerRequest e costruendo un ServerResponse reattivo.

import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

@Component
public class ProductHandler {

    private final ProductService productService;

    public ProductHandler(ProductService productService) {
        this.productService = productService;
    }

    public Mono<ServerResponse> getAll(ServerRequest request) {
        return ServerResponse.ok()
                .contentType(MediaType.APPLICATION_JSON)
                .body(productService.findAll(), Product.class);
    }

    public Mono<ServerResponse> getById(ServerRequest request) {
        String id = request.pathVariable("id");
        return productService.findById(id)
                .flatMap(product -> ServerResponse.ok()
                        .contentType(MediaType.APPLICATION_JSON)
                        .bodyValue(product))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> create(ServerRequest request) {
        Mono<Product> productMono = request.bodyToMono(Product.class);
        return productMono
                .flatMap(productService::save)
                .flatMap(saved -> ServerResponse.ok().bodyValue(saved));
    }

    public Mono<ServerResponse> update(ServerRequest request) {
        String id = request.pathVariable("id");
        return request.bodyToMono(Product.class)
                .flatMap(product -> productService.update(id, product))
                .flatMap(updated -> ServerResponse.ok().bodyValue(updated))
                .switchIfEmpty(ServerResponse.notFound().build());
    }

    public Mono<ServerResponse> delete(ServerRequest request) {
        String id = request.pathVariable("id");
        return productService.deleteById(id)
                .then(ServerResponse.noContent().build());
    }
}

Accesso reattivo ai dati con R2DBC

Per mantenere la natura non bloccante dell'intera pipeline, è essenziale utilizzare driver di accesso ai dati reattivi. R2DBC (Reactive Relational Database Connectivity) è la specifica che fornisce un'API non bloccante per database relazionali come PostgreSQL, MySQL, SQL Server e H2.

import org.springframework.data.annotation.Id;
import org.springframework.data.relational.core.mapping.Table;

@Table("products")
public class Product {

    @Id
    private String id;
    private String name;
    private String description;
    private Double price;
    private Integer stock;

    // Costruttori, getter e setter omessi per brevità
}
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

public interface ProductRepository extends ReactiveCrudRepository<Product, String> {

    // Query derivata dal nome del metodo
    Flux<Product> findByNameContainingIgnoreCase(String keyword);

    // Filtra prodotti con stock superiore a una soglia
    Flux<Product> findByStockGreaterThan(Integer threshold);

    // Restituisce il primo prodotto corrispondente al criterio
    Mono<Product> findFirstByNameOrderByPriceAsc(String name);
}

Il repository estende ReactiveCrudRepository, che fornisce metodi CRUD standard con firme che restituiscono Mono e Flux. Spring Data genera automaticamente l'implementazione, traducendo i nomi dei metodi in query SQL eseguite in modo non bloccante.

Service layer reattivo

Lo strato di servizio compone le operazioni del repository applicando logica di business. È fondamentale evitare chiamate bloccanti all'interno della pipeline reattiva: ogni operazione I/O deve essere espressa tramite operatori che restituiscono Mono o Flux.

import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

@Service
public class ProductService {

    private final ProductRepository repository;

    public ProductService(ProductRepository repository) {
        this.repository = repository;
    }

    public Flux<Product> findAll() {
        return repository.findAll();
    }

    public Mono<Product> findById(String id) {
        return repository.findById(id)
                .switchIfEmpty(Mono.error(
                        new ProductNotFoundException("Prodotto non trovato: " + id)));
    }

    public Mono<Product> save(Product product) {
        // Applica validazioni o trasformazioni prima del salvataggio
        return Mono.just(product)
                .map(this::normalizeProduct)
                .flatMap(repository::save);
    }

    public Mono<Product> update(String id, Product updated) {
        return repository.findById(id)
                .flatMap(existing -> {
                    existing.setName(updated.getName());
                    existing.setPrice(updated.getPrice());
                    existing.setStock(updated.getStock());
                    return repository.save(existing);
                });
    }

    public Mono<Void> deleteById(String id) {
        return repository.deleteById(id);
    }

    // Metodo di supporto per la normalizzazione
    private Product normalizeProduct(Product product) {
        if (product.getName() != null) {
            product.setName(product.getName().trim());
        }
        return product;
    }
}

WebClient per chiamate HTTP non bloccanti

WebFlux include WebClient, il successore reattivo di RestTemplate. È il client HTTP raccomandato per consumare API REST in modo asincrono, con supporto completo per streaming, backpressure e gestione degli errori.

import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Flux;

@Service
public class ExternalApiService {

    private final WebClient webClient;

    public ExternalApiService(WebClient.Builder builder) {
        this.webClient = builder
                .baseUrl("https://api.example.com")
                .defaultHeader("Accept", "application/json")
                .build();
    }

    // Recupera un singolo utente dall'API esterna
    public Mono<User> fetchUser(String userId) {
        return webClient.get()
                .uri("/users/{id}", userId)
                .retrieve()
                .bodyToMono(User.class);
    }

    // Recupera una lista di utenti come flusso
    public Flux<User> fetchAllUsers() {
        return webClient.get()
                .uri("/users")
                .retrieve()
                .bodyToFlux(User.class);
    }

    // Invia dati con gestione degli errori HTTP
    public Mono<User> createUser(User user) {
        return webClient.post()
                .uri("/users")
                .bodyValue(user)
                .retrieve()
                .onStatus(status -> status.is4xxClientError(),
                        response -> Mono.error(new ClientApiException("Richiesta non valida")))
                .onStatus(status -> status.is5xxServerError(),
                        response -> Mono.error(new ServerApiException("Errore del server remoto")))
                .bodyToMono(User.class);
    }
}

Server-Sent Events e streaming

Una delle caratteristiche più potenti di WebFlux è la possibilità di restituire flussi continui di dati al client tramite Server-Sent Events o JSON streaming. Questo permette di costruire feed in tempo reale, dashboard live o notifiche push senza ricorrere a WebSocket.

import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;

import java.time.Duration;
import java.time.LocalDateTime;

@RestController
@RequestMapping("/api/stream")
public class StreamingController {

    // Emette un evento ogni secondo con il timestamp corrente
    @GetMapping(value = "/time", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<String> streamTime() {
        return Flux.interval(Duration.ofSeconds(1))
                .map(tick -> "Timestamp: " + LocalDateTime.now());
    }

    // Streaming di prodotti da un repository reattivo
    @GetMapping(value = "/products", produces = MediaType.APPLICATION_NDJSON_VALUE)
    public Flux<Product> streamProducts(ProductRepository repository) {
        return repository.findAll()
                .delayElements(Duration.ofMillis(500));
    }
}

L'utilizzo di MediaType.TEXT_EVENT_STREAM_VALUE formatta la risposta secondo lo standard SSE, mentre APPLICATION_NDJSON_VALUE produce un flusso JSON delimitato da newline, ideale per il consumo da parte di client che processano oggetti uno alla volta.

Gestione degli errori

WebFlux consente di gestire gli errori in modo dichiarativo all'interno della pipeline reattiva tramite operatori come onErrorReturn, onErrorResume e onErrorMap. Per una gestione globale è possibile definire un @RestControllerAdvice.

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import reactor.core.publisher.Mono;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(ProductNotFoundException.class)
    public Mono<ResponseEntity<ErrorResponse>> handleNotFound(ProductNotFoundException ex) {
        ErrorResponse error = new ErrorResponse("NOT_FOUND", ex.getMessage());
        return Mono.just(ResponseEntity.status(HttpStatus.NOT_FOUND).body(error));
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public Mono<ResponseEntity<ErrorResponse>> handleBadRequest(IllegalArgumentException ex) {
        ErrorResponse error = new ErrorResponse("BAD_REQUEST", ex.getMessage());
        return Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error));
    }

    @ExceptionHandler(Exception.class)
    public Mono<ResponseEntity<ErrorResponse>> handleGeneric(Exception ex) {
        ErrorResponse error = new ErrorResponse("INTERNAL_ERROR", "Errore interno del server");
        return Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error));
    }
}

Testing di applicazioni WebFlux

Spring fornisce WebTestClient per testare endpoint reattivi in modo idiomatico, e la libreria reactor-test per verificare il comportamento di flussi Mono e Flux tramite StepVerifier.

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.test.web.reactive.server.WebTestClient;

@WebFluxTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    private WebTestClient webTestClient;

    @Test
    void shouldReturnAllProducts() {
        webTestClient.get()
                .uri("/api/products")
                .exchange()
                .expectStatus().isOk()
                .expectBodyList(Product.class)
                .hasSize(3);
    }

    @Test
    void shouldReturnProductById() {
        webTestClient.get()
                .uri("/api/products/{id}", "123")
                .exchange()
                .expectStatus().isOk()
                .expectBody(Product.class)
                .value(product -> product.getId().equals("123"));
    }
}
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;

class ReactiveStreamTest {

    @Test
    void shouldEmitExpectedSequence() {
        Flux<Integer> flux = Flux.range(1, 5)
                .map(value -> value * 2);

        // Verifica la sequenza emessa dal flusso
        StepVerifier.create(flux)
                .expectNext(2, 4, 6, 8, 10)
                .verifyComplete();
    }
}

Quando usare WebFlux

WebFlux non è una sostituzione universale di Spring MVC. La scelta dipende dalle caratteristiche del carico di lavoro e dallo stack tecnologico disponibile. WebFlux è la scelta migliore per applicazioni con molte connessioni concorrenti e operazioni I/O intensive, gateway API che orchestrano chiamate verso più servizi, sistemi di streaming e applicazioni che beneficiano di backpressure naturale. Spring MVC resta preferibile per applicazioni con logica prevalentemente sincrona, integrazione con librerie bloccanti come JDBC tradizionale, e team meno familiari con la programmazione reattiva, che presenta una curva di apprendimento significativa.

L'adozione di WebFlux richiede di ripensare l'intera pipeline applicativa: introdurre anche una sola operazione bloccante in un flusso reattivo annulla i benefici dell'architettura non bloccante e può degradare le prestazioni. Quando un'operazione bloccante è inevitabile, è essenziale isolarla su uno scheduler dedicato tramite subscribeOn(Schedulers.boundedElastic()) per evitare di saturare l'event loop.

Conclusione

Spring WebFlux porta il paradigma reattivo nell'ecosistema Spring, offrendo strumenti potenti per costruire applicazioni scalabili e resilienti. La combinazione di Project Reactor, R2DBC e WebClient permette di mantenere una pipeline completamente non bloccante dall'ingresso della richiesta HTTP fino all'accesso ai dati. Comprendere quando applicare WebFlux e quando preferire Spring MVC è fondamentale per progettare sistemi efficienti, sfruttando i vantaggi della programmazione reattiva senza incorrere nella sua complessità intrinseca quando non è necessaria.