УРОК 27 / 30 0%

WebFlux (реактивный Spring Boot) — кратко

🎯

Цель урока

Сделать полноценный CRUD для задач с правильными HTTP-статусами (201/204/404), обработкой ошибок, и подготовить Task к подключению JPA.

🧠

Теория · для собеса

1 Императивный vs Реактивный стек

Императивный (Spring MVC, JDBC, JPA):


Thread-1: [read DB] (блокируется 50ms)
Thread-1: [process]
Thread-1: [return response]

1 запрос = 1 поток. Поток блокируется на I/O. На 1000 одновременных запросов нужно 1000 потоков (или пул).

Реактивный (WebFlux, R2DBC):


Thread-1: [start read] → release thread → ...
  ... 50ms later ... →
Thread-X:  [callback: process]
Thread-X:  [callback: return response]

Много запросов на мало потоков. Поток не блокируется, I/O — асинхронный. На 1000 запросов хватит 10-50 потоков.

2 Project Reactor

Reactor — реактивная библиотека для JVM.

  • Mono<T> — 0 или 1 элемент (аналог Optional<T>)
  • Flux<T> — 0..N элементов (аналог List<T>)

Mono findById(Long id);          // optional
Flux findAll();                   // stream
3 Когда использовать WebFlux

Много I/O-bound запросов (микросервисы, streaming, WebSocket)

Ограниченные ресурсы (нужно держать много соединений на мало RAM)

Backpressure (медленный consumer не залипает)

CPU-bound задачи (реактивность не поможет)

Простые CRUD (лишняя сложность, нет выигрыша)

Команда не готова (сложнее дебажить, читать stack trace)

⚠️ Важно: WebFlux ≠ быстрее. Он эффективнее по памяти и потокам, но не по latency. И он сильно сложнее.

---

💻

Практика: улучшаем CRUD

  1. 1
    Зависимости
    
    dependencies {
        implementation("org.springframework.boot:spring-boot-starter-webflux")
        implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
        runtimeOnly("io.asyncer:r2dbc-postgresql")
        runtimeOnly("org.postgresql:postgresql")  // для миграций Flyway
        implementation("org.flywaydb:flyway-core")
    }
    
  2. 2
    model/Task.java (R2DBC Entity)
    
    package com.taskflow.model;
    
    import org.springframework.data.annotation.Id;
    import org.springframework.data.relational.core.mapping.Table;
    
    import java.time.Instant;
    
    @Table("tasks")
    public class Task {
        @Id
        private Long id;
        private String title;
        private String description;
        private boolean done;
        private Long ownerId;
        private Instant createdAt;
        private Instant updatedAt;
    
        // getters / setters / constructors
    }
    
  3. 3
    repository/TaskRepository.java (R2DBC)
    
    package com.taskflow.repository;
    
    import com.taskflow.model.Task;
    import org.springframework.data.repository.reactive.ReactiveCrudRepository;
    import reactor.core.publisher.Flux;
    import reactor.core.publisher.Mono;
    
    public interface TaskRepository extends ReactiveCrudRepository {
        Flux findByOwnerId(Long ownerId);
        Mono findByIdAndOwnerId(Long id, Long ownerId);
    }
    
  4. 4
    controller/TaskController.java (WebFlux)
    
    @RestController
    @RequestMapping("/api/tasks")
    public class TaskController {
        private final TaskRepository repo;
        private final CurrentUserReactive currentUser;
    
        @GetMapping
        public Flux findAll() {
            return currentUser.get()
                .flatMapMany(user -> user.getRole() == User.Role.ADMIN
                    ? repo.findAll()
                    : repo.findByOwnerId(user.getId()))
                .map(TaskResponse::from);
        }
    
        @GetMapping("/{id}")
        public Mono> findById(@PathVariable Long id) {
            return currentUser.get()
                .flatMap(user -> repo.findById(id)
                    .filter(task -> task.getOwnerId().equals(user.getId()) || user.getRole() == User.Role.ADMIN)
                    .map(ResponseEntity::ok)
                    .defaultIfEmpty(ResponseEntity.notFound().build()));
        }
    
        @PostMapping
        @ResponseStatus(HttpStatus.CREATED)
        public Mono create(@Valid @RequestBody CreateTaskRequest req) {
            return currentUser.get()
                .flatMap(user -> {
                    Task task = new Task(null, req.title(), req.description(), false, user.getId(), Instant.now(), Instant.now());
                    return repo.save(task);
                })
                .map(TaskResponse::from);
        }
    
        @DeleteMapping("/{id}")
        public Mono> delete(@PathVariable Long id) {
            return currentUser.get()
                .flatMap(user -> repo.findByIdAndOwnerId(id, user.getId())
                    .flatMap(task -> repo.delete(task).thenReturn(ResponseEntity.noContent().build()))
                    .defaultIfEmpty(ResponseEntity.notFound().build()));
        }
    }
    
  5. 5
    security/CurrentUserReactive.java (реактивный)
    
    @Component
    public class CurrentUserReactive {
        public Mono get() {
            return ReactiveSecurityContextHolder.getContext()
                .map(SecurityContext::getAuthentication)
                .filter(Authentication::isAuthenticated)
                .map(Authentication::getName)
                .flatMap(username -> userRepository.findByUsername(username));
        }
    }
    
  6. 6
    Преимущества
    
    # 10 000 одновременных запросов на WebFlux
    # Память: ~200 MB, потоки: ~50
    # vs Spring MVC: ~1.5 GB, потоки: 200 (Tomcat по умолчанию)
    

    ---

🎯

Зачем это на собесе

После урока ты должен уметь ответить на:

Для собеса: знать, что такое WebFlux, Mono/Flux, R2DBC, чем отличается от MVC. Сказать, когда выбрал бы.
Для production: микросервисы, API-gateway, streaming (Kafka consumer в WebFlux), WebSocket.
Netflix, LinkedIn используют реактивный стек для высоконагруженных сервисов.

5 вопросов на углубление

Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.

1
Что такое backpressure в WebFlux?

Разбор внутри: медленный consumer и контроль потока данных.

2
Mono.zip vs zipWith — когда что использовать?

Разбор внутри: объединение нескольких Mono и работа с tuple.

3
map vs flatMap в Reactor

Разбор внутри: синхронное преобразование и асинхронная операция, возвращающая Mono.

4
Почему @Transactional иначе работает в WebFlux?

Разбор внутри: ThreadLocal в MVC и реактивный контекст.

5
Почему блокирующий код ломает WebFlux?

Разбор внутри: event loop, JDBC/Thread.sleep и boundedElastic.

Готов идти дальше?

Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.

🚀 Перейти к Уроку 28

Сначала пройди все секции и выбери хотя бы 1 вопрос

⬅️ Назад к Уроку 26
🎉
Новый тир
Новый тир достигнут!