Spring WebMVC vs WebFlux – Które wybrać w 2020?

TL;DR: Spring WebMVC to tradycyjny, synchroniczny model programowania oparty na servletach, idealny dla typowych aplikacji CRUD. Spring WebFlux wprowadza reactive programming z asynchronicznym, non-blocking I/O – świetny dla aplikacji wymagających wysokiej przepustowości i skalowalności. Wybór zależy od typu aplikacji, doświadczenia zespołu i wymagań wydajnościowych.

Spring Framework 5.0 wprowadził rewolucyjną zmianę – obok klasycznego Spring WebMVC otrzymaliśmy Spring WebFlux, całkowicie nowy reactive web stack. Po dwóch latach doświadczeń produkcyjnych nadszedł czas na szczere porównanie obu podejść.

Spring WebFlux nie zastępuje Spring WebMVC – to dwa równoległe podejścia do rozwiązywania różnych problemów w aplikacjach webowych.

## Dlaczego to ważne?

Wybór między WebMVC a WebFlux to decyzja architektoniczna, która wpłynie na cały projekt. Błędny wybór może oznaczać problemy z wydajnością, skomplikowane debugowanie lub konieczność przepisania aplikacji. W 2019 roku wiele firm przeszło przez bolesne migracje między tymi technologiami.

Co się nauczysz:

  • Fundamentalne różnice między WebMVC a WebFlux
  • Kiedy wybrać każde z rozwiązań
  • Performance characteristics obu technologii
  • Praktyczne przykłady implementacji
  • Migration path między technologiami
  • Common pitfalls i jak ich uniknąć
Wymagania wstępne: Znajomość Spring Boot, podstawy HTTP, doświadczenie z REST API. Pomocna znajomość reactive programming concepts.

## Spring WebMVC – Sprawdzony klasyk

Spring WebMVC to synchroniczny, servlet-based web framework działający na tradycyjnym modelu „one thread per request”. Każde żądanie HTTP obsługiwane jest przez dedykowany wątek z puli.

@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public ResponseEntity getUser(@PathVariable Long id) {
        // Synchroniczne wywołanie - blokuje wątek
        User user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    public ResponseEntity createUser(@RequestBody CreateUserRequest request) {
        // Tradycyjne, imperatywne programowanie
        User user = userService.create(request);
        return ResponseEntity.status(HttpStatus.CREATED).body(user);
    }
}

### Zalety WebMVC:

Pro tip: WebMVC to bezpieczny wybór dla 90% aplikacji biznesowych. Debugging jest prostszy, documentation obszerna, a zespół nie potrzebuje dodatkowego szkolenia.

– **Mature ecosystem:** Ogromna baza wiedzy, dokumentacji i przykładów
– **Predictable debugging:** Stack traces są czytelne, debugging straightforward
– **Thread-local storage:** Możliwość używania ThreadLocal, security context
– **Blocking I/O:** Prosty model programowania, łatwy do zrozumienia
– **Servlet filters:** Pełna kompatybilność z servlet API

## Spring WebFlux – Reactive revolution

WebFlux oparty jest na Project Reactor i non-blocking I/O. Używa event loop model z małą liczbą wątków do obsługi tysięcy równoczesnych połączeń.

@RestController
@RequestMapping("/api/users")
public class ReactiveUserController {
    
    @Autowired
    private ReactiveUserService userService;
    
    @GetMapping("/{id}")
    public Mono> getUser(@PathVariable Long id) {
        // Non-blocking - zwraca Mono (reactive stream)
        return userService.findById(id)
            .map(user -> ResponseEntity.ok(user))
            .defaultIfEmpty(ResponseEntity.notFound().build());
    }
    
    @GetMapping
    public Flux getAllUsers() {
        // Streaming response - dane wysyłane na bieżąco
        return userService.findAll()
            .delayElements(Duration.ofMillis(100)); // Symulacja delay
    }
    
    @PostMapping
    public Mono> createUser(@RequestBody Mono request) {
        return request
            .flatMap(userService::create)
            .map(user -> ResponseEntity.status(HttpStatus.CREATED).body(user));
    }
}

### Reactive Service Layer:

@Service
public class ReactiveUserService {
    
    @Autowired
    private ReactiveUserRepository userRepository;
    
    @Autowired
    private WebClient externalApiClient;
    
    public Mono findById(Long id) {
        return userRepository.findById(id)
            .switchIfEmpty(Mono.error(new UserNotFoundException(id)));
    }
    
    public Mono create(CreateUserRequest request) {
        // Composition of multiple reactive operations
        return validateUser(request)
            .then(checkUserExists(request.getEmail()))
            .then(userRepository.save(User.from(request)))
            .flatMap(this::enrichUserData);
    }
    
    private Mono enrichUserData(User user) {
        // Non-blocking external API call
        return externalApiClient
            .get()
            .uri("/profile/{email}", user.getEmail())
            .retrieve()
            .bodyToMono(UserProfile.class)
            .map(profile -> user.withProfile(profile))
            .onErrorReturn(user); // Graceful degradation
    }
}

## Performance porównanie

MetrykaWebMVCWebFluxKomentarz
Throughput (low latency)~15,000 req/s~12,000 req/sWebMVC szybszy dla prostych operacji
Throughput (high latency)~2,000 req/s~25,000 req/sWebFlux znacznie lepszy z I/O wait
Memory usage~200MB~150MBWebFlux mniej pamięci na wątek
CPU usageŚrednieNiskieWebFlux lepiej wykorzystuje CPU
Latency consistencyGorszeLepszeWebFlux bardziej predictable

### Kiedy WebFlux osiąga przewagę:

WebFlux świeci przy I/O-intensive operations: wywołania zewnętrznych API, operacje bazodanowe, file operations. Im więcej czasu wątek spędza czekając, tym większa przewaga reactive approach.

## Practical decision matrix

### Wybierz WebMVC gdy:

Typowy błąd: Wybieranie WebFlux „bo brzmi nowocześnie”. Reactive programming ma steep learning curve i może spowalniać development zespołu bez doświadczenia.

– **Zespół bez doświadczenia reactive** – learning curve jest significant
– **Aplikacja CRUD-heavy** – typowe operacje bazodanowe
– **Prostota ważniejsza od performance** – easier debugging and maintenance
– **Blocking dependencies** – JDBC drivers, legacy libraries
– **Thread-local requirements** – security context, audit trails

### Wybierz WebFlux gdy:

– **High-concurrency requirements** – tysięce równoczesnych połączeń
– **I/O intensive operations** – dużo external API calls
– **Streaming data** – real-time feeds, server-sent events
– **Microservices communication** – service-to-service calls
– **Team ma reactive experience** – znają Reactor, RxJava

## Integration considerations

### Database access:

// WebMVC - traditional JPA
@Repository
public interface UserRepository extends JpaRepository {
    Optional findByEmail(String email);
}

// WebFlux - reactive repository
@Repository
public interface ReactiveUserRepository extends ReactiveCrudRepository {
    Mono findByEmail(String email);
    Flux findByStatus(UserStatus status);
}
Uwaga: WebFlux z R2DBC (reactive database connectivity) to wciąż young technology. JPA + WebFlux = blocking operations w thread pool, co niweluje korzyści reactive.

### Testing approaches:

// WebMVC testing
@SpringBootTest
@AutoConfigureTestDatabase
class UserControllerTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    void shouldReturnUserById() {
        ResponseEntity response = restTemplate
            .getForEntity("/api/users/1", User.class);
        
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody().getId()).isEqualTo(1L);
    }
}

// WebFlux testing  
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ReactiveUserControllerTest {
    
    @Autowired
    private WebTestClient webTestClient;
    
    @Test
    void shouldReturnUserById() {
        webTestClient
            .get()
            .uri("/api/users/1")
            .accept(MediaType.APPLICATION_JSON)
            .exchange()
            .expectStatus().isOk()
            .expectBody(User.class)
            .value(user -> assertThat(user.getId()).isEqualTo(1L));
    }
}

## Migration strategies

### Stopniowa migracja WebMVC → WebFlux:

1. **Zaczynaj od nowych features** – nie ruszaj działającego kodu
2. **WebClient zamiast RestTemplate** – wprowadź reactive HTTP client
3. **Reactive repositories** – R2DBC lub reactive MongoDB
4. **Separate reactive modules** – izoluj reactive code
5. **Testing strategy** – WebTestClient vs MockMvc

Pro tip: Możesz mieć WebMVC i WebFlux w jednej aplikacji. Spring Boot 2.x obsługuje hybrid approach – różne endpoints mogą używać różnych technologii.

## Common pitfalls

### WebFlux mistakes:

Pułapka: Blocking calls w reactive chain. Wywołanie .block() w reactive stream niszczy całą non-blocking nature.
// ❌ BŁĄD - blocking w reactive chain
public Mono getUserWithProfile(Long id) {
    return userRepository.findById(id)
        .map(user -> {
            // To blokuje cały reactive pipeline!
            Profile profile = externalClient.getProfile(user.getId()).block();
            return user.withProfile(profile);
        });
}

// ✅ POPRAWNIE - compose reactive operations  
public Mono getUserWithProfile(Long id) {
    return userRepository.findById(id)
        .flatMap(user -> externalClient.getProfile(user.getId())
            .map(user::withProfile));
}

### WebMVC performance traps:

// ❌ BŁĄD - synchronous calls w loop
@GetMapping("/users-with-profiles")
public List getUsersWithProfiles() {
    List users = userService.findAll();
    return users.stream()
        .map(user -> {
            // N+1 problem - każde wywołanie blokuje wątek
            Profile profile = profileService.getProfile(user.getId());
            return new UserWithProfile(user, profile);
        })
        .collect(toList());
}

// ✅ POPRAWNIE - batch operations
@GetMapping("/users-with-profiles")  
public List getUsersWithProfiles() {
    List users = userService.findAll();
    List userIds = users.stream().map(User::getId).collect(toList());
    Map profiles = profileService.getProfilesBatch(userIds);
    
    return users.stream()
        .map(user -> new UserWithProfile(user, profiles.get(user.getId())))
        .collect(toList());
}
Czy WebFlux zawsze jest szybszy od WebMVC?

Nie. WebFlux jest szybszy przy high-concurrency i I/O-heavy operations. Dla prostych, CPU-intensive operations WebMVC może być szybszy ze względu na mniejszy overhead.

Czy mogę używać JPA z WebFlux?

Technicznie tak, ale JPA jest blocking API. Będziesz musiał użyć subscribeOn() z dedicated thread pool, co niweluje korzyści reactive approach. Lepiej użyć R2DBC.

Jak debugować WebFlux applications?

Debugging reactive streams jest challenging. Użyj .doOnNext(), .doOnError() dla logging, Reactor debugging tools, i consider BlockHound do wykrywania blocking calls.

Czy WebFlux obsługuje sessions?

WebFlux może używać reactive sessions przez WebSession, ale preferowany approach to stateless authentication (JWT tokens). Session affinity komplikuje scaling reactive applications.

Kiedy warto migrować z WebMVC do WebFlux?

Gdy masz konkretne problemy z performance/scalability, dużo I/O operations, albo potrzebujesz streaming capabilities. Nie migruj „bo tak” – reactive programming ma steep learning curve.

Czy WebFlux zużywa mniej pamięci?

Tak, WebFlux używa mniej wątków (typowo 2x CPU cores vs setki w WebMVC), więc mniej stack memory. Ale reactive objects (Mono/Flux) mają swój overhead – korzyść widać przy high concurrency.

Czy mogę mieć WebMVC i WebFlux w jednej aplikacji?

Tak, Spring Boot 2.x pozwala na hybrid approach. Możesz mieć @RestController (WebMVC) i RouterFunction (WebFlux) w tej samej aplikacji, ale na różnych portach.

## Przydatne zasoby

– [Spring WebFlux Documentation](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html)
– [Project Reactor Reference Guide](https://projectreactor.io/docs/core/release/reference/)
– [R2DBC Specification](https://r2dbc.io/spec/0.8.1.RELEASE/spec/html/)
– [Reactive Streams Specification](http://www.reactive-streams.org/)
– [WebFlux Performance Benchmarks](https://github.com/reactor/reactor-benchmark)

🚀 Zadanie dla Ciebie

Stwórz prostą aplikację porównującą WebMVC i WebFlux:

  1. Endpoint zwracający listę użytkowników (synchroniczny w WebMVC)
  2. Ten sam endpoint w WebFlux z Flux
  3. Dodaj sztuczny delay (Thread.sleep vs Mono.delay)
  4. Przetestuj performance przy 100 równoczesnych requestach
  5. Zmierz memory usage i response times

Wykorzystaj Spring Boot 2.2+, WebTestClient do testów, i narzędzia jak JMeter lub Gatling do load testingu.

Która technologia sprawdzi się lepiej w Twoim następnym projekcie? Podziel się swoimi doświadczeniami w komentarzach!

Zostaw komentarz

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

Przewijanie do góry