正在加载,请稍候…

Spring Boot 3 + WebFlux: Reactive APIs with Project Reactor

Build non-blocking reactive REST APIs with Spring WebFlux — Mono/Flux, reactive repositories, backpressure, error handling, testing, and R2DBC.

Why Reactive Programming?

Traditional Spring MVC blocks threads per request. With WebFlux, a small thread pool handles thousands of concurrent connections using non-blocking I/O.

Project Setup

<!-- pom.xml -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
    <groupId>io.r2dbc</groupId>
    <artifactId>r2dbc-postgresql</artifactId>
</dependency>

Mono and Flux Basics

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

// Mono: 0 or 1 item
Mono<String> mono = Mono.just("Hello");
Mono<String> empty = Mono.empty();
Mono<String> error = Mono.error(new RuntimeException("Error"));

// Flux: 0 to N items
Flux<Integer> flux = Flux.just(1, 2, 3, 4, 5);
Flux<String> fromList = Flux.fromIterable(List.of("a", "b", "c"));
Flux<Long> interval = Flux.interval(Duration.ofSeconds(1));

// Operators
flux
  .filter(n -> n % 2 == 0)
  .map(n -> n * 10)
  .take(3)
  .subscribe(System.out::println);

Reactive Controller

@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserService userService;

    @GetMapping
    public Flux<UserDto> getAllUsers(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "20") int size) {
        return userService.findAll(page, size);
    }

    @GetMapping("/{id}")
    public Mono<ResponseEntity<UserDto>> getUserById(@PathVariable String id) {
        return userService.findById(id)
                .map(user -> ResponseEntity.ok(user))
                .defaultIfEmpty(ResponseEntity.notFound().build());
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Mono<UserDto> createUser(@RequestBody @Valid Mono<CreateUserRequest> request) {
        return request.flatMap(userService::create);
    }

    @GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
    public Flux<UserDto> streamUsers() {
        return userService.streamAll()
                .delayElements(Duration.ofMillis(100));
    }
}

R2DBC Reactive Repository

// Repository interface
public interface UserRepository extends ReactiveCrudRepository<User, Long> {
    
    Flux<User> findByEmailContaining(String email);
    
    @Query("SELECT * FROM users WHERE role = :role AND active = true")
    Flux<User> findActiveByRole(String role);
    
    Mono<Long> countByActive(boolean active);
}

// Entity
@Table("users")
public class User {
    @Id
    private Long id;
    private String name;
    private String email;
    private String role;
    private boolean active;
    // getters/setters
}

Service Layer with Error Handling

@Service
public class UserService {

    private final UserRepository userRepository;
    private final UserMapper mapper;

    public Mono<UserDto> findById(String id) {
        return userRepository.findById(Long.parseLong(id))
                .map(mapper::toDto)
                .switchIfEmpty(Mono.error(
                    new UserNotFoundException("User not found: " + id)
                ));
    }

    public Mono<UserDto> create(CreateUserRequest request) {
        return userRepository.existsByEmail(request.email())
                .flatMap(exists -> {
                    if (exists) {
                        return Mono.error(new DuplicateEmailException(request.email()));
                    }
                    User user = mapper.toEntity(request);
                    return userRepository.save(user);
                })
                .map(mapper::toDto);
    }

    public Flux<UserDto> findAll(int page, int size) {
        return userRepository.findAll()
                .skip((long) page * size)
                .take(size)
                .map(mapper::toDto);
    }
}

Global Error Handler

@Component
public class GlobalErrorHandler extends DefaultErrorWebExceptionHandler {

    @Override
    protected Map<String, Object> getErrorAttributes(
            ServerRequest request, ErrorAttributeOptions options) {
        Map<String, Object> attrs = super.getErrorAttributes(request, options);
        Throwable error = getError(request);
        
        if (error instanceof UserNotFoundException) {
            attrs.put("status", 404);
            attrs.put("message", error.getMessage());
        } else if (error instanceof DuplicateEmailException) {
            attrs.put("status", 409);
            attrs.put("message", error.getMessage());
        }
        
        return attrs;
    }
}

Backpressure with Limitrate

Flux.range(1, 1000)
    .onBackpressureBuffer(100)     // Buffer up to 100 items
    .limitRate(10)                  // Request 10 at a time
    .delayElements(Duration.ofMillis(50))
    .subscribe(
        item -> processItem(item),
        err -> log.error("Error", err),
        () -> log.info("Complete")
    );

WebClient (Reactive HTTP Client)

WebClient client = WebClient.builder()
    .baseUrl("https://api.example.com")
    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
    .filter(ExchangeFilterFunctions.basicAuthentication("user", "pass"))
    .build();

Mono<User> user = client.get()
    .uri("/users/{id}", 42)
    .retrieve()
    .onStatus(HttpStatus::is4xxClientError, response ->
        Mono.error(new ClientException(response.statusCode())))
    .bodyToMono(User.class)
    .retryWhen(Retry.fixedDelay(3, Duration.ofSeconds(1)))
    .timeout(Duration.ofSeconds(5));

Testing Reactive Code

@WebFluxTest(UserController.class)
class UserControllerTest {

    @Autowired
    private WebTestClient webTestClient;

    @MockBean
    private UserService userService;

    @Test
    void shouldGetUserById() {
        UserDto mockUser = new UserDto(1L, "John", "john@example.com");
        when(userService.findById("1")).thenReturn(Mono.just(mockUser));

        webTestClient.get()
            .uri("/api/users/1")
            .exchange()
            .expectStatus().isOk()
            .expectBody(UserDto.class)
            .isEqualTo(mockUser);
    }

    @Test
    void shouldReturn404WhenNotFound() {
        when(userService.findById("999"))
            .thenReturn(Mono.error(new UserNotFoundException("999")));

        webTestClient.get()
            .uri("/api/users/999")
            .exchange()
            .expectStatus().isNotFound();
    }
}

When to Use WebFlux vs MVC

Scenario Choice
High concurrency, I/O bound WebFlux
CRUD with blocking DB drivers MVC
Streaming data WebFlux
Team familiar with reactive WebFlux
Simple REST, quick development MVC