Se la tua applicazione Java arranca sotto carico, con thread bloccati in attesa di database o API esterne, sai già di cosa parliamo. Un pool di Tomcat a 200 thread non basta più quando ogni richiesta ne tiene occupato uno per secondi. Noi, di Meteora Web, abbiamo visto troppi backend soffocare perché scritti con lo stampino sincrono. La programmazione reattiva con Project Reactor e Spring WebFlux non è solo una moda: è il modo per usare le risorse della macchina al massimo, senza sprecare un thread mentre aspetti una risposta.
Cosa significa programmazione reattiva e perché serve a un'applicazione Java?
La programmazione reattiva è un paradigma basato su flussi di dati asincroni e sulla propagazione degli eventi. In pratica, invece di chiamare un metodo e aspettare che ritorni, ti sottoscrivi a uno stream e reagisci ogni volta che arriva un dato. Project Reactor è l'implementazione di riferimento per la JVM, ed è il motore di Spring WebFlux.
Perché serve? Perché le applicazioni moderne passano la maggior parte del tempo in I/O (database, chiamate REST, file system). Con il modello bloccante, ogni operazione I/O tiene occupato un thread. Con il modello reattivo, il thread viene liberato e può gestire altre richieste mentre l'operazione è in corso. Il risultato? Meno thread, più concorrenza, latenza ridotta.
Sponsored Protocol
Esempio concreto: una chiamata a un database che impiega 200 ms. Con Tomcat e thread pool da 10, puoi servire al massimo 50 richieste al secondo. Con WebFlux e un solo thread event-loop, ne servire centinaia perché mentre aspetti il database, il thread gestisce altre richieste.
Differenza tra sincrono bloccante e asincrono reattivo
// Bloccante (Spring MVC)
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow(); // thread bloccato finché non arriva il risultato
}
// Reattivo (Spring WebFlux)
@GetMapping("/user/{id}")
public Mono getUser(@PathVariable Long id) {
return userRepository.findById(id); // nessun blocco, il thread torna al loop
}
Quali sono i tipi fondamentali di Project Reactor: Mono e Flux?
Project Reactor si basa su due tipi principali: Mono per 0 o 1 elemento, Flux per 0 o N elementi. Entrambi implementano l'interfaccia Publisher e rispettano la specifica Reactive Streams (backpressure, subscriber, subscription).
Creare uno stream è semplice:
Mono hello = Mono.just("Hello Meteora Web");
Flux numeri = Flux.just(1, 2, 3, 4, 5);
Operatori essenziali
Gli operatori ti permettono di trasformare, filtrare, combinare i flussi. Alcuni fondamentali:
Sponsored Protocol
- map: trasforma ogni elemento sincrono
- flatMap: trasforma ogni elemento in un Publisher e appiattisce (per chiamate asincrone)
- filter: elimina elementi che non soddisfano il predicato
- zip: combina due o più stream in uno solo
Flux nomi = Flux.just("ana", "marco", "luigi");
Flux maiuscoli = nomi.map(String::toUpperCase); // "ANA", "MARCO", "LUIGI"
Flux utenti = nomi.flatMap(nome -> userRepository.findByName(nome)); // chiamate asincrone
Gestione degli errori
React fornisce operatori per gestire gli errori senza rompere la catena:
Flux conErrore = Flux.just(1, 2, 0)
.map(i -> 10 / i)
.onErrorReturn(-1); // se divisione per zero, restituisce -1 e continua
// Oppure retry
Mono conRetry = chiamataEsterna()
.retry(3); // riprova fino a 3 volte in caso di errore
Come si configura e si usa Spring WebFlux per un endpoint reattivo?
Per partire con Spring WebFlux basta aggiungere la dipendenza Maven:
org.springframework.boot
spring-boot-starter-webflux
Spring Boot configura automaticamente Netty come server embedded (al posto di Tomcat). Il controller reattivo si scrive in modo simile a Spring MVC, ma i metodi restituiscono Mono o Flux:
@RestController
@RequestMapping("/api/books")
public class BookController {
private final BookRepository bookRepository;
public BookController(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
@GetMapping
public Flux getAllBooks() {
return bookRepository.findAll(); // repository reattivo (R2DBC o MongoDB reactive)
}
@GetMapping("/{id}")
public Mono> getBook(@PathVariable String id) {
return bookRepository.findById(id)
.map(ResponseEntity::ok)
.defaultIfEmpty(ResponseEntity.notFound().build());
}
}
Nota: il repository deve essere reattivo. Per database relazionali si usa R2DBC, per MongoDB il driver reattivo nativo. Spring Data Reactive supporta entrambi.
Sponsored Protocol
WebClient: il fratello reattivo di RestTemplate
Per chiamate HTTP esterne, abbandona RestTemplate (deprecato) e usa WebClient, che è reattivo:
WebClient webClient = WebClient.create("https://api.example.com");
Mono response = webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(JsonNode.class);
Come gestire la backpressure e il threading in un flusso reattivo?
La backpressure è il meccanismo per cui il consumatore segnala al produttore quanti elementi può gestire. Se il produttore è più veloce del consumatore, senza backpressure la memoria si satura. Reactor gestisce la backpressure automaticamente, ma puoi controllarla con operatori come limitRate() o onBackpressureDrop().
Sponsored Protocol
Flux.range(1, 1_000_000)
.limitRate(100) // richiede 100 elementi alla volta
.subscribe(System.out::println);
Schedulers: dove eseguire il lavoro pesante
Per operazioni bloccanti (es. scrittura su file, chiamata a librerie non reattive) usa Schedulers.boundedElastic(). Per lavoro CPU-intensive usa Schedulers.parallel(). Di default Reactor esegue tutto sul thread di sottoscrizione (event-loop).
Mono.fromCallable(() -> {
// codice bloccante
return fileService.scriviSuDisco();
})
.subscribeOn(Schedulers.boundedElastic()) // esegue su un thread dedicato
.subscribe();
Quali sono gli errori comuni da evitare quando si inizia con WebFlux?
Chi arriva da Spring MVC tende a ripetere gli stessi errori. Ecco i più frequenti:
- Chiamare .block() dentro uno stream reattivo: blocca il thread event-loop, annullando i benefici. Mai usare
block()in un contesto reattivo, a meno che non sia assolutamente necessario (es. in un test). - Mischiare codice bloccante senza Schedulers: se devi scrivere su file, fallo su
boundedElastic, non direttamente in un flusso Reactor. - Ignorare la backpressure: se hai un producer che genera milioni di elementi, assicurati che il subscriber sia pronto o usa operatori di controllo.
- Usare RestTemplate in un'applicazione WebFlux:
RestTemplateè bloccante, usalo solo in contesti sincroni. In WebFlux usa sempreWebClient.
Esempio di errore:
Sponsored Protocol
// SBAGLIATO: block() nel controller
@GetMapping("/user/{id}")
public User getUserBlocking(@PathVariable Long id) {
return userRepository.findById(id).block(); // blocca il thread event-loop!
}
// CORRETTO
@GetMapping("/user/{id}")
public Mono getUserCorrect(@PathVariable Long id) {
return userRepository.findById(id);
}
Cosa fare adesso
Abbiamo visto i concetti fondamentali. Ora passa all'azione:
- Crea un progetto Spring Boot con webflux e scrivi un endpoint che restituisce
Mono.just("Hello Reactive World"). - Prova WebClient chiamando un'API pubblica (es. JSONPlaceholder) e restituisci il risultato come
Flux. - Leggi la documentazione ufficiale di Project Reactor per approfondire operatori come
flatMapSequential,delayElements,timeout. - Se usi database relazionali, aggiungi R2DBC e Spring Data Reactive per un'esperienza completamente reattiva.
Noi, di Meteora Web, utilizziamo queste tecnologie nei progetti che richiedono alte performance e scalabilità. Se vuoi vedere come applicarle al tuo stack, dai un'occhiata alla nostra guida su Java moderno e Spring Boot 3.