Reactive programming to już nie tylko buzzword – to rzeczywistość, która zmienia sposób w jaki tworzymy aplikacje webowe. Spring WebFlux, wprowadzony w Spring Framework 5.0, przynosi reaktywne programowanie do świata Spring, oferując alternatywę dla tradycyjnego Spring MVC.
Dlaczego reactive programming jest ważne?
W erze mikroserwisów i aplikacji cloud-native, tradycyjne podejście „jeden wątek na żądanie” przestaje być wystarczające. Reactive programming pozwala obsłużyć tysiące równoczesnych połączeń przy minimalnym zużyciu zasobów, co przekłada się na niższe koszty infrastruktury i lepszą responsywność aplikacji.
Co się nauczysz:
- Czym jest reactive programming i jak działa w Spring WebFlux
- Różnice między Spring WebFlux a Spring MVC
- Jak używać typów Mono i Flux w praktyce
- Implementacja reaktywnych endpointów REST
- Obsługa backpressure i error handling
- Kiedy używać WebFlux, a kiedy zostać przy MVC
Wymagania wstępne:
- Dobra znajomość Spring Framework i Spring Boot
- Doświadczenie z REST API
- Podstawowa wiedza o programowaniu asynchronicznym
- Java 8+ (lambdy i streams)
Czym jest Spring WebFlux?
Spring WebFlux to reaktywny web framework będący częścią Spring 5. W przeciwieństwie do Spring MVC, który bazuje na Servlet API i blokującym I/O, WebFlux wykorzystuje reaktywne biblioteki jak Project Reactor i obsługuje nieblokujące I/O.
- Nieblokujące I/O – żaden wątek nie czeka bezczynnie
- Backpressure – kontrola przepływu danych
- Funkcyjny model programowania
- Wsparcie dla reactive streams
- Kompatybilność z Spring ecosystem
Reactive Types: Mono i Flux
Podstawą Spring WebFlux są dwa typy z Project Reactor:
// Mono - reprezentuje 0 lub 1 element MonofindUserById(String id) { return Mono.fromCallable(() -> userRepository.findById(id)) .subscribeOn(Schedulers.elastic()); } // Flux - reprezentuje 0 do N elementów Flux findAllUsers() { return Flux.fromIterable(userRepository.findAll()) .delayElements(Duration.ofMillis(100)); // symulacja opóźnienia }
Podstawowe operatory
// Transformacje Fluxnames = Flux.just("Anna", "Jan", "Maria") .map(String::toUpperCase) .filter(name -> name.startsWith("A")); // Łączenie strumieni Flux combined = Flux.range(1, 5) .zipWith(Flux.range(10, 5), (a, b) -> a + b); // Error handling Mono userWithFallback = findUserById("123") .onErrorReturn(new User("default", "Default User"));
Tworzenie reaktywnych REST endpoints
WebFlux oferuje dwa sposoby definiowania endpointów: adnotacje (podobnie jak w MVC) oraz funkcyjny routing:
Podejście z adnotacjami
@RestController @RequestMapping("/api/users") public class UserController { private final UserService userService; @GetMapping("/{id}") public Mono> getUser(@PathVariable String id) { return userService.findById(id) .map(user -> ResponseEntity.ok(user)) .defaultIfEmpty(ResponseEntity.notFound().build()); } @GetMapping(produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux streamUsers() { return userService.findAll() .delayElements(Duration.ofSeconds(1)); // Server-Sent Events } @PostMapping public Mono createUser(@RequestBody Mono userMono) { return userMono .flatMap(user -> userService.save(user)) .doOnNext(saved -> log.info("User created: {}", saved.getId())); } }
Funkcyjne podejście (RouterFunction)
@Configuration public class UserRouter { @Bean public RouterFunctionuserRoutes(UserHandler handler) { return RouterFunctions .route(GET("/api/users/{id}"), handler::getUser) .andRoute(GET("/api/users"), handler::listUsers) .andRoute(POST("/api/users"), handler::createUser); } } @Component public class UserHandler { public Mono getUser(ServerRequest request) { String id = request.pathVariable("id"); return userService.findById(id) .flatMap(user -> ServerResponse.ok() .contentType(MediaType.APPLICATION_JSON) .body(BodyInserters.fromValue(user))) .switchIfEmpty(ServerResponse.notFound().build()); } }
Backpressure – kontrola przepływu danych
Backpressure to mechanizm pozwalający konsumentowi kontrolować tempo dostarczania danych przez producenta. Jest to kluczowe przy przetwarzaniu dużych wolumenów danych.
// Przykład backpressure Fluxnumbers = Flux.range(1, 1000000); numbers .onBackpressureBuffer(100) // bufor na 100 elementów .publishOn(Schedulers.parallel()) .subscribe(new BaseSubscriber () { @Override protected void hookOnSubscribe(Subscription subscription) { request(10); // początkowo pobierz 10 elementów } @Override protected void hookOnNext(Integer value) { // przetwarzanie... if (value % 10 == 0) { request(10); // co 10 elementów, poproś o kolejne 10 } } });
WebClient – reaktywny HTTP client
WebFlux wprowadza WebClient jako reaktywną alternatywę dla RestTemplate:
@Component public class ExternalApiClient { private final WebClient webClient; public ExternalApiClient(WebClient.Builder builder) { this.webClient = builder .baseUrl("https://api.example.com") .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); } public MonofetchData(String id) { return webClient .get() .uri("/data/{id}", id) .retrieve() .onStatus(HttpStatus::is4xxClientError, response -> Mono.error(new ClientException("Client error"))) .bodyToMono(ApiResponse.class) .timeout(Duration.ofSeconds(5)) .retry(3); } }
Testowanie reaktywnych aplikacji
Spring WebFlux dostarcza WebTestClient do testowania reaktywnych endpointów:
@WebFluxTest(UserController.class) class UserControllerTest { @Autowired private WebTestClient webTestClient; @MockBean private UserService userService; @Test void shouldReturnUser() { User user = new User("1", "John Doe"); when(userService.findById("1")).thenReturn(Mono.just(user)); webTestClient .get() .uri("/api/users/1") .exchange() .expectStatus().isOk() .expectBody(User.class) .isEqualTo(user); } @Test void shouldStreamUsers() { Fluxusers = Flux.just( new User("1", "John"), new User("2", "Jane") ).delayElements(Duration.ofMillis(100)); when(userService.findAll()).thenReturn(users); webTestClient .get() .uri("/api/users") .accept(MediaType.TEXT_EVENT_STREAM) .exchange() .expectStatus().isOk() .expectBodyList(User.class) .hasSize(2); } }
Kiedy używać Spring WebFlux?
WebFlux sprawdza się gdy:
- Aplikacja obsługuje tysiące równoczesnych połączeń
- Masz do czynienia z streamingiem danych
- Integrujesz się z reaktywnymi bazami danych (MongoDB, Cassandra, Redis)
- Potrzebujesz Server-Sent Events lub WebSockets
- Chcesz zminimalizować zużycie zasobów
Zostań przy Spring MVC gdy:
- Twój zespół nie ma doświadczenia z reaktywnym programowaniem
- Używasz JDBC lub JPA (brak reaktywnego wsparcia)
- Aplikacja ma niskie obciążenie
- Potrzebujesz prostoty i przewidywalności
Pułapki i best practices
// ❌ NIE RÓB TAK - blokowanie w reaktywnym pipeline Monouser = findUser(id); User blocked = user.block(); // To niszczy całą ideę! return processUser(blocked); // ✅ TAK - pozostań reaktywny return findUser(id) .flatMap(user -> processUser(user));
Error handling best practices
public MonohandleRequest(ServerRequest request) { return processRequest(request) .flatMap(result -> ServerResponse.ok().bodyValue(result)) .onErrorResume(ValidationException.class, e -> ServerResponse.badRequest().bodyValue(e.getMessage())) .onErrorResume(NotFoundException.class, e -> ServerResponse.notFound().build()) .onErrorResume(Exception.class, e -> { log.error("Unexpected error", e); return ServerResponse.status(500).build(); }); }
Często zadawane pytania (FAQ)
Technicznie tak, ale to nie ma sensu. JPA/Hibernate są blokujące, więc tracisz korzyści z reaktywności. Użyj reaktywnych sterowników jak Spring Data R2DBC lub reaktywnego MongoDB.
Użyj operatorów .log() do śledzenia przepływu danych. Włącz debug logging dla Reactor. IntelliJ IDEA ma także wsparcie dla debugowania reaktywnych strumieni.
Nie! Dla małego obciążenia MVC może być nawet szybszy. WebFlux pokazuje przewagę przy wysokim obciążeniu i operacjach I/O-bound. Zawsze testuj wydajność dla swojego przypadku użycia.
Stopniowo! Zacznij od nowych endpointów, szczególnie tych z dużym ruchem. WebFlux i MVC mogą współistnieć w różnych aplikacjach. Przeszkol zespół i migruj incrementalnie.
Spring Security 5.0+ ma pełne wsparcie dla WebFlux. Używasz podobnych konceptów, ale z reaktywnymi typami. Pamiętaj o @EnableWebFluxSecurity zamiast @EnableWebSecurity.
subscribeOn() wpływa na cały upstream (gdzie subskrypcja się zaczyna), publishOn() wpływa na downstream (gdzie dane są emitowane). SubscribeOn działa globalnie, publishOn od miejsca wywołania.
WebFlux domyślnie używa Netty, ale może działać na Tomcat, Jetty lub Undertow. Jednak tylko Netty oferuje pełne nieblokujące I/O. Na tradycyjnych serwerach tracisz część korzyści.
Przydatne zasoby
- Project Reactor Documentation
- Spring WebFlux Reference
- Reactive Streams Specification
- Hands-on Reactive Programming
🚀 Zadanie dla Ciebie
Stwórz prostą aplikację TODO z użyciem Spring WebFlux:
- REST API z operacjami CRUD
- Użyj MongoDB Reactive lub R2DBC
- Zaimplementuj endpoint streamujący zmiany (SSE)
- Dodaj WebClient do integracji z zewnętrznym API
- Napisz testy z WebTestClient
Porównaj wydajność z analogiczną aplikacją w Spring MVC!
Spring WebFlux otwiera nowe możliwości w świecie Java web development. Choć wymaga zmiany sposobu myślenia, korzyści w postaci skalowalności i wydajności są tego warte. A jak Ty wykorzystujesz reaktywne programowanie w swoich projektach? Podziel się doświadczeniami w komentarzach!