Wstęp
W tradycyjnym modelu backendowym każde żądanie do serwera rezerwuje dla siebie wątek na cały czas obsługi. Nawet jeśli większość tego czasu sprowadza się do czekania na odpowiedź z bazy danych czy innego serwisu, wątek pozostaje zajęty. Przy rosnącej liczbie użytkowników szybko prowadzi to do blokad, ograniczonej skalowalności i nieefektywnego wykorzystania zasobów.
Wraz z popularyzacją mikroserwisów i architektury chmurowej zaczęto szukać alternatywy. Klasyczny model blokujący, znany ze Spring MVC, nadal ma swoje zastosowania – jest prostszy w implementacji i wystarczający w wielu scenariuszach. Jednak tam, gdzie liczba równoczesnych użytkowników gwałtownie rośnie, jego ograniczenia stają się zbyt widoczne.
Rozwiązaniem jest programowanie reaktywne, które pozwala wykonywać operacje bez blokowania wątków i traktuje wszystko jako strumień zdarzeń. W świecie Springa tę filozofię realizuje Spring WebFlux, a jego podstawą są dwa typy: Mono i Flux. To dzięki nim możemy modelować dane w reaktywny sposób – od pojedynczych wartości po nieograniczone strumienie – i budować aplikacje, które lepiej radzą sobie z dużym obciążeniem.
Co to jest Spring WebFlux?
Spring WebFlux to reaktywny, nieblokujący framework do budowania aplikacji webowych, wprowadzony w Spring Framework 5. Stanowi alternatywę dla klasycznego Spring MVC, który działa w modelu blokującym.
Kluczowa różnica polega na tym, jak oba podejścia zarządzają wątkami i zasobami:
- Spring MVC (blokujący) – działa w modelu „jeden wątek na żądanie”. Każde żądanie przydziela sobie wątek, który pozostaje zajęty do końca operacji. Jeśli trzeba czekać na bazę danych czy zewnętrzny serwis, wątek w tym czasie bezczynnie blokuje zasoby. Przy większym ruchu pula wątków szybko się wyczerpuje.
- Spring WebFlux (nieblokujący) – opiera się na pętli zdarzeń (event loop) i niewielkiej liczbie wątków. Żądanie inicjuje operację, a zamiast czekać na wynik, rejestrowany jest callback. Dzięki temu wątek może natychmiast obsługiwać kolejne żądania, a gdy dane będą gotowe, pętla zdarzeń przejmuje dalsze przetwarzanie i zwraca odpowiedź.
Efekt? WebFlux potrafi obsłużyć znacznie większą liczbę jednoczesnych połączeń przy użyciu minimalnej liczby wątków, co przekłada się na lepszą skalowalność i efektywniejsze wykorzystanie zasobów serwera.
Kiedy warto sięgnąć?
- Przy budowie mikroserwisów, które intensywnie komunikują się ze sobą.
- Gdy potrzebujesz obsługi wysokiej współbieżności i tysiąca użytkowników jednocześnie.
- Tworząc API strumieniujące dane w czasie rzeczywistym (np. logi, notyfikacje, SSE).
Jak dodać do projektu
Aby zacząć pracę ze Spring WebFlux, wystarczy dodać do projektu jedną zależność startową: spring-boot-starter-webflux.
Dla Mavena (pom.xml):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Dla Gradle (build.gradle):
implementation 'org.springframework.boot:spring-boot-starter-webflux'
Project Reactor
Spring WebFlux opiera się na Project Reactor – bibliotece stworzonej w oparciu o specyfikację Reactive Streams. To właśnie z niej pochodzą dwa kluczowe typy: Mono i Flux. Zanim jednak przejdziemy do ich szczegółów, warto poznać podstawowe pojęcia związane ze strumieniami reaktywnymi:
- Publisher (wydawca) – źródło danych, które emituje zdarzenia do subskrybentów. W Reactorze właśnie Mono i Flux pełnią tę rolę.
- Subscriber (subskrybent) – odbiera dane od Publishera. Może reagować na kolejne elementy (
onNext), błędy (onError) i zakończenie strumienia (onComplete). - Subscription (subskrypcja) – połączenie między Publisherem a Subscriberem. To właśnie tutaj działa mechanizm backpressure, który jest jednym z kluczowych elementów programowania reaktywnego.
Backpressure rozwiązuje problem, gdy producent generuje dane szybciej, niż konsument jest w stanie je przetworzyć. Dzięki niemu konsument może dozować tempo odbierania – np. poprosić tylko o 10 elementów i dopiero po ich przetworzeniu żądać kolejnej porcji. To chroni aplikację przed zalaniem danymi i zapchaniem pamięci. W Reactorze mechanizm działa automatycznie i możesz go kontrolować operatorami jakbuffer,drop,latestczyerror. W praktyce wystarczy wiedzieć, że backpressure to wbudowana ochrona, dzięki której aplikacja reaktywna nie „udusi się” zbyt dużym strumieniem danych. - Operator – funkcja, która pozwala przekształcać strumień, np.
map,filter,flatMap. Dzięki nim możemy deklaratywnie opisywać cały proces przetwarzania danych.
Najważniejsze w tym podejściu jest to, że definiujemy cały pipeline operacji, ale nic się nie dzieje, dopóki nie pojawi się subskrybent. Strumienie w Reactorze są domyślnie „zimne” (cold) – logika wykonuje się dopiero w momencie subskrypcji, a nie wcześniej.
Strumienie zimne i gorące
Warto zatrzymać się przy podziale strumieni na zimne (cold) i gorące (hot), bo to mocno wpływa na to, jak dane są emitowane i odbierane.
Strumienie zimne to domyślne zachowanie w Reactorze. Taki strumień startuje dopiero wtedy, gdy ktoś się do niego zasubskrybuje. Każdy subskrybent dostaje własny, niezależny przebieg danych. Najłatwiej porównać to do oglądania filmu na VOD – każdy klika play i zaczyna od początku, niezależnie od innych. W praktyce oznacza to, że jeśli w dwóch różnych miejscach w kodzie zasubskrybujesz ten sam Mono czy Flux, całe źródło danych uruchomi się dwukrotnie.
Strumienie gorące działają odwrotnie. Emitują dane niezależnie od tego, czy ktoś ich słucha, czy nie. To bardziej przypomina transmisję live – jeśli dołączysz w połowie, zobaczysz tylko to, co jest nadawane od tego momentu, a początek Ci ucieknie. Typowe przykłady to kliknięcia użytkownika, dane z WebSocketa czy strumienie giełdowe w czasie rzeczywistym. W takim przypadku subskrybenci współdzielą źródło danych i dostają tylko to, co pojawi się po ich dołączeniu.
W Reactorze możesz przekształcić strumień zimny w gorący, korzystając z operatorów share() lub publish(). Pierwszy pozwala współdzielić dane na żywo między wielu subskrybentów, a drugi daje większą kontrolę – np. możliwość ręcznego wystartowania emisji. To ważne, bo pozwala uniknąć wielokrotnego wykonywania kosztownych operacji (zapytania do bazy, wywołania API), gdy te same dane są potrzebne w kilku miejscach aplikacji.
Na co dzień, jeśli tworzysz funkcjonalności typu Server-Sent Events czy notyfikacje push, spotkasz się z gorącymi strumieniami. Na początek wystarczy jednak zapamiętać, że w WebFlux domyślnie pracujesz na strumieniach zimnych – czyli każdy subskrybent uruchamia źródło niezależnie dla siebie.
Mono – strumień z 0 lub 1 elementem
Mono to implementacja Publishera, która emituje co najwyżej jeden element. Można o nim myśleć jako o reaktywnym odpowiedniku CompletableFuture<T> lub Optional<T>, ale z dużo bogatszym i bardziej kompozycyjnym API.
Mono<T> może zakończyć się na trzy sposoby:
- Emitując pojedynczy element typu
T(sygnałonNext, a zaraz po nimonComplete). - Kończąc się bez emitowania elementu (sygnał
onComplete). Reprezentuje to pusty wynik. - Kończąc się błędem (sygnał
onError).
Kiedy używać Mono?
Mono to dobry wybór, gdy operacja powinna zwrócić jeden wynik albo nic. W praktyce oznacza to, że pracujemy na wartościach, które są unikalne, pojedyncze lub w danym kontekście nie mają sensu jako kolekcja.
Najczęściej spotkasz go w operacjach CRUD, gdzie naturalnie operujemy na pojedynczych rekordach. Dla przykładu:
findById– pobieranie obiektu po jego identyfikatorze. Jeśli rekord istnieje, zwróci go w Mono, a jeśli nie, zostanie zwróconeMono.empty().save– tworzenie nowego obiektu w bazie i zwrócenie tego obiektu z nadanym identyfikatorem.update– aktualizacja istniejącego zasobu i zwrócenie jego nowej reprezentacji.
Mono doskonale widać również w warstwie API. Typowy endpoint GET /api/zasob/{id} zwraca pojedynczy obiekt, POST /api/zasob tworzy nowy i odsyła go w odpowiedzi, a DELETE /api/zasob/{id} bardzo często zwraca Mono<Void>, które jest czytelnym sygnałem, że operacja zakończyła się sukcesem, ale nie ma treści do przesłania. To bardzo eleganckie podejście – zamiast sztucznego zwracania np. true czy pustego JSON-a, mamy jasną semantykę zakończonej operacji.
Warto też pamiętać o wykorzystaniu Mono przy operacjach agregujących. Wyobraź sobie, że masz Flux<Order> (więcej o Flux poniżej) reprezentujący zamówienia klienta. Gdy chcesz policzyć ich liczbę, używasz count(), a wynik – jedna liczba – zwracany jest właśnie jako Mono<Long>. Podobnie działa reduce(), które potrafi zredukować wiele elementów strumienia do jednej wartości (np. sumy wszystkich kwot zamówień).
Mono jest więc wygodnym narzędziem w każdym przypadku, gdy rezultatem działania metody jest pojedyncza wartość: obiekt, liczba, status operacji albo… brak wyniku. Dzięki temu API staje się czytelniejsze i łatwiej odwzorowuje logikę biznesową.
Przykłady
Zacznijmy od prostego przykładu jak w ogóle stworzyć obiekt Mono. Jest to dość proste, ponieważ mamy do dyspozycji kilka metod fabrycznych, które pozwalają zdefiniować różne scenariusze. Możemy utworzyć Mono z konkretną wartością (just), puste (empty), albo takie, które natychmiast zakończy się błędem (error). To podstawowe sposoby inicjalizacji, które przydają się w codziennej pracy z WebFluxem.
import reactor.core.publisher.Mono;
// Mono z konkretną wartością
Mono<String> monoJust = Mono.just("Mój przykładowy String!");
// Puste Mono, które zakończy się sygnałem onComplete
Mono<String> monoEmpty = Mono.empty();
// Mono, które natychmiast zakończy się błędem
Mono<String> monoError = Mono.error(new RuntimeException("Coś poszło nie tak"));
Endpoint w kontrolerze zwracający typ Mono
Aby uprościć przykład i ułatwić jego przetestowanie we własnym środowisku, zamiast warstwy bazy danych zasymulujemy pobieranie danych. Dzięki temu kod będzie prostszy i bardziej przejrzysty, a jednocześnie pozwoli lepiej skupić się na samej logice reaktywnej.
Załóżmy, że mamy prostą klasę User:
// Prosta klasa reprezentująca użytkownika
record User(String id, String name, boolean isActive) {}
Teraz stwórzmy kontroler, który symuluje wyszukiwanie użytkownika:
@RestController
@RequestMapping("/users")
public class UserController {
// Symulacja prostej bazy danych w pamięci
private final Map<String, User> users = Map.of(
"1", new User("1", "Ala Nowak", true),
"2", new User("2", "Bartek Kowalski", false)
);
// Metoda symulująca asynchroniczne wyszukiwanie
private Mono<User> findUserById(String id) {
return Mono.justOrEmpty(users.get(id));
}
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable String id) {
return findUserById(id)
.map(user -> ResponseEntity.ok(user)) // Jeśli user istnieje, opakuj go w 200 OK
.defaultIfEmpty(ResponseEntity.notFound().build()); // Jeśli Mono jest puste, zwróć 404
}
}
W tym przykładzie metoda findUserById(id) zwraca Mono<User>, korzystając z Mono.justOrEmpty(), które tworzy Mono z wartością, jeśli istnieje, lub puste Mono, jeśli wartość jest null.
Operatory na Mono
Mono (i reaktywnych strumieni w ogóle) to nie tylko sposób na przechowanie jednej asynchronicznej wartości. Cała zabawa zaczyna się wtedy, gdy używamy operatorów – dzięki nim możemy łatwo przekształcać dane, łączyć je albo filtrować. Zamiast pisać rozbudowaną, zagnieżdżoną logikę, układamy prosty i czytelny pipeline, który dokładnie opisuje, co ma się wydarzyć, gdy wynik będzie dostępny. Przyjrzyjmy się kilku najbardziej podstawowym i najczęściej używanym operatorom.
map(Function<T, R> mapper) – Transformuje element wewnątrz Mono w inny. Jest to operacja synchroniczna. Działa na tej samej zasadzie co strumieniach w Javie (Stream).
// Tworzymy użytkownika na potrzeby przykładu
User user = new User("1", "Celina Wiśniewska", true);
Mono<User> userMono = Mono.just(user);
// Wyciągamy tylko nazwę użytkownika
Mono<String> userNameMono = userMono.map(u -> u.name());
// Po zasubskrybowaniu, otrzymamy "Celina Wiśniewska"
userNameMono.subscribe(System.out::println);
Kolejną metodą jaką możemy użyć jest flatMap(Function<T, Mono<R>> mapper). Transformuje element w Mono w inne Mono. Używane do komponowania asynchronicznych operacji, gdy jedna zależy od wyniku drugiej.
// Symulacja serwisu, który pobiera dodatkowe dane
private Mono<String> fetchUserPermissions(String userId) {
// W prawdziwej aplikacji byłaby to np. asynchroniczna komunikacja z innym serwisem
return Mono.just("ROLE_ADMIN").delayElement(Duration.ofMillis(100));
}
Mono<String> permissionsMono = findUserById("1") // Zwraca Mono<User>
.flatMap(user -> fetchUserPermissions(user.id())); // fetchUserPermissions zwraca Mono<String>
// Po zasubskrybowaniu otrzymamy "ROLE_ADMIN"
permissionsMono.subscribe(System.out::println);
Inną metodą jaką jeszcze możemy użyć jest zipWith(Publisher<R> other, BiFunction<T, R, V> combinator). Łączy wynik bieżącego Mono z wynikiem innego Publishera (np. drugiego Mono). Ten operator czeka, aż oba strumienie zakończą się sukcesem, a następnie łączy ich wyniki za pomocą podanej funkcji. Jest idealny, gdy potrzebujesz wyników z dwóch niezależnych, równoległych operacji asynchronicznych.
// Definiujemy klasę do przechowywania połączonych wyników
record UserWithPermissions(User user, String permissions) {}
Mono<User> userMono = findUserById("1");
Mono<String> permissionsMono = fetchUserPermissions("1");
// Łączymy wyniki obu operacji
Mono<UserWithPermissions> combinedMono = userMono
.zipWith(permissionsMono, (user, permissions) -> new UserWithPermissions(user, permissions));
// Po zasubskrybowaniu otrzymamy połączony obiekt
// Wynik: UserWithPermissions[user=User[id=1, name=Ala Nowak, isActive=true], permissions=ROLE_ADMIN]
combinedMono.subscribe(System.out::println);
Flux – strumień z 0 do N elementów
Flux to drugi podstawowy typ w Reactorze. W przeciwieństwie do Mono, które zwraca jeden wynik albo nic, Flux potrafi emitować wiele elementów – od zera aż po nieskończony strumień. Może to być skończona sekwencja, np. lista obiektów z bazy danych, albo strumień, który działa bez końca, jak np. zdarzenia giełdowe.
W trakcie działania Flux przekazuje kolejne elementy do subskrybenta za pomocą sygnałów onNext. Gdy dane się kończą, wysyła sygnał onComplete, a jeśli coś pójdzie nie tak – onError. Dzięki temu mamy pełny obraz cyklu życia strumienia i możemy reagować zarówno na dane, jak i na zakończenie czy ewentualne błędy.
Kiedy używać Flux?
Flux to naturalny wybór wtedy, gdy operacja może zwrócić więcej niż jeden wynik albo dane mają napływać w czasie. W przeciwieństwie do Mono, które pasuje do pojedynczych wartości, Flux świetnie odwzorowuje kolekcje, listy czy ciągłe strumienie zdarzeń.
W praktyce spotkasz go chociażby przy pobieraniu danych z bazy metodami typu findAll albo findByCategory. To także typowy zwrot dla końcówek API, takich jak GET /api/resources, gdzie klient oczekuje listy elementów w odpowiedzi. Bardzo dobrze sprawdza się też w scenariuszach strumieniowania – np. w Server-Sent Events (SSE), gdzie klient otrzymuje aktualizacje w sposób ciągły.
Flux jest też niezastąpiony przy pracy na dużych zbiorach danych przetwarzanych krok po kroku, np. podczas czytania pliku linia po linii i transformowania zawartości. Co więcej, umożliwia obsługę nieskończonych strumieni, takich jak cykliczne generowanie zdarzeń w określonych odstępach czasu, co daje ogromne możliwości w aplikacjach działających w czasie rzeczywistym.
Przykłady
Tworzenie Flux jest równie proste jak w przypadku Mono. Do dyspozycji mamy kilka metod fabrycznych, które pozwalają przygotować strumień danych w zależności od potrzeb – od kilku ręcznie podanych elementów, przez kolekcje, aż po nieskończone strumienie emitujące wartości w czasie. Poniżej kilka podstawowych przykładów:
// Flux z kilkoma predefiniowanymi elementami
Flux<String> fluxJust = Flux.just("Jabłko", "Banan", "Pomarańcza");
// Flux na podstawie kolekcji (np. List)
List<String> owoce = List.of("Truskawka", "Malina", "Jagoda");
Flux<String> fluxFromIterable = Flux.fromIterable(owoce);
// Flux emitujący liczby co sekundę (nieskończony)
Flux<Long> intervalFlux = Flux.interval(Duration.ofSeconds(1));
Endpoint w kontrolerze zwracający typ Flux
Na podstawie poprzedniego przykładu, gdzie przygotowywaliśmy przykład endpointu pod zwracanie typu Mono, teraz go rozbudujemy, aby kontroler UserController zwracał również wszystkich użytkowników w sposób reaktywny.
@RestController
@RequestMapping("/users")
public class UserController {
// ... (poprzednia zawartość z mapą użytkowników)
// Metoda symulująca pobranie wszystkich użytkowników
private Flux<User> findAllUsers() {
return Flux.fromIterable(users.values());
}
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public Flux<User> getAllUsers() {
return findAllUsers();
}
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<User> streamAllUsers() {
return findAllUsers()
.delayElements(Duration.ofSeconds(5)); // Symulacja napływania danych co 5 sekund
}
}
Endpoint /users zwróci tradycyjną tablicę JSON. Z kolei /users/stream będzie wysyłał obiekty User co 5 sekund w formacie Server-Sent Events.
Operatory na Flux
Flux, podobnie jak Mono, ma do dyspozycji sporo operatorów – i to jeszcze bogatszy zestaw. Dzięki nim można deklaratywnie przetwarzać całe sekwencje danych. Jeśli wcześniej korzystałeś z Stream API w Javie 8, wiele rzeczy będzie wyglądać znajomo – map, filter czy flatMap działają tu bardzo podobnie.
Różnica jest taka, że w Flux pracujemy na elementach, które mogą pojawiać się stopniowo w czasie, a nie na gotowej kolekcji. Każdy operator zwraca nowy Flux, więc łatwo budować z nich przejrzyste i rozbudowane pipeline’y przetwarzania. Przyjrzyjmy się kilku najważniejszym z nich.
Metoda filter(Predicate<T> predicate) filtruje strumień, przepuszczając tylko te elementy, które spełniają warunek.
Flux<User> activeUsersFlux = findAllUsers()
.filter(user -> user.isActive());
// Po zasubskrybowaniu otrzymamy tylko użytkownika "Ala Nowak"
activeUsersFlux.subscribe(user -> System.out.println(user.name()));
flatMap(Function<T, Publisher<R>> mapper) transformuje każdy element w strumieniu w inny strumień (Mono lub Flux), a następnie spłaszcza wyniki w jeden Flux. Niezbędny do asynchronicznych operacji na każdym elemencie.
// Symulacja serwisu zwracającego zamówienia dla użytkownika
private Flux<String> getOrdersForUser(String userId) {
if (userId.equals("1")) {
return Flux.just("Zamówienie A", "Zamówienie B").delayElements(Duration.ofMillis(50));
}
return Flux.empty();
}
Flux<String> allOrdersForActiveUsers = findAllUsers() // Flux<User>
.filter(User::isActive) // Filtrujemy aktywnych
.flatMap(user -> getOrdersForUser(user.id())); // Dla każdego aktywnego usera pobieramy jego zamówienia
// Po zasubskrybowaniu otrzymamy "Zamówienie A", "Zamówienie B"
allOrdersForActiveUsers.subscribe(System.out::println);
Jeszcze ciekawą metodą jest collectList(). Zbiera wszystkie elementy ze strumienia Flux<T> do listy i zwraca Mono<List<T>>. To jeden z najczęstszych sposobów przejścia z Flux do Mono.
Mono<List<User>> usersListMono = findAllUsers().collectList();
// Po zasubskrybowaniu otrzymamy Mono zawierające listę wszystkich użytkowników
usersListMono.subscribe(list -> System.out.println("Liczba użytkowników: " + list.size()));
Kluczowe różnice i kiedy wybrać Mono, a kiedy Flux?
Zasada jest prosta: użyj Mono, jeśli spodziewasz się 0 lub 1 wyniku, a sięgnij po Flux, jeśli wynikiem może być 0, 1 lub wiele elementów. To kryterium liczności danych (tzw. kardynalności) powinno być podstawą przy modelowaniu API i logiki aplikacji.
Mono reprezentuje pojedynczy, asynchroniczny wynik. Najbliższą analogią w świecie Javy są CompletableFuture<T> czy Optional<T>. Typowym przykładem użycia jest endpoint GET /users/{id}, który zwraca jednego użytkownika, POST /users, który zwraca utworzony obiekt (Mono<User>), albo DELETE /users/{id}, który często kończy się Mono<Void>. Do pracy z Mono często wykorzystuje się operatory końcowe takie jak subscribe() lub – tylko w testach – block().
Flux natomiast reprezentuje sekwencję wielu wyników. W pewnym sensie można go porównać do Stream<T> albo Iterable<T>, z tą różnicą, że działa asynchronicznie. Najczęściej pojawia się przy GET /users, które zwraca listę użytkowników, albo przy operacjach, które zwracają strumień danych. Przykładem może być DELETE /users?status=inactive, gdzie zamiast pustej odpowiedzi serwer zwraca listę usuniętych użytkowników jako Flux<User>. Do pracy z Fluxem używa się m.in. operatorów collectList() czy subscribe().
Podsumowanie
Spring WebFlux wraz z Mono i Flux daje solidne podstawy do budowania aplikacji, które dobrze skalują się pod dużym obciążeniem. Na początku może to wymagać zmiany sposobu myślenia, ale zrozumienie różnicy między tymi dwoma typami to klucz do swobodnej pracy z podejściem reaktywnym.
Zasada jest prosta: Mono używamy, gdy chcemy zwrócić jeden element albo wcale – świetnie pasuje do pobierania pojedynczych obiektów czy operacji tworzenia i aktualizacji. Flux wybieramy wtedy, gdy wyników jest więcej – do list, sekwencji zdarzeń czy strumieniowania danych.
Dzięki świadomemu używaniu Mono i Flux Twój kod staje się czytelniejszy, łatwiejszy w utrzymaniu i gotowy na obsługę naprawdę dużej liczby żądań. A Ty – miałeś już okazję pracować z WebFlux? Jakie masz doświadczenia z Mono i Flux? Podziel się w komentarzu!
Pierwszy raz zetknąłem się z Webfluxem w aplikacji webowej, która zwracała dane, które nie mieściły się w pamięci. Trzeba było je strumieniowaniować. I tutaj Webflux bardzo się sprawdził, bo znakomicie uprościł kod.
A poznawanie jego tajników było bardzo ekscytujące.
Po pewnym czasie zauważyłem jednak, że kod reaktywny jest trudny w zrozumieniu. Wyraźnie czułem, że kiedy przechodzę do kodowania imperatywnego, oddycham spokojniej i jestem zrelaksowany.
O ile pierwsza aplikacja faktycznie świetnie do Webfluxa pasowała, później pracowałem z innymi, które już nie pasowały. I Webflux bardziej przeszkadzał niż pomagał.
A później pojawiły się wirtualne wątki i okazało się że można kodować prosto i wydajnie zarazem. Szperałem po Internecie czy wirtualne wątki całkiem zdetronizowały Webfluksa czy też są obszary gdzie Webflux króluje.
Takiej autorytatywnej odpowiedzi nie znalazłem. Spotkałem twierdzenia, że wirtualne wątki nie są tak wydajne, jak się je przedstawia. Spotkałem opinie, że zastępują Webfluksa, ale czułem niedosyt argumentów.
Uważam, że być może model współbieżności udostępniony przez Webfluksa ma w sobie ukrytą moc i w niektórych projektach może być to narzędzie użyteczne. Ale w 99% jest odwrotnie.