Spring WebFlux – reactive programming

TL;DR: Spring WebFlux to nowy moduł Spring Framework 5.0 wprowadzający programowanie reaktywne do aplikacji webowych. Bazuje na Project Reactor i oferuje nieblokujące I/O, backpressure oraz funkcyjny model programowania. Idealny dla aplikacji o wysokim natężeniu ruchu i wymagających niskich opóźnień.

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.

Kluczowe cechy Spring WebFlux:

  • 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
Mono findUserById(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
Flux names = 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 RouterFunction userRoutes(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.

Uwaga: Bez backpressure aplikacja może zostać przytłoczona danymi i wyczerpać pamięć!
// Przykład backpressure
Flux numbers = 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 Mono fetchData(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() {
        Flux users = 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?

Pro tip: WebFlux nie jest srebrną kulą – używaj go świadomie, gdy rzeczywiście potrzebujesz reaktywności.

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

Pułapka: Blokowanie w reaktywnym kodzie niszczy wydajność! Unikaj .block() i .blockFirst() w produkcyjnym kodzie.
// ❌ NIE RÓB TAK - blokowanie w reaktywnym pipeline
Mono user = 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 Mono handleRequest(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)

Czy mogę używać Spring WebFlux z JPA/Hibernate?

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.

Jak debugować reaktywny kod?

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.

Czy WebFlux jest zawsze szybszy niż Spring MVC?

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.

Jak migrować z Spring MVC do WebFlux?

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.

Co z bezpieczeństwem w WebFlux?

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.

Jaka jest różnica między publishOn() a subscribeOn()?

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.

Czy WebFlux działa z tradycyjnymi serwerami aplikacji?

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

🚀 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!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry