Module 08 · Executable guide

Spring WebFlux

Xây reactive backend trong Spring ecosystem với controllers, WebClient, R2DBC, streaming, security và testing.


1. Architecture

Client → Reactor Netty → WebFlux → Controller/Handler → Service
                                      ↓
                              R2DBC / WebClient

WebFlux chỉ non-blocking end-to-end khi downstream không block hoặc blocking work được cô lập đúng.

2. Annotated Controller

@RestController
@RequestMapping("/users")
class UserController {
    @GetMapping("/{id}")
    Mono<UserDto> get(@PathVariable long id) {
        return service.find(id);
    }

    @GetMapping
    Flux<UserDto> list() {
        return service.findAll();
    }
}

3. Functional Endpoints

RouterFunction<ServerResponse> routes(UserHandler h) {
    return route()
        .GET("/users/{id}", h::get)
        .POST("/users", h::create)
        .build();
}

4. WebClient

return webClient.post()
    .uri("/payments")
    .bodyValue(req)
    .retrieve()
    .onStatus(HttpStatusCode::is5xxServerError,
        response -> Mono.error(new PaymentUnavailable()))
    .bodyToMono(PaymentResponse.class)
    .timeout(Duration.ofSeconds(2));

5. R2DBC

interface OrderRepository
    extends ReactiveCrudRepository<OrderEntity, Long> {
    Flux<OrderEntity> findByUserId(long userId);
}
WebFlux + JDBC không tự nhiên thành reactive. JDBC blocking trên event loop có thể gây latency.

6. Reactive transaction

Reactive transaction context không nên dựa vào ThreadLocal như imperative flow vì pipeline có thể đổi execution context.

7. Error handling

Service layer xử lý domain recovery bằng operators; HTTP boundary map domain errors thành 400/404/409/503 phù hợp.

8. Security

Reactive security chain cũng phải tránh blocking authentication lookup trong request path.

9. SSE

@GetMapping(value="/orders/{id}/events",
            produces=MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<ServerSentEvent<OrderEvent>> events(@PathVariable long id) {
    return eventService.stream(id)
        .map(e -> ServerSentEvent.builder(e).build());
}

10. Testing

webTestClient.get()
    .uri("/users/42")
    .exchange()
    .expectStatus().isOk()
    .expectBody()
    .jsonPath("$.id").isEqualTo(42);

11. Anti-patterns

  • block() trong request path.
  • Blocking I/O trên Reactor Netty event loop.
  • Retry payment không idempotent.
  • Dùng parallel scheduler cho blocking call.

12. Lab

  1. GET /users/{id}/profile.
  2. Load user bằng R2DBC.
  3. Gọi Order Service bằng WebClient.
  4. Zip thành profile.
  5. Timeout 700 ms.
  6. Fallback orders=[] khi downstream timeout.
  7. Test 200/404/timeout.