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ść.
## 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ąć
## 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 ResponseEntitygetUser(@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:
– **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 MonofindById(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
Metryka | WebMVC | WebFlux | Komentarz |
---|---|---|---|
Throughput (low latency) | ~15,000 req/s | ~12,000 req/s | WebMVC szybszy dla prostych operacji |
Throughput (high latency) | ~2,000 req/s | ~25,000 req/s | WebFlux znacznie lepszy z I/O wait |
Memory usage | ~200MB | ~150MB | WebFlux mniej pamięci na wątek |
CPU usage | Średnie | Niskie | WebFlux lepiej wykorzystuje CPU |
Latency consistency | Gorsze | Lepsze | WebFlux bardziej predictable |
### Kiedy WebFlux osiąga przewagę:
## Practical decision matrix
### Wybierz WebMVC gdy:
– **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); }
### Testing approaches:
// WebMVC testing @SpringBootTest @AutoConfigureTestDatabase class UserControllerTest { @Autowired private TestRestTemplate restTemplate; @Test void shouldReturnUserById() { ResponseEntityresponse = 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
## Common pitfalls
### WebFlux mistakes:
// ❌ BŁĄD - blocking w reactive chain public MonogetUserWithProfile(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 ListgetUsersWithProfiles() { 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()); }
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.
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.
Debugging reactive streams jest challenging. Użyj .doOnNext(), .doOnError() dla logging, Reactor debugging tools, i consider BlockHound do wykrywania blocking calls.
WebFlux może używać reactive sessions przez WebSession, ale preferowany approach to stateless authentication (JWT tokens). Session affinity komplikuje scaling reactive applications.
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.
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.
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:
- Endpoint zwracający listę użytkowników (synchroniczny w WebMVC)
- Ten sam endpoint w WebFlux z Flux
- Dodaj sztuczny delay (Thread.sleep vs Mono.delay)
- Przetestuj performance przy 100 równoczesnych requestach
- 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!