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Зависимости
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
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
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
controller/TaskController.java(WebFlux)@RestController @RequestMapping("/api/tasks") public class TaskController { private final TaskRepository repo; private final CurrentUserReactive currentUser; @GetMapping public FluxfindAll() { 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
security/CurrentUserReactive.java(реактивный)@Component public class CurrentUserReactive { public Monoget() { return ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) .filter(Authentication::isAuthenticated) .map(Authentication::getName) .flatMap(username -> userRepository.findByUsername(username)); } } -
6Преимущества
# 10 000 одновременных запросов на WebFlux # Память: ~200 MB, потоки: ~50 # vs Spring MVC: ~1.5 GB, потоки: 200 (Tomcat по умолчанию)---
Зачем это на собесе
После урока ты должен уметь ответить на:
5 вопросов на углубление
Раскрой вопрос и нажми «🤔 Хочу разобрать подробнее» — он попадёт в страницу ответов.
Разбор внутри: медленный consumer и контроль потока данных.
Разбор внутри: объединение нескольких Mono и работа с tuple.
Разбор внутри: синхронное преобразование и асинхронная операция, возвращающая Mono.
Разбор внутри: ThreadLocal в MVC и реактивный контекст.
Разбор внутри: event loop, JDBC/Thread.sleep и boundedElastic.
Готов идти дальше?
Выбери вопросы, которые тебе интересны, и изучи их. Потом — к следующему уроку.
🚀 Перейти к Уроку 28Сначала пройди все секции и выбери хотя бы 1 вопрос
⬅️ Назад к Уроку 26